feat(product): 实现商品管理与详情展示功能

- 新增商品相关API接口封装,包括创建、查询、上传图片等功能
- 实现商品详情页面,支持多货币SKU选择与图片预览
- 实现商品管理页面,展示商品列表与链接复制功能
- 添加商品状态标签与销售地区展示
- 实现购买确认弹窗与订单跳转逻辑
- 添加响应式布局适配移动端展示
- 集成Element Plus组件库实现UI交互效果
This commit is contained in:
2025-12-22 18:14:11 +08:00
parent 5d0bdef650
commit 2f3606e967
3 changed files with 1485 additions and 0 deletions

67
src/api/product.js Normal file
View File

@@ -0,0 +1,67 @@
import request from './request'
/**
* 创建商品
*/
export function createProduct(data) {
return request({
url: '/product',
method: 'post',
data
})
}
/**
* 获取商品详情通过商品ID或链接码
*/
export function getProduct(id) {
return request({
url: `/product/${id}`,
method: 'get'
})
}
/**
* 根据链接码获取商品详情
*/
export function getProductByLinkCode(linkCode) {
return request({
url: `/product/link/${linkCode}`,
method: 'get'
})
}
/**
* 获取商品列表
*/
export function getProductList() {
return request({
url: '/product/list',
method: 'get'
})
}
/**
* 获取商品URL
*/
export function getProductUrl(id) {
return request({
url: `/product/${id}/url`,
method: 'get'
})
}
/**
* 上传商品图片
*/
export function uploadProductImage(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/product/upload/image',
method: 'post',
data: formData
// 注意:不设置 Content-Type让浏览器自动设置包含 boundary
})
}

1058
src/views/ProductDetail.vue Normal file

File diff suppressed because it is too large Load Diff

360
src/views/ProductManage.vue Normal file
View File

