Initial commit

This commit is contained in:
wangziqi
2026-02-09 09:51:14 -08:00
commit a7ce0a089e
104 changed files with 6470 additions and 0 deletions

View File

@@ -0,0 +1,644 @@
<template>
<a-layout style="min-height: 100vh">
<a-layout-sider :width="230" theme="dark" collapsible>
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">超级管理员后台</div>
<a-menu :selected-keys="[active]" @menu-item-click="onMenuClick">
<a-menu-item key="overview">数据概览</a-menu-item>
<a-menu-item key="orders">订单管理</a-menu-item>
<a-menu-item key="risk">风控审核</a-menu-item>
<a-menu-item key="products">商品管理</a-menu-item>
<a-menu-item key="audit">审核管理</a-menu-item>
<a-menu-item key="users">用户管理</a-menu-item>
<a-menu-item key="banners">轮播图设置</a-menu-item>
<a-menu-item key="reviews">评价管理</a-menu-item>
<a-menu-item key="logistics">物流总览</a-menu-item>
<a-menu-item key="inventory">库存总览</a-menu-item>
<a-menu-item key="profile">个人信息</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
<strong>{{ titleMap[active] }}</strong>
<a-space>
<span>{{ userStore.profile?.nickname }} (ADMIN)</span>
<a-button @click="goHome">前台首页</a-button>
<a-button status="danger" @click="logout">退出</a-button>
</a-space>
</a-layout-header>
<a-layout-content style="padding:16px;">
<a-card v-if="active==='overview'" title="平台概览">
<a-space style="margin-bottom: 16px">
<a-tag color="arcoblue" size="large">订单总量: {{ overview.orderCount || 0 }}</a-tag>
<a-tag color="green" size="large">销售额: ¥{{ (overview.salesAmount || 0).toLocaleString() }}</a-tag>
<a-button @click="loadOverview">刷新</a-button>
</a-space>
<a-row :gutter="16">
<a-col :span="12">
<a-card title="品类占比" size="small">
<div ref="categoryChartRef" style="height: 280px;"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="热销排行 Top10" size="small">
<div ref="hotChartRef" style="height: 280px;"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="通知公告" size="small">
<a-timeline v-if="overview.notifications && overview.notifications.length">
<a-timeline-item v-for="(note, idx) in overview.notifications" :key="idx" :color="idx === 0 ? 'red' : 'blue'">
{{ note }}
</a-timeline-item>
</a-timeline>
<a-empty v-else description="暂无公告" />
</a-card>
</a-card>
<a-card v-if="active==='orders'" title="订单管理">
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="openOrderModal(record)">修改</a-button>
<a-button size="mini" v-if="record.status === 'REFUND_REQUESTED'" @click="auditRefund(record, true)">通过退款</a-button>
<a-button size="mini" status="danger" v-if="record.status === 'REFUND_REQUESTED'" @click="auditRefund(record, false)">驳回退款</a-button>
<a-button size="mini" v-if="record.status === 'SHIPPED' || record.status === 'PAID'" @click="auditShipment(record, true)">发货审核通过</a-button>
<a-button size="mini" status="danger" v-if="record.status === 'SHIPPED'" @click="auditShipment(record, false)">发货审核驳回</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='risk'" title="订单风控与异常管控">
<a-space style="margin-bottom: 10px">
<a-button @click="loadRisks">刷新</a-button>
</a-space>
<a-table :columns="riskColumns" :data="risks" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='products'" title="商品管理">
<a-space style="margin-bottom: 10px">
<a-button type="primary" @click="openProductModal()">新增商品</a-button>
<a-button @click="loadProducts">刷新</a-button>
</a-space>
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
<template #approved="{ record }">
<a-tag :color="record.approved ? 'green' : 'orange'">{{ record.approved ? '已通过' : '待审核' }}</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="openProductModal(record)">编辑</a-button>
<a-button size="mini" @click="toggleApprove(record)">{{ record.approved ? '下架' : '通过' }}</a-button>
<a-button size="mini" status="danger" @click="deleteProduct(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='audit'" title="商家审核管理">
<a-table :columns="applyColumns" :data="applications" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-button size="mini" type="primary" @click="openAuditModal(record)">审核</a-button>
</template>
</a-table>
</a-card>
<a-card v-if="active==='users'" title="用户管理">
<a-space style="margin-bottom: 10px">
<a-button type="primary" @click="openUserModal()">新增用户</a-button>
<a-button @click="loadUsers">刷新</a-button>
</a-space>
<a-table :columns="userColumns" :data="users" :pagination="{ pageSize: 8 }">
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="openUserModal(record)">编辑</a-button>
<a-button size="mini" status="danger" @click="deleteUser(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='banners'" title="轮播图设置">
<a-space style="margin-bottom: 10px">
<a-button type="primary" @click="openBannerModal()">新增轮播图</a-button>
<a-button @click="loadBanners">刷新</a-button>
</a-space>
<a-table :columns="bannerColumns" :data="banners" :pagination="false">
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="openBannerModal(record)">编辑</a-button>
<a-button size="mini" status="danger" @click="deleteBanner(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='reviews'" title="评价管理">
<a-table :columns="reviewColumns" :data="reviews" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='logistics'" title="物流总览">
<a-table :columns="logisticsColumns" :data="logistics" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='inventory'" title="库存总览">
<a-table :columns="inventoryColumns" :data="inventory" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='profile'" title="个人信息">
<a-form :model="profile" layout="vertical" style="max-width: 480px">
<a-form-item label="账号"><a-input v-model="profile.username" disabled /></a-form-item>
<a-form-item label="角色"><a-input v-model="profile.role" disabled /></a-form-item>
<a-form-item label="昵称"><a-input v-model="profile.nickname" /></a-form-item>
<a-form-item label="手机号"><a-input v-model="profile.phone" /></a-form-item>
<a-form-item label="地址"><a-input v-model="profile.address" /></a-form-item>
<a-button type="primary" @click="saveProfile">保存修改</a-button>
</a-form>
</a-card>
</a-layout-content>
</a-layout>
</a-layout>
<a-modal v-model:visible="orderModalVisible" title="修改订单" @ok="submitOrderModal">
<a-form :model="orderForm" layout="vertical">
<a-form-item label="订单状态">
<a-select v-model="orderForm.status">
<a-option value="PENDING_PAYMENT">PENDING_PAYMENT</a-option>
<a-option value="PAID">PAID</a-option>
<a-option value="SHIPPED">SHIPPED</a-option>
<a-option value="COMPLETED">COMPLETED</a-option>
<a-option value="REFUND_REQUESTED">REFUND_REQUESTED</a-option>
<a-option value="REFUNDED">REFUNDED</a-option>
<a-option value="CANCELLED">CANCELLED</a-option>
</a-select>
</a-form-item>
<a-form-item label="物流信息">
<a-input v-model="orderForm.logisticsInfo" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:visible="auditModalVisible" title="审核商家" @ok="submitAuditModal">
<a-form :model="auditForm" layout="vertical">
<a-form-item label="审核结果">
<a-select v-model="auditForm.status">
<a-option value="APPROVED">通过</a-option>
<a-option value="REJECTED">拒绝</a-option>
</a-select>
</a-form-item>
<a-form-item label="备注">
<a-input v-model="auditForm.remark" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:visible="userModalVisible" :title="userForm.id ? '编辑用户' : '新增用户'" @ok="submitUserModal">
<a-form :model="userForm" layout="vertical">
<a-form-item label="账号"><a-input v-model="userForm.username" /></a-form-item>
<a-form-item label="密码"><a-input v-model="userForm.password" /></a-form-item>
<a-form-item label="角色">
<a-select v-model="userForm.role">
<a-option value="CUSTOMER">CUSTOMER</a-option>
<a-option value="MERCHANT">MERCHANT</a-option>
<a-option value="ADMIN">ADMIN</a-option>
</a-select>
</a-form-item>
<a-form-item label="昵称"><a-input v-model="userForm.nickname" /></a-form-item>
<a-form-item label="是否启用"><a-switch v-model="userForm.enabled" /></a-form-item>
</a-form>
</a-modal>
<a-modal v-model:visible="bannerModalVisible" :title="bannerForm.id ? '编辑轮播图' : '新增轮播图'" @ok="submitBannerModal">
<a-form :model="bannerForm" layout="vertical">
<a-form-item label="图片URL"><a-input v-model="bannerForm.imageUrl" /></a-form-item>
<a-form-item label="跳转URL"><a-input v-model="bannerForm.linkUrl" /></a-form-item>
<a-form-item label="排序"><a-input-number v-model="bannerForm.sortNo" style="width: 100%" /></a-form-item>
<a-form-item label="启用">
<a-switch v-model="bannerForm.enabled" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:visible="productModalVisible" :title="productForm.id ? '编辑商品' : '新增商品'" @ok="submitProductModal">
<a-form :model="productForm" layout="vertical">
<a-form-item label="商品名称"><a-input v-model="productForm.name" /></a-form-item>
<a-form-item label="分类"><a-input v-model="productForm.category" /></a-form-item>
<a-form-item label="描述"><a-input v-model="productForm.description" /></a-form-item>
<a-form-item label="价格"><a-input-number v-model="productForm.price" style="width: 100%" /></a-form-item>
<a-form-item label="库存"><a-input-number v-model="productForm.stock" style="width: 100%" /></a-form-item>
<a-form-item label="图片URL"><a-input v-model="productForm.imageUrl" /></a-form-item>
<a-form-item label="所属商家">
<a-select v-model="productForm.merchantId" placeholder="请选择商家">
<a-option v-for="m in merchantOptions" :key="m.value" :value="m.value">{{ m.label }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="审核通过"><a-switch v-model="productForm.approved" /></a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { onMounted, reactive, ref, watch, nextTick, onUnmounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import * as echarts from 'echarts'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const active = ref('overview')
const categoryChartRef = ref(null)
const hotChartRef = ref(null)
let categoryChart = null
let hotChart = null
const titleMap = {
overview: '数据概览', orders: '订单管理', risk: '风控审核', products: '商品管理', audit: '审核管理',
users: '用户管理', banners: '轮播图设置', reviews: '评价管理', logistics: '物流总览', inventory: '库存总览', profile: '个人信息'
}
const overview = reactive({})
const orders = ref([])
const risks = ref([])
const products = ref([])
const applications = ref([])
const users = ref([])
const merchantOptions = ref([])
const banners = ref([])
const reviews = ref([])
const logistics = ref([])
const inventory = ref([])
const profile = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
const orderModalVisible = ref(false)
const auditModalVisible = ref(false)
const userModalVisible = ref(false)
const bannerModalVisible = ref(false)
const productModalVisible = ref(false)
const orderForm = reactive({ id: null, status: 'PAID', logisticsInfo: '' })
const auditForm = reactive({ id: null, status: 'APPROVED', remark: '' })
const userForm = reactive({ id: null, username: '', password: '123456', role: 'CUSTOMER', nickname: '', enabled: true })
const bannerForm = reactive({ id: null, imageUrl: '', linkUrl: '', sortNo: 1, enabled: true })
const productForm = reactive({ id: null, name: '', category: '', description: '', price: 0, stock: 0, imageUrl: '', merchantId: null, approved: false })
const orderColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '金额', dataIndex: 'totalAmount' },
{ title: '状态', dataIndex: 'status' },
{ title: '物流', dataIndex: 'logisticsInfo' },
{ title: '操作', slotName: 'actions' }
]
const productColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '商品名', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category' },
{ title: '价格', dataIndex: 'price' },
{ title: '库存', dataIndex: 'stock' },
{ title: '所属商家', dataIndex: 'merchantName' },
{ title: '商家账号', dataIndex: 'merchantUsername' },
{ title: '审核', slotName: 'approved' },
{ title: '操作', slotName: 'actions' }
]
const applyColumns = [
{ title: '申请ID', dataIndex: 'id' },
{ title: '申请人账号', dataIndex: 'applicantUsername' },
{ title: '申请人昵称', dataIndex: 'applicantNickname' },
{ title: '资质', dataIndex: 'qualification' },
{ title: '状态', dataIndex: 'status' },
{ title: '操作', slotName: 'actions' }
]
const userColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '账号', dataIndex: 'username' },
{ title: '角色', dataIndex: 'role' },
{ title: '昵称', dataIndex: 'nickname' },
{ title: '状态', slotName: 'enabled' },
{ title: '操作', slotName: 'actions' }
]
const riskColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '状态', dataIndex: 'status' },
{ title: '金额', dataIndex: 'totalAmount' },
{ title: '风险类型', dataIndex: 'riskType' },
{ title: '风险说明', dataIndex: 'riskReason' }
]
const bannerColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '图片', dataIndex: 'imageUrl' },
{ title: '链接', dataIndex: 'linkUrl' },
{ title: '排序', dataIndex: 'sortNo' },
{ title: '状态', slotName: 'enabled' },
{ title: '操作', slotName: 'actions' }
]
const reviewColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '商品名称', dataIndex: 'productName' },
{ title: '顾客账号', dataIndex: 'customerUsername' },
{ title: '评分', dataIndex: 'rating' },
{ title: '内容', dataIndex: 'content' },
{ title: '时间', dataIndex: 'createdAt' }
]
const logisticsColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '商家账号', dataIndex: 'merchantUsername' },
{ title: '状态', dataIndex: 'status' },
{ title: '备注', dataIndex: 'note' },
{ title: '时间', dataIndex: 'createdAt' }
]
const inventoryColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '商品名称', dataIndex: 'productName' },
{ title: '商家账号', dataIndex: 'merchantUsername' },
{ title: '变动', dataIndex: 'changeQty' },
{ title: '备注', dataIndex: 'note' },
{ title: '时间', dataIndex: 'createdAt' }
]
const onMenuClick = (key) => { active.value = key }
const goHome = () => router.push('/')
const logout = async () => { await userStore.logout(); router.replace('/login') }
const loadOverview = async () => {
Object.assign(overview, await api.adminOverview())
await nextTick()
initCharts()
}
const initCharts = () => {
if (!overview.categoryRatio) overview.categoryRatio = {}
if (!overview.hotProducts) overview.hotProducts = {}
const categoryData = Object.entries(overview.categoryRatio).map(([name, value]) => ({ name, value }))
if (categoryChart) {
categoryChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [{ type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, data: categoryData }]
})
} else if (categoryChartRef.value) {
categoryChart = echarts.init(categoryChartRef.value)
categoryChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [{ type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, data: categoryData }]
})
}
const hotEntries = Object.entries(overview.hotProducts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
const hotData = hotEntries.map(([name, value]) => ({ name: name.length > 10 ? name.substring(0, 10) + '...' : name, value }))
if (hotChart) {
hotChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
})
} else if (hotChartRef.value) {
hotChart = echarts.init(hotChartRef.value)
hotChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
})
}
}
const handleResize = () => {
categoryChart?.resize()
hotChart?.resize()
}
watch(active, async (v) => {
if (v === 'overview') {
await loadOverview()
await nextTick()
initCharts()
}
if (v === 'orders') return loadOrders()
if (v === 'risk') return loadRisks()
if (v === 'products') return loadProducts()
if (v === 'audit') return loadApplications()
if (v === 'users') return loadUsers()
if (v === 'banners') return loadBanners()
if (v === 'reviews') return loadReviews()
if (v === 'logistics') return loadLogistics()
if (v === 'inventory') return loadInventory()
if (v === 'profile') return loadProfile()
})
const loadOrders = async () => (orders.value = await api.adminOrders())
const loadRisks = async () => (risks.value = await api.adminOrderRisks())
const loadProducts = async () => (products.value = await api.adminProductViews())
const loadApplications = async () => (applications.value = await api.adminMerchantApplications())
const loadUsers = async () => (users.value = await api.adminUsers())
const loadBanners = async () => (banners.value = await api.adminBanners())
const loadReviews = async () => (reviews.value = await api.adminReviews())
const loadLogistics = async () => (logistics.value = await api.adminLogistics())
const loadInventory = async () => (inventory.value = await api.adminInventory())
const loadProfile = async () => {
const me = await api.me()
profile.username = me.username
profile.role = me.role
profile.nickname = me.nickname || ''
profile.phone = me.phone || ''
profile.address = me.address || ''
}
const openOrderModal = (row) => {
orderForm.id = row.id
orderForm.status = row.status
orderForm.logisticsInfo = row.logisticsInfo || ''
orderModalVisible.value = true
}
const submitOrderModal = async () => {
await api.adminUpdateOrder(orderForm.id, { status: orderForm.status, logisticsInfo: orderForm.logisticsInfo })
Message.success('订单已更新')
orderModalVisible.value = false
await loadOrders()
}
const openAuditModal = (row) => {
auditForm.id = row.id
auditForm.status = row.status === 'APPROVED' ? 'APPROVED' : 'REJECTED'
auditForm.remark = row.remark || ''
auditModalVisible.value = true
}
const submitAuditModal = async () => {
await api.adminAuditMerchantApplication(auditForm.id, { status: auditForm.status, remark: auditForm.remark })
Message.success('审核完成')
auditModalVisible.value = false
await loadApplications()
}
const openUserModal = (row = null) => {
if (row) {
userForm.id = row.id
userForm.username = row.username
userForm.password = row.password || '123456'
userForm.role = row.role
userForm.nickname = row.nickname || ''
userForm.enabled = row.enabled !== false
} else {
userForm.id = null
userForm.username = ''
userForm.password = '123456'
userForm.role = 'CUSTOMER'
userForm.nickname = ''
userForm.enabled = true
}
userModalVisible.value = true
}
const submitUserModal = async () => {
const username = (userForm.username || '').trim()
const password = (userForm.password || '').trim()
if (!username) return Message.warning('请输入账号')
if (!password) return Message.warning('请输入密码')
await api.adminSaveUser({ ...userForm, username, password })
Message.success(userForm.id ? '用户已更新' : '用户已新增')
userModalVisible.value = false
await loadUsers()
}
const deleteUser = async (id) => {
await api.adminDeleteUser(id)
Message.success('用户已删除')
await loadUsers()
}
const auditRefund = async (row, approve) => {
const remark = window.prompt(approve ? '请输入退款通过备注(可为空)' : '请输入驳回原因(可为空)', '') || ''
await api.adminAuditRefund(row.id, { approve, remark })
Message.success(approve ? '退款审核已通过' : '退款审核已驳回')
await Promise.all([loadOrders(), loadRisks()])
}
const auditShipment = async (row, approve) => {
const remark = window.prompt(approve ? '请输入发货审核备注(可为空)' : '请输入驳回原因(可为空)', '') || ''
await api.adminAuditShipment(row.id, { approve, remark })
Message.success(approve ? '发货审核已通过' : '发货审核已驳回')
await Promise.all([loadOrders(), loadRisks()])
}
const openBannerModal = (row = null) => {
if (row) {
bannerForm.id = row.id
bannerForm.imageUrl = row.imageUrl || ''
bannerForm.linkUrl = row.linkUrl || ''
bannerForm.sortNo = row.sortNo || 1
bannerForm.enabled = !!row.enabled
} else {
bannerForm.id = null
bannerForm.imageUrl = ''
bannerForm.linkUrl = ''
bannerForm.sortNo = 1
bannerForm.enabled = true
}
bannerModalVisible.value = true
}
const submitBannerModal = async () => {
await api.adminSaveBanner({ ...bannerForm })
Message.success(bannerForm.id ? '轮播图已更新' : '轮播图已新增')
bannerModalVisible.value = false
await loadBanners()
}
const deleteBanner = async (id) => {
await api.adminDeleteBanner(id)
Message.success('轮播图已删除')
await loadBanners()
}
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()
}
const saveProfile = async () => {
await api.updateMe({ nickname: profile.nickname, phone: profile.phone, address: profile.address })
Message.success('已保存')
}
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 }))
}
onMounted(async () => {
try {
await userStore.fetchMe()
if (userStore.role !== 'ADMIN') {
Message.warning('请使用管理员账号访问')
return router.replace('/login')
}
await loadOverview()
await loadProfile()
await loadMerchantOptions()
window.addEventListener('resize', handleResize)
} catch (e) {
Message.error(e.message)
router.replace('/login')
}
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
categoryChart?.dispose()
hotChart?.dispose()
})
</script>

