| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- <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>
|