@@ -0,0 +1,360 @@
<template>
<div class="product-manage">
<el-card>
<template #header>
<div class="card-header">
<span>商品管理</span>
<el-button type="primary" @click="goToCreate">
<el-icon><Plus /></el-icon>
新增商品
</el-button>
</div>
</template>
<!-- 商品列表 -->
<el-table :data="productList" v-loading="loading" style="width: 100%">
<!-- 商品封面图 -->
<el-table-column label="商品封面" width="120" align="center">
<template #default="{ row }">
<el-image
v-if="getFirstMainImage(row)"
:src="getFirstMainImage(row)"
style="width: 80px; height: 80px; border-radius: 4px"
fit="cover"
:preview-src-list="row.mainImages || [row.mainImage]"
:initial-index="0"
/>
<span v-else class="no-image">无图片</span>
</template>
</el-table-column>
<!-- 商品名称名称下方附带商品Id -->
<el-table-column label="商品名称" min-width="250">
<template #default="{ row }">
<div class="product-name-cell">
<div class="product-name">{{ row.name }}</div>
<div class="product-id">商品ID: {{ row.id }}</div>
</div>
</template>
</el-table-column>
<!-- 商品链接 -->
<el-table-column label="商品链接" min-width="300">
<template #default="{ row }">
<div class="product-url-cell">
<el-input
v-model="row.productUrl"
readonly
size="small"
style="width: 100%"
>
<template #append>
<el-button
type="primary"
size="small"
@click="copyProductUrl(row.id, row.productUrl)"
>
复制
</el-button>
</template>
</el-input>
</div>
</template>
</el-table-column>
<!-- 商品价格 -->
<el-table-column label="商品价格" width="150" align="center">
<template #default="{ row }">
<div class="price-cell">
<span class="price-value">{{ formatPrice(row.price) }}</span>
<span class="price-currency">CNY</span>
</div>
</template>
</el-table-column>
<!-- 商品状态 -->
<el-table-column label="商品状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'" size="small">
{{ row.status === 'ACTIVE' ? '上架' : '下架' }}
</el-tag>
</template>
</el-table-column>
<!-- 发售地区 -->
<el-table-column label="发售地区" width="200">
<template #default="{ row }">
<div class="sales-regions-cell">
<el-tag
v-for="region in getSalesRegions(row)"
:key="region.code"
size="small"
style="margin-right: 5px; margin-bottom: 5px"
>
{{ region.name }}
</el-tag>
<span v-if="!getSalesRegions(row) || getSalesRegions(row).length === 0" class="no-region">
未设置
</span>
</div>
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="editProduct(row.id)">
修改
</el-button>
<el-button type="success" link size="small" @click="copyProductUrl(row.id, row.productUrl)">
复制
</el-button>
<el-button type="danger" link size="small" @click="deleteProduct(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getProductList, getProductUrl } from '../api/product'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const loading = ref(false)
const productList = ref([])
// 跳转到新增商品页面
const goToCreate = () => {
router.push('/manage/product/create')
}
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
}
// 货币到销售地区的映射
const currencyToRegionMap = {
'MYR': { code: 'MY', name: '马来西亚' },
'PHP': { code: 'PH', name: '菲律宾' },
'THB': { code: 'TH', name: '泰国' },
'VND': { code: 'VN', name: '越南' },
'SGD': { code: 'SG', name: '新加坡' },
'CNY': { code: 'CN', name: '中国' },
'USD': { code: 'US', name: '美国' },
'EUR': { code: 'EU', name: '欧洲' },
'GBP': { code: 'GB', name: '英国' }
}
// 获取第一张主图
const getFirstMainImage = (row) => {
if (row.mainImages && row.mainImages.length > 0) {
return row.mainImages[0]
}
return row.mainImage || null
}
// 获取销售地区从SKU的currency中提取
const getSalesRegions = (row) => {
if (!row.skus || row.skus.length === 0) {
return []
}
// 从SKU中提取所有不同的货币
const currencies = [...new Set(row.skus.map(sku => sku.currency).filter(Boolean))]
// 映射到销售地区
const regions = currencies
.map(currency => currencyToRegionMap[currency])
.filter(Boolean)
return regions
}
// 加载商品列表
const loadProductList = async () => {
loading.value = true
try {
const response = await getProductList()
if (response.code === '0000' && response.data) {
// 为每个商品获取链接URL
const productsWithUrl = await Promise.all(
response.data.map(async (product) => {
try {
const urlResponse = await getProductUrl(product.id)
if (urlResponse.code === '0000' && urlResponse.data && urlResponse.data.url) {
product.productUrl = urlResponse.data.url
} else {
product.productUrl = ''
}
} catch (error) {
console.error(`获取商品${product.id}链接失败:`, error)
product.productUrl = ''
}
return product
})
)
productList.value = productsWithUrl
} else {
ElMessage.error(response.message || '获取商品列表失败')
productList.value = []
}
} catch (error) {
console.error('加载商品列表失败:', error)
ElMessage.error('加载商品列表失败')
productList.value = []
} finally {
loading.value = false
}
}
// 编辑商品
const editProduct = (id) => {
// TODO: 实现编辑功能,跳转到编辑页面
ElMessage.info('编辑功能待实现')
// router.push(`/manage/product/edit/${id}`)
}
// 复制商品链接
const copyProductUrl = async (id, url) => {
try {
let urlToCopy = url
// 如果没有URL先获取
if (!urlToCopy) {
const response = await getProductUrl(id)
if (response.code === '0000' && response.data && response.data.url) {
urlToCopy = response.data.url
// 更新列表中的URL
const product = productList.value.find(p => p.id === id)
if (product) {
product.productUrl = urlToCopy
}
} else {
ElMessage.error('获取商品链接失败')
return
}
}
// 复制到剪贴板
await navigator.clipboard.writeText(urlToCopy)
ElMessage.success('商品链接已复制到剪贴板')
} catch (error) {
console.error('复制商品链接失败:', error)
ElMessage.error('复制商品链接失败')
}
}
// 删除商品
const deleteProduct = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该商品吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// TODO: 实现删除商品API
ElMessage.info('删除功能待实现')
// await request.delete(`/api/product/${id}`)
// ElMessage.success('商品删除成功')
// loadProductList()
} catch (error) {
if (error !== 'cancel') {
console.error('删除商品失败:', error)
ElMessage.error('删除商品失败')
}
}
}
onMounted(() => {
console.log('ProductManage 组件已挂载')
loadProductList()
})
</script>
<style scoped>
.product-manage {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
}
/* 商品名称单元格 */
.product-name-cell {
display: flex;
flex-direction: column;
gap: 5px;
}
.product-name {
font-size: 14px;
font-weight: 500;
color: #303133;
line-height: 1.5;
}
.product-id {
font-size: 12px;
color: #909399;
}
/* 商品链接单元格 */
.product-url-cell {
width: 100%;
}
/* 价格单元格 */
.price-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.price-value {
font-size: 16px;
font-weight: 600;
color: #f56c6c;
}
.price-currency {
font-size: 12px;
color: #909399;
}
/* 销售地区单元格 */
.sales-regions-cell {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.no-region {
color: #c0c4cc;
font-size: 12px;
}
/* 无图片 */
.no-image {
color: #c0c4cc;
font-size: 12px;
}
</style>