View File

@@ -0,0 +1,86 @@
<template>
<div class="container">
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
<h2>萌贝母婴商城 - 购物车</h2>
<a-space>
<a-button @click="goProducts">继续购物</a-button>
<a-button @click="goOrders">我的订单</a-button>
<a-button @click="goFavorites">我的收藏</a-button>
<a-button @click="goProfile">个人信息</a-button>
<a-button @click="logout">退出</a-button>
</a-space>
</a-space>
<a-card title="购物车结算">
<a-space style="margin-bottom: 10px">
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
<a-button type="primary" @click="checkout">结算购物车</a-button>
<a-button @click="loadCart">刷新</a-button>
</a-space>
<a-table :columns="cartColumns" :data="cart" :pagination="false">
<template #actions="{ record }">
<a-button size="mini" status="danger" @click="removeCart(record.productId)">移除</a-button>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const cart = ref([])
const address = ref(localStorage.getItem('customer_address') || '辽宁省大连市高新区')
const cartColumns = [
{ title: '商品名称', dataIndex: 'productName' },
{ title: '单价', dataIndex: 'unitPrice' },
{ title: '数量', dataIndex: 'quantity' },
{ title: '小计', dataIndex: 'subtotal' },
{ title: '操作', slotName: 'actions' }
]
const goProducts = () => router.push('/products')
const goOrders = () => router.push('/orders')
const goFavorites = () => router.push('/favorites')
const goProfile = () => router.push('/profile')
const logout = async () => {
await userStore.logout()
router.replace('/login')
}
const loadCart = async () => {
cart.value = await api.customerCartViews()
}
const removeCart = async (productId) => {
await api.delCart(productId)
Message.success('已移除')
await loadCart()
}
const checkout = async () => {
if (!address.value) return Message.warning('请填写收货地址')
localStorage.setItem('customer_address', address.value)
await api.checkout({ address: address.value })
Message.success('结算成功')
router.push('/orders')
}
onMounted(async () => {
try {
await userStore.fetchMe()
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
await loadCart()
} catch {
router.replace('/login')
}
})
</script>

