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

278 lines
12 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 class="tabs">
<view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view>
<view class="tab" :class="{active: tab==='search'}" @click="switchTab('search')">查询</view>
<view class="tab extra" @click="goMySubmissions">我的提交</view>
</view>
<view class="search" :class="{ 'template-mode': query.mode==='template' }" v-if="tab==='search'">
<view class="mode">
<picker mode="selector" :range="['直接查询','名称模糊查询','按模板参数查询']" @change="e => (query.mode = ['direct','nameLike','template'][Number(e.detail.value)] || 'direct')">
<view class="picker">{{ modeLabel }}</view>
</picker>
</view>
<block v-if="query.mode==='direct' || query.mode==='nameLike'">
<input v-model.trim="query.kw" placeholder="输入名称/条码/规格查询" @confirm="reload" />
</block>
<block v-if="query.mode==='template'">
<view class="picker-row">
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">{{ categoryLabel }}</view>
</picker>
<picker mode="selector" :range="templateNames" @change="onPickTemplate">
<view class="picker">{{ templateLabel }}</view>
</picker>
</view>
<view class="params-wrap">
<view class="param-row" v-for="(p,idx) in selectedTemplateParams" :key="p.fieldKey">
<input v-if="p.type==='string'" v-model.trim="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
<input v-else-if="p.type==='number'" type="number" v-model.number="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
<switch v-else-if="p.type==='boolean'" :checked="!!paramValues[p.fieldKey]" @change="onParamBoolChange(p, $event)" />
<picker v-else-if="p.type==='enum'" mode="selector" :range="p.enumOptions||[]" @change="onPickParamEnumWrapper(p, $event)">
<view class="picker">{{ displayParamEnum(p) }}</view>
</picker>
<picker v-else-if="p.type==='date'" mode="date" @change="onParamDateChange(p, $event)">
<view class="picker">{{ paramValues[p.fieldKey] || ('选择' + p.fieldLabel) }}</view>
</picker>
<input v-else v-model.trim="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
</view>
</view>
</block>
<button size="mini" @click="reload">查询</button>
</view>
<scroll-view scroll-y class="list" @scrolltolower="loadMore">
<block v-if="items.length">
<view class="item" v-for="it in items" :key="it.id" @click="openDetail(it.id)">
<image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" />
<view class="content">
<view class="name">
<text>{{ it.name }}</text>
<text v-if="it.deleted" class="tag-deleted">已删除</text>
<text v-if="it.platformStatus==='platform'" class="tag-platform">平台推荐</text>
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
</view>
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
<view class="card-params" v-if="it.cardParams && Object.keys(it.cardParams).length">
<view class="param" v-for="(v,k) in it.cardParams" :key="k">{{ k }}{{ v }}</view>
</view>
</view>
</view>
</block>
<view v-else class="empty">
<text>暂无数据点击右上角新增</text>
</view>
</scroll-view>
<!-- 右下角的"+"按钮 -->
<view class="fab" @click="goSubmit"></view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
items: [],
query: { kw: '', page: 1, size: 20, categoryId: '', mode: 'direct', templateId: '', params: {} },
finished: false,
loading: false,
tab: 'all',
categories: [],
templates: [],
paramValues: {}
}
},
onLoad() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) {
this.items = []
this.categories = []
uni.showToast({ title: '请登录使用该功能', icon: 'none' })
return
}
this.fetchCategories()
this.reload()
},
onShow() {
const hasToken = (() => { try { return !!uni.getStorageSync('TOKEN') } catch(e){ return false } })()
if (!hasToken) return
// 检查是否需要打开查询Tab并设置查询模式从首页"配件查询"按钮进入)
try {
const configStr = uni.getStorageSync('PRODUCT_SEARCH_CONFIG')
if (configStr) {
const config = JSON.parse(configStr)
// 切换到指定Tab
if (config.openTab) {
this.tab = config.openTab
}
// 设置查询模式
if (config.mode) {
this.query.mode = config.mode
}
// 清除标志,避免下次进入时再次切换
uni.removeStorageSync('PRODUCT_SEARCH_CONFIG')
}
} catch(e) {
console.error('[list] 处理查询配置失败:', e)
}
// 从创建/编辑页返回时,确保刷新最新列表
this.reload()
},
computed: {
categoryNames() { return this.categories.map(c => c.name) },
categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.query.categoryId))
return c ? '类别:' + c.name : '选择类别'
},
modeLabel() {
const map = { direct: '直接查询', nameLike: '名称模糊查询', template: '按模板参数查询' }
return map[this.query.mode] || '直接查询'
},
templateNames() { return this.templates.map(t => t.name) },
templateLabel() {
const t = this.templates.find(x => String(x.id) === String(this.query.templateId))
return t ? '模板:' + t.name : '选择模板'
},
selectedTemplate() { return this.templates.find(t => String(t.id) === String(this.query.templateId)) || null },
selectedTemplateParams() { return (this.selectedTemplate && Array.isArray(this.selectedTemplate.params)) ? this.selectedTemplate.params : [] }
},
methods: {
switchTab(t) {
this.tab = t
this.query.categoryId = ''
this.query.templateId = ''
this.paramValues = {}
this.reload()
},
onPickCategory(e) {
const idx = Number(e.detail.value)
const c = this.categories[idx]
this.query.categoryId = c ? c.id : ''
this.fetchTemplates()
},
onPickTemplate(e) {
const idx = Number(e.detail.value)
const t = this.templates[idx]
this.query.templateId = t ? t.id : ''
this.paramValues = {}
},
onPickParamEnumWrapper(p, e) {
const idx = Number(e.detail.value)
const arr = p.enumOptions || []
this.paramValues[p.fieldKey] = arr[idx]
},
onParamBoolChange(p, e) { this.paramValues[p.fieldKey] = e?.detail?.value ? true : false },
onParamDateChange(p, e) { this.paramValues[p.fieldKey] = e?.detail?.value || '' },
async fetchCategories() {
try {
const res = await get('/api/product-categories', {})
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
async fetchTemplates() {
try {
const res = await get('/api/product-templates', this.query.categoryId ? { categoryId: this.query.categoryId } : {})
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.templates = list
} catch (_) { this.templates = [] }
},
reload() {
this.items = []
this.query.page = 1
this.finished = false
this.loadMore()
},
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size }
if (this.tab === 'search') {
if (this.query.categoryId) params.categoryId = this.query.categoryId
if (this.query.templateId) params.templateId = this.query.templateId
if (this.paramValues && Object.keys(this.paramValues).length) {
for (const k of Object.keys(this.paramValues)) {
const v = this.paramValues[k]
if (v !== undefined && v !== null && v !== '') params['param_' + k] = v
}
}
}
const res = await get('/api/products', params)
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.items = this.items.concat(list)
if (list.length < this.query.size) this.finished = true
this.query.page += 1
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
openDetail(id) {
uni.navigateTo({ url: '/pages/product/product-detail?id=' + id })
},
goMySubmissions() {
uni.navigateTo({ url: '/pages/product/submissions' })
},
async remove(it) {
try {
const r = await new Promise(resolve => {
uni.showModal({ content: '确认删除该货品?删除后可在后台恢复', success: resolve })
})
if (!r || !r.confirm) return
const { del } = require('../../common/http.js')
await del('/api/products/' + it.id)
uni.showToast({ title: '已删除', icon: 'success' })
this.reload()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
},
goSubmit() {
uni.navigateTo({ url: '/pages/product/submit' })
}
}
}
</script>
<style lang="scss">
.page { display:flex; flex-direction: column; height: 100vh; }
.tabs { display:flex; background:$uni-bg-color-grey; }
.tab { flex:1; text-align:center; padding: 20rpx 0; color:$uni-text-color-grey; }
.tab.active { color:$uni-color-primary; font-weight: 600; }
.tab.extra { flex: 0 0 180rpx; color:$uni-color-primary; font-weight: 600; }
.search { display:flex; gap: 12rpx; padding: 16rpx; background:$uni-bg-color-grey; align-items: center; }
.search input { flex:1; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 12rpx; color: $uni-text-color; }
.picker { padding: 8rpx 12rpx; background:$uni-bg-color-hover; border-radius: 10rpx; color:$uni-text-color-grey; }
.template-mode { flex-direction: column; align-items: stretch; gap: 8rpx; }
.picker-row { display:flex; gap: 12rpx; }
.params-wrap { margin-top: 6rpx; background:$uni-bg-color-grey; border-radius: 12rpx; padding: 8rpx 8rpx; }
.list { flex:1; }
.item { display:flex; padding: 20rpx; background:$uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:$uni-bg-color-hover; }
.content { flex:1; }
.name { color:$uni-text-color; margin-bottom: 6rpx; font-weight: 600; display:flex; align-items:center; gap: 12rpx; }
.tag-platform { font-size: 22rpx; color:#fff; background:#2d8cf0; padding: 4rpx 10rpx; border-radius: 8rpx; }
.tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; }
.tag-deleted { font-size: 22rpx; color:#fff; background:#909399; padding: 4rpx 10rpx; border-radius: 8rpx; }
.meta { color:$uni-text-color-grey; font-size: 24rpx; }
.card-params { display:flex; flex-wrap:wrap; gap: 8rpx 16rpx; margin-top: 8rpx; }
.card-params .param { color:$uni-text-color-grey; font-size: 22rpx; background:$uni-bg-color-grey; padding: 2rpx 6rpx; border-radius: 8rpx; }
.price { margin-left: 20rpx; color:$uni-color-primary; }
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; }
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background: linear-gradient(135deg, #4c8dff, #6ab7ff); color: #fff; border-radius: 50rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; box-shadow: 0 20rpx 40rpx rgba(0,0,0,0.2); }
</style>