|
@@ -0,0 +1,261 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="product-picker">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ :value="selectedValue"
|
|
|
|
|
+ :placeholder="placeholder"
|
|
|
|
|
+ :clearable="clearable"
|
|
|
|
|
+ :disabled="disabled"
|
|
|
|
|
+ :filterable="true"
|
|
|
|
|
+ :remote="true"
|
|
|
|
|
+ :remote-method="handleSearch"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ :style="{ width: selectWidth }"
|
|
|
|
|
+ value-key="id"
|
|
|
|
|
+ class="product-picker-select"
|
|
|
|
|
+ @change="handleChange"
|
|
|
|
|
+ @clear="handleClear"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="item in options"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ :label="getOptionLabel(item)"
|
|
|
|
|
+ :value="item.id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="product-picker-option">
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="item.coverImage"
|
|
|
|
|
+ :src="item.coverImage"
|
|
|
|
|
+ class="product-picker-cover"
|
|
|
|
|
+ @error="handleImgError($event)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div v-else class="product-picker-cover product-picker-cover--placeholder">
|
|
|
|
|
+ <i class="el-icon-goods"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="product-picker-info">
|
|
|
|
|
+ <div class="product-picker-name">{{ item.name }}</div>
|
|
|
|
|
+ <div class="product-picker-meta">
|
|
|
|
|
+ <span class="product-picker-price">{{ formatPrice(item.price) }}</span>
|
|
|
|
|
+ <span v-if="item.productType" class="product-picker-type">{{ typeLabel(item.productType) }}</span>
|
|
|
|
|
+ <span class="product-picker-id">ID: {{ item.id }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-option>
|
|
|
|
|
+ <div v-if="loading" class="product-picker-empty">加载中...</div>
|
|
|
|
|
+ <div v-else-if="searched && options.length === 0" class="product-picker-empty">未找到匹配的商品</div>
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getProductList, getProduct } from '@/api/admin'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'ProductPicker',
|
|
|
|
|
+ props: {
|
|
|
|
|
+ // v-model 绑定的商品ID
|
|
|
|
|
+ value: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null
|
|
|
|
|
+ },
|
|
|
|
|
+ placeholder: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: '请输入商品名称搜索'
|
|
|
|
|
+ },
|
|
|
|
|
+ clearable: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true
|
|
|
|
|
+ },
|
|
|
|
|
+ disabled: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: false
|
|
|
|
|
+ },
|
|
|
|
|
+ selectWidth: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: '320px'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ options: [],
|
|
|
|
|
+ selectedValue: null,
|
|
|
|
|
+ selectedItem: null,
|
|
|
|
|
+ searched: false,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 20,
|
|
|
|
|
+ hasMore: false,
|
|
|
|
|
+ keyword: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ value: {
|
|
|
|
|
+ immediate: true,
|
|
|
|
|
+ handler(val) {
|
|
|
|
|
+ // 处理数字/字符串类型
|
|
|
|
|
+ if (val !== null && val !== undefined && val !== '') {
|
|
|
|
|
+ const num = Number(val)
|
|
|
|
|
+ if (!isNaN(num)) {
|
|
|
|
|
+ this.selectedValue = num
|
|
|
|
|
+ // 如果已有选中项且ID匹配,无需重新拉取
|
|
|
|
|
+ if (this.selectedItem && this.selectedItem.id === num) return
|
|
|
|
|
+ this.fetchProductById(num)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.selectedValue = null
|
|
|
|
|
+ this.selectedItem = null
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.selectedValue = null
|
|
|
|
|
+ this.selectedItem = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ typeLabel(val) {
|
|
|
|
|
+ const map = { physical: '实物商品', assessment: '测评服务' }
|
|
|
|
|
+ return map[val] || val || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ formatPrice(price) {
|
|
|
|
|
+ if (price === null || price === undefined) return ''
|
|
|
|
|
+ return (price / 100).toFixed(2) + '元'
|
|
|
|
|
+ },
|
|
|
|
|
+ getOptionLabel(item) {
|
|
|
|
|
+ // 选中后显示在输入框中的文本
|
|
|
|
|
+ return `${item.name} (¥${(item.price / 100).toFixed(2)})`
|
|
|
|
|
+ },
|
|
|
|
|
+ async fetchProductById(id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getProduct(id)
|
|
|
|
|
+ if (res && res.data && res.data.id) {
|
|
|
|
|
+ this.selectedItem = res.data
|
|
|
|
|
+ // 把当前选中项放入选项列表(确保显示)
|
|
|
|
|
+ const exists = this.options.some(o => o.id === res.data.id)
|
|
|
|
|
+ if (!exists) {
|
|
|
|
|
+ this.options = [res.data, ...this.options]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('[ProductPicker] 加载商品详情失败', e)
|
|
|
|
|
+ // 编辑回显失败不影响使用,允许用户重新搜索
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleSearch(query) {
|
|
|
|
|
+ this.keyword = (query || '').trim()
|
|
|
|
|
+ this.page = 1
|
|
|
|
|
+ this.searched = true
|
|
|
|
|
+ await this.loadOptions(true)
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadOptions(reset) {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getProductList({
|
|
|
|
|
+ keyword: this.keyword || undefined,
|
|
|
|
|
+ page: this.page,
|
|
|
|
|
+ size: this.pageSize
|
|
|
|
|
+ })
|
|
|
|
|
+ const data = (res && res.data) || {}
|
|
|
|
|
+ const records = data.records || []
|
|
|
|
|
+ if (reset) {
|
|
|
|
|
+ this.options = records
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.options = this.options.concat(records)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果有当前选中项但不在列表中,补回
|
|
|
|
|
+ if (this.selectedItem && !this.options.some(o => o.id === this.selectedItem.id)) {
|
|
|
|
|
+ this.options = [this.selectedItem, ...this.options]
|
|
|
|
|
+ }
|
|
|
|
|
+ const total = data.total || records.length
|
|
|
|
|
+ this.hasMore = this.options.length < total
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('[ProductPicker] 搜索商品失败', e)
|
|
|
|
|
+ if (reset) this.options = []
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ handleChange(val) {
|
|
|
|
|
+ this.selectedValue = val
|
|
|
|
|
+ if (val !== null && val !== undefined && val !== '') {
|
|
|
|
|
+ const found = this.options.find(o => o.id === Number(val))
|
|
|
|
|
+ this.selectedItem = found || null
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.selectedItem = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$emit('input', val === null || val === undefined ? null : Number(val))
|
|
|
|
|
+ this.$emit('change', this.selectedItem)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleClear() {
|
|
|
|
|
+ this.selectedItem = null
|
|
|
|
|
+ this.$emit('input', null)
|
|
|
|
|
+ this.$emit('change', null)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleImgError(e) {
|
|
|
|
|
+ // 封面图加载失败时用占位符替换
|
|
|
|
|
+ e.target.style.display = 'none'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.product-picker-option {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 4px 0;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-cover {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-cover--placeholder {
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-name {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-meta {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-price {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-type {
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ padding: 1px 6px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-id {
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+}
|
|
|
|
|
+.product-picker-empty {
|
|
|
|
|
+ padding: 10px 0;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|