View File

@@ -0,0 +1,173 @@
<template>
<a-layout style="min-height: 100vh">
<a-layout-sider :width="220" theme="dark" collapsible>
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">顾客中心</div>
<a-menu :selected-keys="[active]" @menu-item-click="(k)=>active=k">
<a-menu-item key="products">商品购买</a-menu-item>
<a-menu-item key="cart">购物车</a-menu-item>
<a-menu-item key="orders">订单管理</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
<strong>{{ titleMap[active] }}</strong>
<a-space>
<a-input v-model="address" style="width: 280px" placeholder="默认收货地址" />
<a-button @click="goHome">去商城</a-button>
<a-button status="danger" @click="logout">退出</a-button>
</a-space>
</a-layout-header>
<a-layout-content style="padding:16px;">
<a-card v-if="active==='products'" title="商品购买">
<a-space style="margin-bottom: 10px">
<a-input-search v-model="keyword" placeholder="搜索商品" search-button @search="loadProducts" />
<a-button @click="loadProducts">刷新</a-button>
</a-space>
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" @click="addCart(record.id)">加入购物车</a-button>
<a-button size="mini" type="primary" @click="buyNow(record.id)">立即购买</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='cart'" title="我的购物车">
<a-space style="margin-bottom: 10px">
<a-button type="primary" @click="checkout">结算购物车</a-button>
<a-button @click="loadCart">刷新</a-button>
</a-space>
<a-table :columns="cartColumns" :data="cart" :pagination="false">
<template #actions="{ record }">
<a-button size="mini" status="danger" @click="delCart(record.productId)">移除</a-button>
</template>
</a-table>
</a-card>
<a-card v-if="active==='orders'" title="我的订单">
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" @click="refund(record.id)">退款</a-button>
<a-button size="mini" @click="delOrder(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const active = ref('products')
const titleMap = { products: '商品购买', cart: '购物车管理', orders: '订单管理' }
const address = ref('辽宁省大连市高新区')
const keyword = ref('')
const products = ref([])
const cart = ref([])
const orders = ref([])
const productColumns = [
{ title: '名称', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category' },
{ title: '价格', dataIndex: 'price' },
{ title: '库存', dataIndex: 'stock' },
{ title: '操作', slotName: 'actions' }
]
const cartColumns = [
{ title: '商品名称', dataIndex: 'productName' },
{ title: '分类', dataIndex: 'category' },
{ title: '单价', dataIndex: 'unitPrice' },
{ title: '数量', dataIndex: 'quantity' },
{ title: '小计', dataIndex: 'subtotal' },
{ title: '库存', dataIndex: 'stock' },
{ title: '操作', slotName: 'actions' }
]
const orderColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '金额', dataIndex: 'totalAmount' },
{ title: '状态', dataIndex: 'status' },
{ title: '操作', slotName: 'actions' }
]
const goHome = () => router.push('/')
const logout = async () => {
await userStore.logout()
router.replace('/login')
}
const loadProducts = async () => {
products.value = await api.products(keyword.value)
}
const loadCart = async () => {
cart.value = await api.customerCartViews()
}
const loadOrders = async () => {
orders.value = await api.customerOrders()
}
const addCart = async (productId) => {
await api.addCart({ productId, quantity: 1 })
Message.success('已加入购物车')
await loadCart()
}
const delCart = async (productId) => {
await api.delCart(productId)
Message.success('已移除')
await loadCart()
}
const buyNow = async (productId) => {
if (!address.value) return Message.warning('请先填写收货地址')
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
Message.success('购买成功')
await loadOrders()
active.value = 'orders'
}
const checkout = async () => {
if (!address.value) return Message.warning('请先填写收货地址')
await api.checkout({ address: address.value })
Message.success('结算成功')
await Promise.all([loadCart(), loadOrders()])
active.value = 'orders'
}
const refund = async (id) => {
await api.refundOrder(id, { reason: '不想要了' })
Message.success('退款申请已提交')
await loadOrders()
}
const delOrder = async (id) => {
await api.deleteOrder(id)
Message.success('订单已删除')
await loadOrders()
}
watch(active, async (v) => {
if (v === 'products') await loadProducts()
if (v === 'cart') await loadCart()
if (v === 'orders') await loadOrders()
})
onMounted(async () => {
await userStore.fetchMe()
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
await Promise.all([loadProducts(), loadCart(), loadOrders()])
})
</script>

