feat(product): 实现商品管理与详情展示功能
- 新增商品相关API接口封装,包括创建、查询、上传图片等功能 - 实现商品详情页面,支持多货币SKU选择与图片预览 - 实现商品管理页面,展示商品列表与链接复制功能 - 添加商品状态标签与销售地区展示 - 实现购买确认弹窗与订单跳转逻辑 - 添加响应式布局适配移动端展示 - 集成Element Plus组件库实现UI交互效果
This commit is contained in:
67
src/api/product.js
Normal file
67
src/api/product.js
Normal 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
1058
src/views/ProductDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
360
src/views/ProductManage.vue
Normal file
360
src/views/ProductManage.vue
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user