| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- <template>
- <div class="nutrition-products admin-page">
- <el-card shadow="never">
- <div slot="header" class="admin-page-header">
- <span class="admin-page-title">
- <i class="el-icon-first-aid-kit" style="color: #67c23a; margin-right: 8px;"></i>
- 营养产品管理
- </span>
- <div class="header-actions">
- <el-input
- v-model="keyword"
- placeholder="搜索产品名称..."
- prefix-icon="el-icon-search"
- clearable
- size="mini"
- style="width: 200px; margin-right: 8px;"
- @keyup.enter.native="handleSearch"
- @clear="handleSearch"
- />
- <el-select
- v-model="categoryFilter"
- placeholder="分类筛选"
- size="mini"
- clearable
- style="width: 140px; margin-right: 8px;"
- @change="handleSearch"
- >
- <el-option label="全部分类" value="" />
- <el-option v-for="opt in categoryOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
- </el-select>
- <el-select
- v-model="brandFilter"
- placeholder="品牌筛选"
- size="mini"
- clearable
- style="width: 140px; margin-right: 8px;"
- @change="handleSearch"
- >
- <el-option label="全部品牌" value="" />
- <el-option v-for="opt in brandOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
- </el-select>
- </div>
- </div>
- <div class="adm-table-wrap">
- <el-table
- :data="list"
- v-loading="loading"
- border
- stripe
- size="mini"
- style="width: 100%"
- height="100%"
- >
- <el-table-column type="index" label="#" width="50" align="center" />
- <el-table-column label="图片" width="100" align="center">
- <template slot-scope="{ row }">
- <el-image
- v-if="productImage(row)"
- :src="productImage(row)"
- :preview-src-list="productImageList(row)"
- fit="cover"
- style="width: 60px; height: 60px; border-radius: 4px;"
- lazy
- />
- <span v-else class="no-data">-</span>
- </template>
- </el-table-column>
- <el-table-column label="产品名称" min-width="200">
- <template slot-scope="{ row }">
- <span style="font-weight: 500;">{{ row.name }}</span>
- </template>
- <template slot="header">
- <span class="sort-header" @click.stop="onSortClick('name', $event)">产品名称{{ sortIcon('name') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="品牌" width="110" align="center">
- <template slot-scope="{ row }">
- <span>{{ brandName(row.brandId) }}</span>
- </template>
- <template slot="header">
- <span class="sort-header" @click.stop="onSortClick('brandId', $event)">品牌<span v-if="sortIndex('brandId') !== null" class="sort-index" :class="sortClass('brandId') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('brandId') }}</span>{{ sortIcon('brandId') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="分类" width="95" align="center">
- <template slot-scope="{ row }">
- <el-tag size="mini" effect="plain">{{ categoryName(row.categoryId) }}</el-tag>
- </template>
- <template slot="header">
- <span class="sort-header" @click.stop="onSortClick('categoryId', $event)">分类<span v-if="sortIndex('categoryId') !== null" class="sort-index" :class="sortClass('categoryId') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('categoryId') }}</span>{{ sortIcon('categoryId') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="价格" width="120" align="right">
- <template slot-scope="{ row }">
- <span class="price-val">¥{{ row.price != null ? Number(row.price).toFixed(2) : '-' }}</span>
- <span v-if="row.originalPrice" class="price-orig">¥{{ row.originalPrice }}</span>
- </template>
- <template slot="header">
- <span class="sort-header" @click.stop="onSortClick('price', $event)">价格<span v-if="sortIndex('price') !== null" class="sort-index" :class="sortClass('price') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('price') }}</span>{{ sortIcon('price') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="已售" width="70" align="center">
- <template slot-scope="{ row }">{{ row.soldNum != null ? row.soldNum : '-' }}</template>
- <template slot="header">
- <span class="sort-header" @click.stop="onSortClick('soldNum', $event)">已售{{ sortIcon('soldNum') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="来源" width="90" align="center">
- <template slot-scope="{ row }">
- <span v-if="row.sourcePlatform">{{ row.sourcePlatform }}</span>
- <span v-else class="no-data">-</span>
- </template>
- </el-table-column>
- <el-table-column label="规格" width="100" align="center">
- <template slot-scope="{ row }">{{ row.spec || '-' }}</template>
- </el-table-column>
- <el-table-column label="成份/配料" min-width="200" show-overflow-tooltip>
- <template slot-scope="{ row }">
- <span v-if="row.ingredients">{{ row.ingredients }}</span>
- <span v-else class="no-data">-</span>
- </template>
- </el-table-column>
- <el-table-column label="关联商品" width="90" align="center">
- <template slot-scope="{ row }">
- <el-tag v-if="row.localProductId" type="success" size="mini">已关联</el-tag>
- <span v-else class="no-data">-</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="110" align="center" fixed="right">
- <template slot-scope="{ row }">
- <el-button type="text" size="mini" @click="showDetail(row.id)">详情</el-button>
- <el-link v-if="row.sourceUrl" :href="row.sourceUrl" target="_blank" type="primary" :underline="false" style="margin-left: 6px;">原平台</el-link>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 分页 -->
- <div class="pagination-wrapper">
- <el-pagination
- background
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- :page-sizes="[10, 20, 50, 100]"
- :page-size="pageSize"
- :current-page.sync="page"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- />
- </div>
- </el-card>
- <!-- 详情对话框 -->
- <el-dialog
- title="营养产品详情"
- :visible.sync="detailVisible"
- width="800px"
- :close-on-click-modal="false"
- >
- <div v-loading="detailLoading" v-if="detailData" style="max-height: 60vh; overflow-y: auto;">
- <el-row :gutter="12" style="margin-bottom: 12px;">
- <el-col :span="8">
- <el-image
- v-if="detailImages.length"
- :src="detailImages[0]"
- :preview-src-list="detailImages"
- fit="cover"
- style="width: 100%; height: 180px; border-radius: 6px;"
- />
- <div v-else class="no-data" style="height: 180px; display: flex; align-items: center; justify-content: center;">暂无图片</div>
- </el-col>
- <el-col :span="16">
- <el-descriptions :column="2" border size="mini">
- <el-descriptions-item label="产品名称" :span="2">{{ detailData.product.name }}</el-descriptions-item>
- <el-descriptions-item label="品牌">{{ brandName(detailData.product.brandId) }}</el-descriptions-item>
- <el-descriptions-item label="分类">{{ categoryName(detailData.product.categoryId) }}</el-descriptions-item>
- <el-descriptions-item label="价格">¥{{ detailData.product.price != null ? Number(detailData.product.price).toFixed(2) : '-' }}</el-descriptions-item>
- <el-descriptions-item label="原价">¥{{ detailData.product.originalPrice || '-' }}</el-descriptions-item>
- <el-descriptions-item label="已售">{{ detailData.product.soldNum != null ? detailData.product.soldNum : '-' }}</el-descriptions-item>
- <el-descriptions-item label="来源平台">{{ detailData.product.sourcePlatform || '-' }}</el-descriptions-item>
- <el-descriptions-item label="规格">{{ detailData.product.spec || '-' }}</el-descriptions-item>
- <el-descriptions-item label="净含量">{{ detailData.product.netContent || '-' }}</el-descriptions-item>
- <el-descriptions-item label="剂型">{{ detailData.product.form || '-' }}</el-descriptions-item>
- <el-descriptions-item label="保质期">{{ detailData.product.shelfLife || '-' }}</el-descriptions-item>
- <el-descriptions-item label="适用人群">{{ detailData.product.suitableFor || '-' }}</el-descriptions-item>
- <el-descriptions-item label="成份/配料表">{{ detailData.product.ingredients || '-' }}</el-descriptions-item>
- <el-descriptions-item label="原平台链接" :span="2">
- <el-link v-if="detailData.product.sourceUrl" :href="detailData.product.sourceUrl" target="_blank" type="primary" :underline="false">
- {{ detailData.product.sourceUrl }}
- </el-link>
- <span v-else class="no-data">-</span>
- </el-descriptions-item>
- <el-descriptions-item label="关联商品ID" :span="2">
- <el-tag v-if="detailData.product.localProductId" type="success">
- {{ detailData.product.localProductId }}
- </el-tag>
- <span v-else class="no-data">未关联</span>
- </el-descriptions-item>
- <el-descriptions-item label="产品描述" :span="2">
- {{ detailData.product.description || '-' }}
- </el-descriptions-item>
- </el-descriptions>
- </el-col>
- </el-row>
- <el-divider content-position="left">成份明细</el-divider>
- <el-table v-if="detailData.ingredients && detailData.ingredients.length" :data="detailData.ingredients" size="mini" border stripe>
- <el-table-column prop="name" label="成分名" min-width="150" />
- <el-table-column prop="ingredientType" label="类型" width="100" />
- <el-table-column prop="strain" label="菌株号" width="120" />
- <el-table-column label="每份含量" width="120">
- <template slot-scope="{ row }">{{ row.amountPerServing }} {{ row.unit }}</template>
- </el-table-column>
- <el-table-column prop="purpose" label="功效" min-width="150" />
- <div v-else class="no-data">暂无成份数据</div>
- <el-divider content-position="left">平台价格</el-divider>
- <el-table v-if="detailData.platformListings && detailData.platformListings.length" :data="detailData.platformListings" size="mini" border stripe>
- <el-table-column prop="platform" label="平台" width="100" />
- <el-table-column label="当前价格" width="120">
- <template slot-scope="{ row }">¥{{ row.currentPrice }}</template>
- </el-table-column>
- <el-table-column label="原价" width="120">
- <template slot-scope="{ row }">¥{{ row.originalPrice || '-' }}</template>
- </el-table-column>
- <el-table-column prop="priceUnit" label="价格单位" width="100" />
- <el-table-column prop="salesCount" label="销量" width="80" />
- <el-table-column prop="platformUrl" label="链接" min-width="200">
- <template slot-scope="{ row }">
- <el-link v-if="row.platformUrl" :href="row.platformUrl" target="_blank" type="primary" :underline="false">
- {{ row.platformUrl | truncate(40) }}
- </el-link>
- <span v-else class="no-data">-</span>
- </template>
- </el-table-column>
- <div v-else class="no-data">暂无平台价格数据</div>
- <el-divider content-position="left">本平台关联商品</el-divider>
- <div v-if="detailData.localProduct">
- <el-tag type="success">已关联商品 ID: {{ detailData.localProduct.id }}</el-tag>
- <p style="margin-top: 8px;">名称: {{ detailData.localProduct.name }}</p>
- <p>价格: ¥{{ (detailData.localProduct.price / 100).toFixed(2) }}</p>
- </div>
- <div v-else class="no-data">未关联本平台商品</div>
-
- </el-table>
-
- </el-table>
- </div>
- <div v-else-if="detailLoading" style="text-align: center; padding: 40px;">
- <i class="el-icon-loading" style="font-size: 24px;"></i>
- </div>
- <div v-else class="no-data" style="text-align: center; padding: 40px;">未找到产品信息</div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { getCategoryList, getBrandList, searchProducts, getProductDetail } from '@/api/nutritionProduct'
- import SortMixin from '@/mixins/SortMixin'
- export default {
- name: 'NutritionProducts',
- mixins: [SortMixin],
- data() {
- return {
- keyword: '',
- categoryFilter: '',
- brandFilter: '',
- list: [],
- sortableColumns: { name: '产品名称', soldNum: '已售', price: '价格', brandId: '品牌', categoryId: '分类' },
- total: 0,
- page: 1,
- pageSize: 20,
- loading: false,
- categories: [],
- brands: [],
- detailVisible: false,
- detailLoading: false,
- detailData: null,
- tableHeight: window.innerHeight - 260
- }
- },
- computed: {
- categoryOptions() {
- return this.categories.map(c => ({ value: c.id, label: c.name }))
- },
- brandOptions() {
- return this.brands.map(b => ({ value: b.id, label: b.name }))
- },
- detailImages() {
- if (!this.detailData || !this.detailData.product) return []
- return this.parseImages(this.detailData.product)
- }
- },
- created() {
- this.loadCategories()
- this.loadBrands()
- this.loadList()
- },
- methods: {
- parseImages(row) {
- const images = []
- if (row.productImages) {
- try {
- const parsed = JSON.parse(row.productImages)
- if (Array.isArray(parsed)) {
- return parsed.filter(u => u)
- }
- if (typeof parsed === 'string') {
- return parsed.split(',').filter(u => u)
- }
- } catch (e) {
- // ignore
- }
- }
- if (row.imageUrl) {
- images.push(row.imageUrl)
- }
- return images
- },
- productImage(row) {
- const imgs = this.parseImages(row)
- return imgs.length ? imgs[0] : ''
- },
- productImageList(row) {
- return this.parseImages(row)
- },
- async loadCategories() {
- try {
- const res = await getCategoryList()
- this.categories = res.data || []
- } catch (e) {
- // ignore
- }
- },
- async loadBrands() {
- try {
- const res = await getBrandList()
- this.brands = res.data || []
- } catch (e) {
- // ignore
- }
- },
- async loadList() {
- this.loading = true
- try {
- const params = {
- page: this.page,
- pageSize: this.pageSize
- }
- if (this.keyword) params.name = this.keyword
- if (this.categoryFilter) params.categoryId = this.categoryFilter
- if (this.brandFilter) params.brandId = this.brandFilter
- if (this.sortState.length > 0) params.sort = this.sortState
- const res = await searchProducts(params)
- const data = res.data || {}
- this.list = data.records || []
- this.total = data.total || 0
- } catch (e) {
- this.list = []
- this.total = 0
- } finally {
- this.loading = false
- }
- },
- handleSearch() {
- this.page = 1
- this.loadList()
- },
- handleSizeChange(size) {
- this.pageSize = size
- this.page = 1
- this.loadList()
- },
- handlePageChange(page) {
- this.page = page
- this.loadList()
- },
- categoryName(id) {
- if (!id) return '-'
- const found = this.categories.find(c => c.id === id)
- return found ? found.name : '-'
- },
- brandName(id) {
- if (!id) return '-'
- const found = this.brands.find(b => b.id === id)
- return found ? found.name : '-'
- },
- async showDetail(productId) {
- this.detailVisible = true
- this.detailLoading = true
- this.detailData = null
- try {
- const res = await getProductDetail(productId)
- this.detailData = res.data
- } catch (e) {
- this.$message.error('获取详情失败')
- } finally {
- this.detailLoading = false
- }
- }
- }
- }
- </script>
- <style scoped>
- .nutrition-products {
- padding: 16px;
- }
- .admin-page-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .admin-page-title {
- font-size: 16px;
- font-weight: 600;
- }
- .header-actions {
- display: flex;
- align-items: center;
- }
- .pagination-wrapper {
- display: flex;
- justify-content: flex-end;
- margin-top: 14px;
- }
- .price-val {
- font-weight: 600;
- color: #e74c3c;
- }
- .price-orig {
- margin-left: 6px;
- color: #999;
- text-decoration: line-through;
- font-size: 12px;
- }
- .no-data {
- color: #999;
- }
- .sort-header {
- cursor: pointer;
- user-select: none;
- font-weight: 500;
- font-size: 12px;
- }
- .sort-header:hover {
- color: #409eff;
- }
- </style>
|