View File

@@ -0,0 +1,86 @@
<template>
<div class="container">
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
<h2>萌贝母婴商城 - 我的收藏</h2>
<a-space>
<a-button @click="goProducts">继续购物</a-button>
<a-button @click="goCart">购物车</a-button>
<a-button @click="goOrders">我的订单</a-button>
</a-space>
</a-space>
<a-card title="收藏列表">
<a-space style="margin-bottom: 10px">
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
<a-button @click="loadFavorites">刷新</a-button>
</a-space>
<a-table :columns="columns" :data="favorites" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" @click="addCart(record.productId)">加入购物车</a-button>
<a-button size="mini" type="primary" @click="buyNow(record.productId)">立即购买</a-button>
<a-button size="mini" status="danger" @click="removeFavorite(record.productId)">取消收藏</a-button>
</a-space>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const favorites = ref([])
const address = ref(localStorage.getItem('customer_address') || '辽宁省大连市高新区')
const columns = [
{ title: '商品名称', dataIndex: 'productName' },
{ title: '分类', dataIndex: 'category' },
{ title: '单价', dataIndex: 'unitPrice' },
{ title: '库存', dataIndex: 'stock' },
{ title: '操作', slotName: 'actions' }
]
const goProducts = () => router.push('/products')
const goCart = () => router.push('/cart')
const goOrders = () => router.push('/orders')
const loadFavorites = async () => {
favorites.value = await api.customerFavoriteViews()
}
const removeFavorite = async (productId) => {
await api.deleteFavorite(productId)
Message.success('已取消收藏')
await loadFavorites()
}
const addCart = async (productId) => {
await api.addCart({ productId, quantity: 1 })
Message.success('已加入购物车')
}
const buyNow = async (productId) => {
if (!address.value) return Message.warning('请先填写收货地址')
localStorage.setItem('customer_address', address.value)
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
Message.success('购买成功')
router.push('/orders')
}
onMounted(async () => {
try {
await userStore.fetchMe()
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
await loadFavorites()
} catch {
router.replace('/login')
}
})
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div class="container">
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
<h2>萌贝母婴商城</h2>
<a-space>
<a-button v-if="!userStore.loggedIn" type="primary" @click="goLogin">登录</a-button>
<template v-else>
<span>{{ userStore.profile?.nickname }} ({{ userStore.role }})</span>
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goCart">购物车</a-button>
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goOrders">我的订单</a-button>
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goFavorites">我的收藏</a-button>
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goProfile">个人信息</a-button>
<a-button v-if="userStore.role === 'ADMIN' || userStore.role === 'MERCHANT'" type="primary" @click="goConsole">进入工作台</a-button>
<a-button @click="logout">退出</a-button>
</template>
</a-space>
</a-space>
<a-carousel auto-play style="height: 220px; margin-bottom: 16px">
<a-carousel-item v-for="b in banners" :key="b.id">
<img :src="b.imageUrl" style="width: 100%; height: 220px; object-fit: cover" />
</a-carousel-item>
</a-carousel>
<a-input-search v-model="keyword" placeholder="搜索商品" search-button @search="loadProducts" />
<a-grid :cols="{ xs: 1, md: 3 }" :col-gap="12" :row-gap="12" style="margin-top: 16px">
<a-grid-item v-for="p in products" :key="p.id">
<a-card>
<template #title>{{ p.name }}</template>
<div>分类{{ p.category || '未分类' }}</div>
<div>价格{{ p.price }}</div>
<div>库存{{ p.stock }}</div>
<div style="margin-top: 8px">
<a-space>
<a-button size="small" @click="addCart(p.id)" :disabled="userStore.role !== 'CUSTOMER'">加入购物车</a-button>
<a-button size="small" type="primary" @click="buyNow(p.id)" :disabled="userStore.role !== 'CUSTOMER'">立即购买</a-button>
<a-button size="small" @click="addFavorite(p.id)" :disabled="userStore.role !== 'CUSTOMER'">收藏</a-button>
</a-space>
</div>
</a-card>
</a-grid-item>
</a-grid>
<a-card v-if="userStore.role === 'CUSTOMER'" title="前台购买操作" style="margin-top: 16px">
<a-space>
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
<span>你可以在此页立即购买也可以去购物车结算</span>
</a-space>
</a-card>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const keyword = ref('')
const products = ref([])
const banners = ref([])
const address = ref('辽宁省大连市高新区')
const goLogin = () => router.push('/login')
const goCart = () => router.push('/cart')
const goOrders = () => router.push('/orders')
const goFavorites = () => router.push('/favorites')
const goProfile = () => router.push('/profile')
const goConsole = () => {
if (userStore.role === 'ADMIN') return router.push('/admin')
if (userStore.role === 'MERCHANT') return router.push('/merchant')
return router.push('/products')
}
const logout = async () => {
await userStore.logout()
Message.success('已退出')
}
const loadProducts = async () => {
products.value = await api.products(keyword.value)
}
const loadBanners = async () => {
banners.value = await api.banners()
if (banners.value.length === 0) {
banners.value = [{ id: 0, imageUrl: 'https://picsum.photos/1200/220?baby=1' }]
}
}
const addCart = async (productId) => {
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
await api.addCart({ productId, quantity: 1 })
Message.success('已加入购物车')
}
const buyNow = async (productId) => {
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
if (!address.value) return Message.warning('请先填写收货地址')
localStorage.setItem('customer_address', address.value)
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
Message.success('购买成功')
router.push('/orders')
}
const addFavorite = async (productId) => {
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
await api.addFavorite({ productId })
Message.success('已收藏')
}
onMounted(async () => {
await loadBanners()
await loadProducts()
const token = localStorage.getItem('token')
if (token) {
try {
await userStore.fetchMe()
if (userStore.role === 'CUSTOMER') {
address.value = localStorage.getItem('customer_address') || address.value
}
} catch (e) {
userStore.profile = null
localStorage.removeItem('token')
}
}
})
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="container" style="max-width: 460px; margin-top: 80px">
<a-card title="萌贝母婴商城 - 登录/注册">
<a-form :model="form" layout="vertical">
<a-form-item label="账号">
<a-input v-model="form.username" placeholder="请输入账号" />
</a-form-item>
<a-form-item label="密码">
<a-input-password v-model="form.password" placeholder="请输入密码" />
</a-form-item>
<a-space>
<a-button type="primary" @click="onLogin">登录</a-button>
<a-button @click="onRegister">注册</a-button>
</a-space>
</a-form>
</a-card>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const form = reactive({ username: '', password: '' })
const routeByRole = (role) => {
if (role === 'ADMIN') return '/admin'
if (role === 'MERCHANT') return '/merchant'
return '/products'
}
const onLogin = async () => {
try {
const res = await api.login({ username: form.username, password: form.password })
localStorage.setItem('token', res.token)
await userStore.fetchMe()
Message.success('登录成功')
router.replace(routeByRole(userStore.role))
} catch (e) {
Message.error(e.message)
}
}
const onRegister = async () => {
try {
await api.register({ username: form.username, password: form.password })
Message.success('注册成功,请登录')
} catch (e) {
Message.error(e.message)
}
}
</script>

