feat: 添加品牌管理页面
This commit is contained in:
@@ -119,6 +119,7 @@
|
||||
"userManagement": "User Management",
|
||||
"productManagement": "Product Management",
|
||||
"categoryManagement": "Category Management",
|
||||
"brandManagement": "Brand Management",
|
||||
"orderManagement": "Order Management",
|
||||
"supplierManagement": "Supplier Management",
|
||||
"lotteryManagement": "Lottery Management",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"userManagement": "ユーザー管理",
|
||||
"productManagement": "商品管理",
|
||||
"categoryManagement": "カテゴリ管理",
|
||||
"brandManagement": "ブランド管理",
|
||||
"orderManagement": "注文管理",
|
||||
"supplierManagement": "サプライヤー管理",
|
||||
"lotteryManagement": "抽選管理",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"userManagement": "用户管理",
|
||||
"productManagement": "商品管理",
|
||||
"categoryManagement": "分类管理",
|
||||
"brandManagement": "品牌管理",
|
||||
"orderManagement": "订单管理",
|
||||
"supplierManagement": "供货商管理",
|
||||
"lotteryManagement": "抽奖管理",
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span class="nav-text">{{ $t('admin.categoryManagement') }}</span>
|
||||
</router-link>
|
||||
<router-link to="/admin/brands" class="nav-link">
|
||||
<el-icon><Stamp /></el-icon>
|
||||
<span class="nav-text">{{ $t('admin.brandManagement') }}</span>
|
||||
</router-link>
|
||||
<router-link to="/admin/products" class="nav-link">
|
||||
<el-icon><Goods /></el-icon>
|
||||
<span class="nav-text">{{ $t('admin.productManagement') }}</span>
|
||||
@@ -152,7 +156,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { DataAnalysis, User, Folder, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { DataAnalysis, User, Folder, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '../store/user'
|
||||
import { useCartStore } from '../store/cart'
|
||||
|
||||
@@ -170,6 +174,7 @@ const pageTitles: Record<string, string> = {
|
||||
'/admin': '控制台',
|
||||
'/admin/users': '用户管理',
|
||||
'/admin/categories': '分类管理',
|
||||
'/admin/brands': '品牌管理',
|
||||
'/admin/products': '商品管理',
|
||||
'/admin/orders': '订单管理',
|
||||
'/admin/suppliers': '供应商管理',
|
||||
|
||||
@@ -40,6 +40,7 @@ const routes = [
|
||||
{ path: '', name: 'AdminDashboard', component: () => import('../views/admin/Dashboard.vue') },
|
||||
{ path: 'users', name: 'AdminUsers', component: () => import('../views/admin/Users.vue') },
|
||||
{ path: 'categories', name: 'AdminCategories', component: () => import('../views/admin/Categories.vue') },
|
||||
{ path: 'brands', name: 'AdminBrands', component: () => import('../views/admin/Brands.vue') },
|
||||
{ path: 'products', name: 'AdminProducts', component: () => import('../views/admin/Products.vue') },
|
||||
{ path: 'orders', name: 'AdminOrders', component: () => import('../views/admin/Orders.vue') },
|
||||
{ path: 'suppliers', name: 'AdminSuppliers', component: () => import('../views/admin/Suppliers.vue') },
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="brands-page">
|
||||
<div class="page-header">
|
||||
<el-button type="primary" @click="openAdd">创建品牌</el-button>
|
||||
</div>
|
||||
<div class="table-card">
|
||||
<el-table :data="brands">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="Logo" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.logo" :src="getImageUrl(row.logo)" fit="cover" style="width: 50px; height: 50px; border-radius: 4px;">
|
||||
<template #error>
|
||||
<div class="table-image-placeholder">-</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-else style="color: rgba(255,255,255,0.3)">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="品牌名称" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="editBrand(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteBrand(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-dialog v-model="showAdd" :title="editing ? '编辑品牌' : '创建品牌'" width="500px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="form.name" placeholder="请输入品牌名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入品牌描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Logo">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
action=""
|
||||
list-type="picture-card"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
accept="image/*"
|
||||
:limit="1"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAdd = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveBrand" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadFile, UploadUserFile } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { adminApi, brandApi, uploadApi } from '../../api'
|
||||
import { getImageUrl } from '../../utils/image'
|
||||
|
||||
const brands = ref<any[]>([])
|
||||
const showAdd = ref(false)
|
||||
const editing = ref<any>(null)
|
||||
const saving = ref(false)
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const uploadedUrl = ref('')
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
logo: '',
|
||||
sort_order: 0
|
||||
})
|
||||
|
||||
function handleFileChange(file: UploadFile) {
|
||||
if (file.status === 'ready') {
|
||||
file.status = 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileRemove() {
|
||||
fileList.value = []
|
||||
uploadedUrl.value = ''
|
||||
}
|
||||
|
||||
async function uploadFile(): Promise<string> {
|
||||
const file = fileList.value.find(f => f.raw && f.status === 'ready')
|
||||
if (!file) return uploadedUrl.value
|
||||
|
||||
try {
|
||||
const res: any = await uploadApi.uploadImage(file.raw as File)
|
||||
if (res.url) {
|
||||
file.url = res.url
|
||||
file.status = 'success'
|
||||
return res.url
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload failed:', e)
|
||||
file.status = 'fail'
|
||||
}
|
||||
return uploadedUrl.value
|
||||
}
|
||||
|
||||
async function fetchBrands() {
|
||||
const res: any = await brandApi.list()
|
||||
brands.value = res.data || []
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
editing.value = null
|
||||
Object.assign(form, { name: '', description: '', logo: '', sort_order: 0 })
|
||||
fileList.value = []
|
||||
uploadedUrl.value = ''
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
function editBrand(row: any) {
|
||||
editing.value = row
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
description: row.description || '',
|
||||
logo: row.logo || '',
|
||||
sort_order: row.sort_order || 0
|
||||
})
|
||||
uploadedUrl.value = row.logo || ''
|
||||
fileList.value = row.logo ? [{
|
||||
name: 'logo',
|
||||
url: getImageUrl(row.logo),
|
||||
status: 'success' as const,
|
||||
uid: Date.now()
|
||||
}] : []
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
async function saveBrand() {
|
||||
if (!form.name) {
|
||||
ElMessage.warning('请输入品牌名称')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const logo = await uploadFile()
|
||||
const submitData = { ...form, logo }
|
||||
if (editing.value) {
|
||||
await adminApi.updateBrand(editing.value.id, submitData)
|
||||
} else {
|
||||
await adminApi.createBrand(submitData)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
showAdd.value = false
|
||||
editing.value = null
|
||||
await fetchBrands()
|
||||
} catch (error: any) {
|
||||
const msg = error.response?.data?.error || '保存失败'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBrand(id: number) {
|
||||
await ElMessageBox.confirm('确定删除此品牌?', '确认删除')
|
||||
await adminApi.deleteBrand(id)
|
||||
ElMessage.success('删除成功')
|
||||
await fetchBrands()
|
||||
}
|
||||
|
||||
onMounted(fetchBrands)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.brands-page { padding: 0; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
.table-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 20px; }
|
||||
.table-image-placeholder { width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.04); border-radius: 4px; color: rgba(255,255,255,0.3); }
|
||||
|
||||
:deep(.el-upload--picture-card) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
:deep(.el-upload--picture-card:hover) {
|
||||
border-color: #4e6ef2;
|
||||
color: #4e6ef2;
|
||||
}
|
||||
|
||||
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__inner) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #4e6ef2;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
color: #fff;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
background: #2d2d44;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 16px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__title) {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__headerbtn .el-dialog__close) {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 16px 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user