- 拆分 AdminView 为多个子页面组件,使用嵌套路由 - 拆分 MerchantView 为多个子页面组件,使用嵌套路由 - 创建 AdminLayout 和 MerchantLayout 布局组件 - 修复编译错误:IconSend 重复导入、IconDatabase 不存在 - 修复 HomeView 缺失 onMounted 导入 - 添加文件上传后端接口和静态资源映射 - 为商品和轮播图添加图片上传功能(支持预览、清除)
292 lines
9.1 KiB
Vue
292 lines
9.1 KiB
Vue
<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>
|