View File

@@ -0,0 +1,355 @@
<template>
<a-layout style="min-height: 100vh">
<a-layout-sider :width="220" theme="dark" collapsible>
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">商家后台</div>
<a-menu :selected-keys="[active]" @menu-item-click="(k)=>active=k">
<a-menu-item key="overview">数据概览</a-menu-item>
<a-menu-item key="products">商品管理</a-menu-item>
<a-menu-item key="orders">订单管理</a-menu-item>
<a-menu-item key="reviews">评价管理</a-menu-item>
<a-menu-item key="logistics">物流管理</a-menu-item>
<a-menu-item key="inventory">库存管理</a-menu-item>
<a-menu-item key="profile">个人信息</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
<strong>{{ titleMap[active] }}</strong>
<a-space>
<a-button @click="goHome">去商城</a-button>
<a-button status="danger" @click="logout">退出</a-button>
</a-space>
</a-layout-header>
<a-layout-content style="padding:16px;">
<a-card v-if="active==='overview'" title="经营概览">
<a-space style="margin-bottom: 16px">
<a-tag color="arcoblue" size="large">订单量: {{ overview.orderCount || 0 }}</a-tag>
<a-tag color="green" size="large">销售额: ¥{{ (overview.salesAmount || 0).toLocaleString() }}</a-tag>
<a-button @click="loadOverview">刷新</a-button>
</a-space>
<a-row :gutter="16">
<a-col :span="12">
<a-card title="品类占比" size="small">
<div ref="categoryChartRef" style="height: 250px;"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="热销排行 Top10" size="small">
<div ref="hotChartRef" style="height: 250px;"></div>
</a-card>
</a-col>
</a-row>
<a-divider />
<a-card title="通知公告" size="small">
<a-timeline v-if="overview.notifications && overview.notifications.length">
<a-timeline-item v-for="(note, idx) in overview.notifications" :key="idx" :color="idx === 0 ? 'red' : 'blue'">
{{ note }}
</a-timeline-item>
</a-timeline>
<a-empty v-else description="暂无公告" />
</a-card>
</a-card>
<a-card v-if="active==='products'" title="商品管理">
<a-space style="margin-bottom: 10px">
<a-button type="primary" @click="openProductModal()">新增商品</a-button>
<a-button @click="loadProducts">刷新</a-button>
</a-space>
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="openProductModal(record)">编辑</a-button>
<a-button size="mini" status="danger" @click="delProduct(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='orders'" title="订单管理">
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="ship(record.id)">发货</a-button>
<a-button size="mini" @click="refund(record.id, true)">同意退款</a-button>
<a-button size="mini" @click="openOrderLogistics(record.id)">查看物流</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card v-if="active==='reviews'" title="评价管理">
<a-table :columns="reviewColumns" :data="reviews" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='logistics'" title="物流管理">
<a-table :columns="logisticsColumns" :data="logistics" :pagination="{ pageSize: 8 }" />
</a-card>
<a-card v-if="active==='inventory'" title="库存管理">
<a-table :columns="inventoryColumns" :data="inventory" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-button size="mini" status="danger" @click="deleteInventory(record.id)">删除</a-button>
</template>
</a-table>
</a-card>
<a-card v-if="active==='profile'" title="个人信息">
<a-form :model="profile" layout="vertical" style="max-width: 480px">
<a-form-item label="账号"><a-input v-model="profile.username" disabled /></a-form-item>
<a-form-item label="角色"><a-input v-model="profile.role" disabled /></a-form-item>
<a-form-item label="昵称"><a-input v-model="profile.nickname" /></a-form-item>
<a-form-item label="手机号"><a-input v-model="profile.phone" /></a-form-item>
<a-form-item label="地址"><a-input v-model="profile.address" /></a-form-item>
<a-button type="primary" @click="saveProfile">保存修改</a-button>
</a-form>
</a-card>
</a-layout-content>
</a-layout>
</a-layout>
<a-modal v-model:visible="productModalVisible" :title="productForm.id ? '编辑商品' : '新增商品'" @ok="submitProduct">
<a-form :model="productForm" layout="vertical">
<a-form-item label="商品名称"><a-input v-model="productForm.name" /></a-form-item>
<a-form-item label="分类"><a-input v-model="productForm.category" /></a-form-item>
<a-form-item label="描述"><a-input v-model="productForm.description" /></a-form-item>
<a-form-item label="价格"><a-input-number v-model="productForm.price" style="width: 100%" /></a-form-item>
<a-form-item label="库存"><a-input-number v-model="productForm.stock" style="width: 100%" /></a-form-item>
<a-form-item label="图片URL"><a-input v-model="productForm.imageUrl" /></a-form-item>
</a-form>
</a-modal>
<a-modal v-model:visible="logisticsModalVisible" title="订单物流">
<a-table :columns="logisticsColumns" :data="filteredLogistics" :pagination="false" />
</a-modal>
</template>
<script setup>
import { onMounted, reactive, ref, watch, nextTick, onUnmounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import * as echarts from 'echarts'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const active = ref('overview')
const titleMap = {
overview: '数据概览', products: '商品管理', orders: '订单管理', reviews: '评价管理',
logistics: '物流管理', inventory: '库存管理', profile: '个人信息'
}
const categoryChartRef = ref(null)
const hotChartRef = ref(null)
let categoryChart = null
let hotChart = null
const overview = reactive({})
const products = ref([])
const orders = ref([])
const reviews = ref([])
const logistics = ref([])
const inventory = ref([])
const productModalVisible = ref(false)
const logisticsModalVisible = ref(false)
const productForm = reactive({ id: null, name: '', category: '', description: '', price: 0, stock: 0, imageUrl: '', approved: false })
const profile = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
const filteredLogistics = ref([])
const productColumns = [
{ title: 'ID', dataIndex: 'id' },
{ title: '名称', dataIndex: 'name' },
{ title: '分类', dataIndex: 'category' },
{ title: '价格', dataIndex: 'price' },
{ title: '库存', dataIndex: 'stock' },
{ title: '审核', dataIndex: 'approved' },
{ title: '操作', slotName: 'actions' }
]
const orderColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '金额', dataIndex: 'totalAmount' },
{ title: '状态', dataIndex: 'status' },
{ title: '操作', slotName: 'actions' }
]
const reviewColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '商品名称', dataIndex: 'productName' },
{ title: '评分', dataIndex: 'rating' },
{ title: '内容', dataIndex: 'content' },
{ title: '时间', dataIndex: 'createdAt' }
]
const logisticsColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '状态', dataIndex: 'status' },
{ title: '备注', dataIndex: 'note' },
{ title: '时间', dataIndex: 'createdAt' }
]
const inventoryColumns = [
{ title: '商品名称', dataIndex: 'productName' },
{ title: '变动', dataIndex: 'changeQty' },
{ title: '备注', dataIndex: 'note' },
{ title: '时间', dataIndex: 'createdAt' },
{ title: '操作', slotName: 'actions' }
]
const goHome = () => router.push('/products')
const logout = async () => { await userStore.logout(); router.replace('/login') }
const loadOverview = async () => {
Object.assign(overview, await api.merchantOverview())
await nextTick()
initCharts()
}
const initCharts = () => {
if (!overview.categoryRatio) overview.categoryRatio = {}
if (!overview.hotProducts) overview.hotProducts = {}
const categoryData = Object.entries(overview.categoryRatio).map(([name, value]) => ({ name, value }))
if (categoryChart) {
categoryChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [{ type: 'pie', radius: ['40%', '70%'], data: categoryData }]
})
} else if (categoryChartRef.value) {
categoryChart = echarts.init(categoryChartRef.value)
categoryChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [{ type: 'pie', radius: ['40%', '70%'], data: categoryData }]
})
}
const hotEntries = Object.entries(overview.hotProducts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
const hotData = hotEntries.map(([name, value]) => ({ name: name.length > 10 ? name.substring(0, 10) + '...' : name, value }))
if (hotChart) {
hotChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
})
} else if (hotChartRef.value) {
hotChart = echarts.init(hotChartRef.value)
hotChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
})
}
}
const handleResize = () => {
categoryChart?.resize()
hotChart?.resize()
}
watch(active, async (v) => {
if (v === 'overview') {
await loadOverview()
await nextTick()
initCharts()
}
if (v === 'products') return loadProducts()
if (v === 'orders') return loadOrders()
if (v === 'reviews') return loadReviews()
if (v === 'logistics') return loadLogistics()
if (v === 'inventory') return loadInventory()
})
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
productForm.stock = row.stock
productForm.imageUrl = row.imageUrl
} else {
productForm.id = null
productForm.name = ''
productForm.category = ''
productForm.description = ''
productForm.price = 0
productForm.stock = 0
productForm.imageUrl = ''
}
productModalVisible.value = true
}
const submitProduct = async () => {
await api.saveMerchantProduct({ ...productForm })
Message.success('商品已保存')
productModalVisible.value = false
await loadProducts()
}
const delProduct = async (id) => {
await api.deleteMerchantProduct(id)
Message.success('商品已删除')
await loadProducts()
}
const ship = async (id) => {
await api.shipOrder(id, { note: '已发货' })
Message.success('发货成功')
await loadOrders()
}
const refund = async (id, agree) => {
await api.merchantRefund(id, { agree })
Message.success('退款已处理')
await loadOrders()
}
const deleteInventory = async (id) => {
await api.deleteMerchantInventory(id)
Message.success('库存记录已删除')
await loadInventory()
}
const openOrderLogistics = async (orderId) => {
await loadLogistics()
filteredLogistics.value = logistics.value.filter((l) => l.orderId === orderId)
logisticsModalVisible.value = true
}
const loadProfile = async () => {
const me = await api.me()
profile.username = me.username
profile.role = me.role
profile.nickname = me.nickname || ''
profile.phone = me.phone || ''
profile.address = me.address || ''
}
const saveProfile = async () => {
await api.updateMe({ nickname: profile.nickname, phone: profile.phone, address: profile.address })
Message.success('已保存')
}
onMounted(async () => {
await userStore.fetchMe()
if (userStore.role !== 'MERCHANT') return router.replace('/login')
await loadOverview()
await loadProfile()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
categoryChart?.dispose()
hotChart?.dispose()
})
</script>

