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