Files
PartsInquiry/frontend/pages/product/product-detail.vue
2025-10-08 19:15:20 +08:00

254 lines
7.9 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>
<scroll-view scroll-y class="page" v-if="detail">
<view class="header">
<text class="model">{{ detail.model }}</text>
<text v-if="detail.deleted" class="status deleted">已删除</text>
</view>
<view class="section">
<view class="row"><text class="label">名称</text><text class="value">{{ detail.name || '-' }}</text></view>
<view class="row"><text class="label">品牌</text><text class="value">{{ detail.brand || '-' }}</text></view>
<view class="row"><text class="label">型号</text><text class="value">{{ detail.model || '-' }}</text></view>
<view class="row"><text class="label">条码</text><text class="value">{{ detail.barcode || '-' }}</text></view>
<view class="row"><text class="label">类别</text><text class="value">{{ categoryName }}</text></view>
<view class="row"><text class="label">模板</text><text class="value">{{ templateName }}</text></view>
<view class="row" v-if="detail.externalCode"><text class="label">编号</text><text class="value">{{ detail.externalCode }}</text></view>
</view>
<view class="section">
<view class="block-title">参数</view>
<view v-if="labeledPairs.length" class="params">
<view class="param" v-for="item in labeledPairs" :key="item.key">
<text class="param-key">{{ item.label }}<text v-if="item.unit">{{ item.unit }}</text></text>
<text class="param-val">{{ item.value }}</text>
</view>
</view>
<view v-else class="placeholder">未填写参数</view>
</view>
<view class="section">
<view class="block-title">图片</view>
<view v-if="detail.images && detail.images.length" class="images">
<image v-for="(img, idx) in detail.images" :key="idx" :src="img.url || img" class="image" mode="aspectFill" @click="preview(idx)" />
</view>
<view v-else class="placeholder">未上传图片</view>
</view>
<view class="section">
<view class="block-title">备注</view>
<view class="placeholder">{{ detail.remark || '无' }}</view>
</view>
<view class="footer">
<button size="mini" @click="back">返回</button>
<button size="mini" type="warn" @click="remove">删除</button>
</view>
</scroll-view>
<view v-else class="loading">加载中...</view>
</template>
<script>
import { get, del } from '../../common/http.js'
export default {
data() {
return { id: '', detail: null, categoryName: '-', templateName: '-' }
},
async onLoad(query) {
this.id = query?.id || ''
if (!this.id) { uni.showToast({ title: '参数缺失', icon: 'none' }); return }
await this.preloadDictionaries()
await this.loadDetail()
},
methods: {
async preloadDictionaries() {
try {
const needCats = !Array.isArray(uni.getStorageSync('CACHE_CATEGORIES'))
const needTpls = !Array.isArray(uni.getStorageSync('CACHE_TEMPLATES'))
if (!needCats && !needTpls) return
const reqs = []
if (needCats) reqs.push(get('/api/product-categories'))
if (needTpls) reqs.push(get('/api/product-templates'))
const res = await Promise.all(reqs)
let idx = 0
if (needCats) { const r = res[idx++]; const list = Array.isArray(r?.list)?r.list:(Array.isArray(r)?r:[]); uni.setStorageSync('CACHE_CATEGORIES', list) }
if (needTpls) { const r = res[idx++]; const list = Array.isArray(r?.list)?r.list:(Array.isArray(r)?r:[]); uni.setStorageSync('CACHE_TEMPLATES', list) }
} catch (_) {}
},
async loadDetail() {
try {
const data = await get('/api/products/' + this.id)
this.detail = data
this.categoryName = this.categoryLookup(data.categoryId)
this.templateName = this.templateLookup(data.templateId)
} catch (e) { uni.showToast({ title: e?.message || '加载失败', icon: 'none' }) }
},
preview(idx) {
try { const list = (this.detail?.images||[]).map(i => i.url || i); uni.previewImage({ urls: list, current: idx }) } catch (_) {}
},
categoryLookup(id) {
try { const list = uni.getStorageSync('CACHE_CATEGORIES') || []; const f = list.find(x => String(x.id)===String(id)); return f?f.name:'-'} catch(_){return'-'}
},
templateLookup(id) {
try { const list = uni.getStorageSync('CACHE_TEMPLATES') || []; const f = list.find(x => String(x.id)===String(id)); return f?f.name:'-'} catch(_){return'-'}
},
async remove() {
try {
const r = await new Promise(resolve => { uni.showModal({ content: '确认删除该货品?删除后可在后台恢复', success: resolve }) })
if (!r || !r.confirm) return
await del('/api/products/' + this.id)
uni.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => uni.navigateBack(), 400)
} catch (e) { uni.showToast({ title: '删除失败', icon: 'none' }) }
},
back(){ uni.navigateBack({ delta: 1 }) }
},
computed: {
labeledPairs() {
const params = this.detail?.parameters
if (!params || typeof params !== 'object') return []
let labelMap = {}, unitMap = {}
try {
const templates = uni.getStorageSync('CACHE_TEMPLATES') || []
const tpl = templates.find(t => String(t.id) === String(this.detail?.templateId))
if (tpl && Array.isArray(tpl.params)) for (const p of tpl.params) { labelMap[p.fieldKey] = p.fieldLabel; unitMap[p.fieldKey] = p.unit }
} catch (_) {}
return Object.keys(params).map(k => ({ key: k, label: labelMap[k] || k, unit: unitMap[k] || '', value: params[k] }))
}
}
}
</script>
<style lang="scss">
.page {
min-height: 100vh;
padding: 20rpx 24rpx 160rpx;
background: #f6f7fb;
box-sizing: border-box;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
gap: 16rpx;
flex-wrap: wrap;
}
.model {
font-size: 36rpx;
font-weight: 700;
color: #2d3a4a;
word-break: break-word;
overflow-wrap: break-word;
flex: 1;
min-width: 0;
}
.status.deleted {
font-size: 24rpx;
padding: 6rpx 18rpx;
border-radius: 999rpx;
background: #c0c4cc;
color: #fff;
white-space: nowrap;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 20rpx 22rpx;
margin-bottom: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.04);
}
.row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid #f1f2f5;
}
.row:last-child { border-bottom: none; }
.label {
flex-shrink: 0;
width: 140rpx;
font-size: 26rpx;
color: #7a8899;
}
.value {
flex: 1;
min-width: 0;
text-align: right;
font-size: 26rpx;
color: #2d3a4a;
word-break: break-word;
overflow-wrap: break-word;
}
.block-title {
font-size: 28rpx;
font-weight: 600;
color: #2d3a4a;
margin-bottom: 12rpx;
}
.placeholder {
font-size: 26rpx;
color: #7a8899;
word-break: break-word;
}
.params {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.param {
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: 16rpx;
font-size: 26rpx;
color: #2d3a4a;
}
.param-key {
flex-shrink: 0;
color: #7a8899;
min-width: 160rpx;
}
.param-val {
flex: 1;
min-width: 0;
text-align: left;
word-break: break-word;
overflow-wrap: break-word;
}
.images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.image {
width: 100%;
height: 200rpx;
border-radius: 16rpx;
background: #f0f2f5;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: flex-end;
gap: 20rpx;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: rgba(255,255,255,0.96);
box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.08);
}
.loading {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #7a8899;
}
</style>