|
@@ -0,0 +1,242 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="product-manage">
|
|
|
|
|
+ <div class="header">
|
|
|
|
|
+ <h2>商品管理</h2>
|
|
|
|
|
+ <div class="filters">
|
|
|
|
|
+ <el-select v-model="statusFilter" placeholder="商品状态" @change="loadList" style="width: 140px;">
|
|
|
|
|
+ <el-option label="全部" value="" />
|
|
|
|
|
+ <el-option label="待审核" value="pending" />
|
|
|
|
|
+ <el-option label="已上架" value="approved" />
|
|
|
|
|
+ <el-option label="已拒绝" value="rejected" />
|
|
|
|
|
+ <el-option label="已下架" value="shelved" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-select v-model="typeFilter" placeholder="商品类型" @change="loadList" style="width: 140px;">
|
|
|
|
|
+ <el-option label="全部类型" value="" />
|
|
|
|
|
+ <el-option label="实物商品" value="physical" />
|
|
|
|
|
+ <el-option label="虚拟商品" value="virtual" />
|
|
|
|
|
+ <el-option label="优惠券" value="coupon" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table :data="list" v-loading="loading" border stripe>
|
|
|
|
|
+ <el-table-column prop="id" label="ID" width="70" />
|
|
|
|
|
+ <el-table-column label="商品图片" width="80">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <img v-if="row.coverImage" :src="row.coverImage" class="product-thumb" @click="previewImage(row.coverImage)" />
|
|
|
|
|
+ <span v-else>-</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="name" label="商品名称" min-width="150" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="类型" width="90">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ typeLabel(row.productType) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="price" label="售价(元)" width="90">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ ¥{{ (row.price / 100).toFixed(2) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="stock" label="库存" width="70" />
|
|
|
|
|
+ <el-table-column label="供应商" width="120" show-overflow-tooltip>
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.vendorName || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="状态" width="90">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="createTime" label="创建时间" width="160">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ formatTime(row.createTime) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="220" fixed="right">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-button v-if="row.status === 'pending'" type="success" size="small" @click="handleReview(row, 'approve')">通过</el-button>
|
|
|
|
|
+ <el-button v-if="row.status === 'pending'" type="danger" size="small" @click="showRejectDialog(row)">拒绝</el-button>
|
|
|
|
|
+ <el-button v-if="row.status === 'approved'" type="warning" size="small" @click="handleShelve(row, false)">下架</el-button>
|
|
|
|
|
+ <el-button v-if="row.status === 'shelved'" type="primary" size="small" @click="handleShelve(row, true)">上架</el-button>
|
|
|
|
|
+ <el-button type="info" size="small" @click="viewDetail(row)">详情</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ @size-change="onPageChange"
|
|
|
|
|
+ @current-change="onPageChange"
|
|
|
|
|
+ :current-page="page"
|
|
|
|
|
+ :page-size="size"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ layout="total, prev, pager, next"
|
|
|
|
|
+ style="margin-top: 20px; text-align: right;"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 拒绝原因弹窗 -->
|
|
|
|
|
+ <el-dialog title="拒绝原因" :visible.sync="rejectDialogVisible" width="400px">
|
|
|
|
|
+ <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入拒绝原因" />
|
|
|
|
|
+ <span slot="footer">
|
|
|
|
|
+ <el-button @click="rejectDialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="danger" @click="confirmReject">确认拒绝</el-button>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 商品详情弹窗 -->
|
|
|
|
|
+ <el-dialog title="商品详情" :visible.sync="detailDialogVisible" width="600px">
|
|
|
|
|
+ <el-descriptions :column="2" border v-if="detail">
|
|
|
|
|
+ <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="售价">¥{{ (detail.price / 100).toFixed(2) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="会员价">¥{{ detail.memberPrice ? (detail.memberPrice / 100).toFixed(2) : '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="销量">{{ detail.salesCount || 0 }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="供应商">{{ detail.vendorName || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="供应商手机">{{ detail.vendorPhone || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
|
|
+ <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="拒绝原因" v-if="detail.rejectReason">{{ detail.rejectReason }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="商品描述" :span="2">{{ detail.description || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="创建时间" :span="2">{{ formatTime(detail.createTime) }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getProductList, reviewProduct, shelveProduct } from '@/api/admin'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ list: [],
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ size: 20,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ statusFilter: '',
|
|
|
|
|
+ typeFilter: '',
|
|
|
|
|
+ rejectDialogVisible: false,
|
|
|
|
|
+ rejectTarget: null,
|
|
|
|
|
+ rejectReason: '',
|
|
|
|
|
+ detailDialogVisible: false,
|
|
|
|
|
+ detail: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.loadList()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async loadList() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getProductList({ status: this.statusFilter })
|
|
|
|
|
+ this.list = res.data || []
|
|
|
|
|
+ this.total = (res.data || []).length
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onPageChange(val) {
|
|
|
|
|
+ this.page = val
|
|
|
|
|
+ this.loadList()
|
|
|
|
|
+ },
|
|
|
|
|
+ statusType(status) {
|
|
|
|
|
+ const map = { pending: 'warning', approved: 'success', rejected: 'danger', shelved: 'info' }
|
|
|
|
|
+ return map[status] || 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+ statusLabel(status) {
|
|
|
|
|
+ const map = { pending: '待审核', approved: '已上架', rejected: '已拒绝', shelved: '已下架' }
|
|
|
|
|
+ return map[status] || status
|
|
|
|
|
+ },
|
|
|
|
|
+ typeLabel(type) {
|
|
|
|
|
+ const map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
|
|
|
|
|
+ return map[type] || type
|
|
|
|
|
+ },
|
|
|
|
|
+ formatTime(time) {
|
|
|
|
|
+ if (!time) return '-'
|
|
|
|
|
+ return new Date(time).toLocaleString()
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleReview(row, action) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.$confirm(`确认通过该商品审核?`, '提示')
|
|
|
|
|
+ await reviewProduct(row.id, { action })
|
|
|
|
|
+ this.$message.success('操作成功')
|
|
|
|
|
+ this.loadList()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ if (e !== 'cancel') this.$message.error('操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ showRejectDialog(row) {
|
|
|
|
|
+ this.rejectTarget = row
|
|
|
|
|
+ this.rejectReason = ''
|
|
|
|
|
+ this.rejectDialogVisible = true
|
|
|
|
|
+ },
|
|
|
|
|
+ async confirmReject() {
|
|
|
|
|
+ if (!this.rejectReason.trim()) {
|
|
|
|
|
+ return this.$message.warning('请填写拒绝原因')
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await reviewProduct(this.rejectTarget.id, { action: 'reject', reason: this.rejectReason })
|
|
|
|
|
+ this.$message.success('已拒绝')
|
|
|
|
|
+ this.rejectDialogVisible = false
|
|
|
|
|
+ this.loadList()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleShelve(row, shelve) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.$confirm(`确认${shelve ? '上架' : '下架'}该商品?`, '提示')
|
|
|
|
|
+ await shelveProduct(row.id, { shelve })
|
|
|
|
|
+ this.$message.success('操作成功')
|
|
|
|
|
+ this.loadList()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ if (e !== 'cancel') this.$message.error('操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ viewDetail(row) {
|
|
|
|
|
+ this.detail = row
|
|
|
|
|
+ this.detailDialogVisible = true
|
|
|
|
|
+ },
|
|
|
|
|
+ previewImage(url) {
|
|
|
|
|
+ if (!url) return
|
|
|
|
|
+ this.$alert(`<img src="${url}" style="max-width:100%;" />`, '商品图片', {
|
|
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
|
|
+ closeOnClickModal: true
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.product-manage {
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+.header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+.header h2 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.filters {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.product-thumb {
|
|
|
|
|
+ width: 50px;
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|