Files
cuimengxue/frontend/src/views/admin/AdminProducts.vue
wangziqi d6451cf188 feat: 重构后台路由并使用嵌套路由;添加图片上传功能
- 拆分 AdminView 为多个子页面组件,使用嵌套路由
- 拆分 MerchantView 为多个子页面组件,使用嵌套路由
- 创建 AdminLayout 和 MerchantLayout 布局组件
- 修复编译错误:IconSend 重复导入、IconDatabase 不存在
- 修复 HomeView 缺失 onMounted 导入
- 添加文件上传后端接口和静态资源映射
- 为商品和轮播图添加图片上传功能(支持预览、清除)
2026-02-10 15:14:23 +08:00

292 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-card :bordered="false" class="content-card">
<template #title>
<a-space>
<span>商品管理</span>
<a-tag color="green"> {{ products.length }} 件商品</a-tag>
</a-space>
</template>
<template #extra>
<a-space>
<a-button type="primary" @click="openProductModal()">
<template #icon><icon-plus /></template>
新增商品
</a-button>
<a-button @click="loadProducts">
<template #icon><icon-refresh /></template>
刷新
</a-button>
</a-space>
</template>
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }" :bordered="false">
<template #approved="{ record }">
<a-switch :model-value="record.approved" @change="() => toggleApprove(record)" />
</template>
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" shape="round" @click="openProductModal(record)">编辑</a-button>
<a-popconfirm content="确定删除此商品吗?" @ok="deleteProduct(record.id)">
<a-button size="mini" status="danger" shape="round">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-card>
<a-modal v-model:visible="productModalVisible" :title="productForm.id ? '编辑商品' : '新增商品'" :ok-text="productForm.id ? '保存' : '添加'" cancel-text="取消" @ok="submitProductModal" width="600">
<a-form :model="productForm" layout="vertical">
<a-row :gutter="16">
<a-col :span="16">
<a-form-item label="商品名称" required>
<a-input v-model="productForm.name" placeholder="请输入商品名称" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="分类">
<a-select v-model="productForm.category" placeholder="选择分类">
<a-option value="奶粉">奶粉</a-option>
<a-option value="尿裤">尿裤</a-option>
<a-option value="辅食">辅食</a-option>
<a-option value="玩具">玩具</a-option>
<a-option value="服饰">服饰</a-option>
<a-option value="用品">用品</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="商品描述">
<a-textarea v-model="productForm.description" placeholder="请输入商品描述" :rows="3" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="价格" required>
<a-input-number v-model="productForm.price" :min="0" :precision="2" :step="0.01" style="width: 100%" prefix="¥" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="库存" required>
<a-input-number v-model="productForm.stock" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="商品图片">
<a-upload
:action="uploadAction"
:show-file-list="false"
:headers="uploadHeaders"
accept="image/*"
@success="onUploadSuccess"
@error="onUploadError"
@before-upload="beforeUpload"
>
<template #upload-button>
<div class="upload-trigger" :class="{ 'has-image': productForm.imageUrl }">
<img v-if="productForm.imageUrl" :src="productForm.imageUrl" class="preview-image" />
<div v-else class="upload-placeholder">
<icon-plus :size="24" />
<span>点击上传图片</span>
</div>
</div>
</template>
</a-upload>
<div v-if="productForm.imageUrl" class="upload-actions">
<a-button type="text" size="small" @click="clearImage">
<template #icon><icon-delete /></template>
清除图片
</a-button>
</div>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所属商家">
<a-select v-model="productForm.merchantId" placeholder="选择商家" allow-search>
<a-option v-for="m in merchantOptions" :key="m.value" :value="m.value">{{ m.label }}</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="审核状态">
<a-switch v-model="productForm.approved" />
<span style="margin-left: 8px">{{ productForm.approved ? '已上架' : '待审核' }}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import { api } from '../../api'
import { IconPlus, IconRefresh, IconDelete } from '@arco-design/web-vue/es/icon'
const products = ref([])
const merchantOptions = ref([])
const productModalVisible = ref(false)
const productForm = reactive({ id: null, name: '', category: '', description: '', price: 0, stock: 0, imageUrl: '', merchantId: null, approved: false })
const uploadAction = 'http://localhost:8080/api/upload/image'
const uploadHeaders = computed(() => ({
'X-Token': localStorage.getItem('token') || ''
}))
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
Message.error('只能上传图片文件!')
return false
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
Message.error('图片大小不能超过 5MB')
return false
}
return true
}
const onUploadSuccess = (response) => {
if (response && response.data) {
productForm.imageUrl = response.data
Message.success('图片上传成功')
} else if (typeof response === 'string') {
productForm.imageUrl = response
Message.success('图片上传成功')
}
}
const onUploadError = (error) => {
Message.error('图片上传失败:' + (error.message || '未知错误'))
}
const clearImage = () => {
productForm.imageUrl = ''
}
const productColumns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '商品名', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category', width: 80 },
{ title: '价格', dataIndex: 'price', render: ({ record }) => `¥${record.price}`, width: 80 },
{ title: '库存', dataIndex: 'stock', width: 70 },
{ title: '商家', dataIndex: 'merchantUsername', width: 100 },
{ title: '审核', slotName: 'approved', width: 80 },
{ title: '操作', slotName: 'actions', width: 140 }
]
const loadProducts = async () => {
products.value = await api.adminProductViews()
}
const loadMerchantOptions = async () => {
const list = await api.adminUsers()
merchantOptions.value = list.filter((u) => u.role === 'MERCHANT').map((u) => ({ label: `${u.nickname || u.username} (${u.username})`, value: u.id }))
}
const openProductModal = (row = null) => {
if (row) {
productForm.id = row.id
productForm.name = row.name || ''
productForm.category = row.category || ''
productForm.description = row.description || ''
productForm.price = row.price || 0
productForm.stock = row.stock || 0
productForm.imageUrl = row.imageUrl || ''
productForm.merchantId = row.merchantId || null
productForm.approved = !!row.approved
} else {
productForm.id = null
productForm.name = ''
productForm.category = ''
productForm.description = ''
productForm.price = 0
productForm.stock = 0
productForm.imageUrl = ''
productForm.merchantId = null
productForm.approved = false
}
if (merchantOptions.value.length === 0) loadMerchantOptions()
productModalVisible.value = true
}
const submitProductModal = async () => {
if (!productForm.merchantId) return Message.warning('请选择所属商家')
await api.adminSaveProduct({ ...productForm })
Message.success(productForm.id ? '商品已更新' : '商品已新增')
productModalVisible.value = false
await loadProducts()
}
const toggleApprove = async (row) => {
await api.adminApproveProduct(row.id, { approved: !row.approved })
Message.success(!row.approved ? '已上架' : '已下架')
await loadProducts()
}
const deleteProduct = async (id) => {
await api.adminDeleteProduct(id)
Message.success('商品已删除')
await loadProducts()
}
onMounted(() => {
loadProducts()
loadMerchantOptions()
})
</script>
<style scoped>
.content-card {
border-radius: 12px;
}
:deep(.arco-btn-primary) {
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
border-color: transparent;
}
:deep(.arco-card-header) {
border-bottom: 1px solid #f0f0f0;
}
.upload-trigger {
width: 100%;
height: 150px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
overflow: hidden;
}
.upload-trigger:hover {
border-color: #722ed1;
}
.upload-trigger.has-image {
border: none;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
color: #999;
gap: 8px;
}
.upload-actions {
margin-top: 8px;
text-align: center;
}
</style>