View File

@@ -0,0 +1,154 @@
<template>
<div class="container">
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
<h2>萌贝母婴商城 - 我的订单</h2>
<a-space>
<a-button @click="goProducts">继续购物</a-button>
<a-button @click="goCart">购物车</a-button>
<a-button @click="goFavorites">我的收藏</a-button>
<a-button @click="goProfile">个人信息</a-button>
<a-button @click="loadOrders">刷新</a-button>
</a-space>
</a-space>
<a-card title="订单列表">
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
<template #actions="{ record }">
<a-space>
<a-button size="mini" @click="openLogistics(record.id)">查看物流</a-button>
<a-button size="mini" @click="openAddress(record)">修改地址</a-button>
<a-button size="mini" @click="openReview(record.id)">评价</a-button>
<a-button size="mini" @click="refund(record.id)">退款</a-button>
<a-button size="mini" @click="deleteOrder(record.id)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
</div>
<a-modal v-model:visible="addressModalVisible" title="修改收货地址" @ok="submitAddress">
<a-input v-model="addressForm.address" placeholder="请输入新的收货地址" />
</a-modal>
<a-modal v-model:visible="logisticsModalVisible" title="物流信息">
<a-table :columns="logisticsColumns" :data="logistics" :pagination="false" />
</a-modal>
<a-modal v-model:visible="reviewModalVisible" title="提交评价" @ok="submitReview">
<a-form :model="reviewForm" layout="vertical">
<a-form-item label="商品">
<a-select v-model="reviewForm.productId">
<a-option v-for="item in orderItems" :key="item.productId" :value="item.productId">{{ item.productName }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="评分">
<a-rate v-model="reviewForm.rating" :count="5" />
</a-form-item>
<a-form-item label="内容">
<a-textarea v-model="reviewForm.content" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const orders = ref([])
const orderColumns = [
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '金额', dataIndex: 'totalAmount' },
{ title: '状态', dataIndex: 'status' },
{ title: '地址', dataIndex: 'address' },
{ title: '操作', slotName: 'actions' }
]
const goProducts = () => router.push('/products')
const goCart = () => router.push('/cart')
const goFavorites = () => router.push('/favorites')
const goProfile = () => router.push('/profile')
const loadOrders = async () => {
orders.value = await api.customerOrders()
}
const addressModalVisible = ref(false)
const logisticsModalVisible = ref(false)
const reviewModalVisible = ref(false)
const addressForm = reactive({ id: null, address: '' })
const logistics = ref([])
const logisticsColumns = [
{ title: '状态', dataIndex: 'status' },
{ title: '备注', dataIndex: 'note' },
{ title: '时间', dataIndex: 'createdAt' }
]
const orderItems = ref([])
const reviewForm = reactive({ orderId: null, productId: null, rating: 5, content: '' })
const refund = async (id) => {
await api.refundOrder(id, { reason: '不想要了' })
Message.success('已提交退款申请')
await loadOrders()
}
const deleteOrder = async (id) => {
await api.deleteOrder(id)
Message.success('订单已删除')
await loadOrders()
}
const openAddress = (order) => {
addressForm.id = order.id
addressForm.address = order.address || ''
addressModalVisible.value = true
}
const submitAddress = async () => {
await api.updateOrderAddress(addressForm.id, { address: addressForm.address })
Message.success('地址已更新')
addressModalVisible.value = false
await loadOrders()
}
const openLogistics = async (orderId) => {
logistics.value = await api.orderLogistics(orderId)
logisticsModalVisible.value = true
}
const openReview = async (orderId) => {
orderItems.value = await api.orderItems(orderId)
if (orderItems.value.length > 0) {
reviewForm.productId = orderItems.value[0].productId
}
reviewForm.orderId = orderId
reviewForm.rating = 5
reviewForm.content = ''
reviewModalVisible.value = true
}
const submitReview = async () => {
if (!reviewForm.productId) return Message.warning('请选择商品')
await api.addReview({ orderId: reviewForm.orderId, productId: reviewForm.productId, rating: reviewForm.rating, content: reviewForm.content })
Message.success('评价已提交')
reviewModalVisible.value = false
}
onMounted(async () => {
try {
await userStore.fetchMe()
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
await loadOrders()
} catch {
router.replace('/login')
}
})
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="container">
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
<h2>萌贝母婴商城 - 个人信息</h2>
<a-space>
<a-button @click="goProducts">继续购物</a-button>
<a-button @click="goOrders">我的订单</a-button>
</a-space>
</a-space>
<a-card title="个人资料">
<a-form :model="form" layout="vertical" style="max-width: 480px">
<a-form-item label="账号"><a-input v-model="form.username" disabled /></a-form-item>
<a-form-item label="角色"><a-input v-model="form.role" disabled /></a-form-item>
<a-form-item label="昵称"><a-input v-model="form.nickname" /></a-form-item>
<a-form-item label="手机号"><a-input v-model="form.phone" /></a-form-item>
<a-form-item label="收货地址"><a-input v-model="form.address" /></a-form-item>
<a-button type="primary" @click="save">保存修改</a-button>
</a-form>
</a-card>
<a-card title="商家入驻申请" style="margin-top: 12px">
<a-space direction="vertical" style="width: 100%">
<a-textarea v-model="qualification" :rows="3" placeholder="请填写商家资质、经营能力说明" />
<a-button type="primary" @click="submitMerchantApplication">提交入驻申请</a-button>
</a-space>
</a-card>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { api } from '../api'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const form = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
const qualification = ref('')
const goProducts = () => router.push('/products')
const goOrders = () => router.push('/orders')
const load = async () => {
const me = await api.me()
form.username = me.username
form.role = me.role
form.nickname = me.nickname || ''
form.phone = me.phone || ''
form.address = me.address || ''
}
const save = async () => {
await api.updateMe({ nickname: form.nickname, phone: form.phone, address: form.address })
Message.success('已保存')
}
const submitMerchantApplication = async () => {
await api.applyMerchant({ qualification: qualification.value })
qualification.value = ''
Message.success('入驻申请已提交,等待管理员审核')
}
onMounted(async () => {
try {
await userStore.fetchMe()
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
await load()
} catch {
router.replace('/login')
}
})
</script>