Initial commit
This commit is contained in:
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>萌贝母婴商城</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1684
frontend/package-lock.json
generated
Normal file
1684
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "maternal-mall-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
"axios": "^1.7.9",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
3
frontend/src/App.vue
Normal file
3
frontend/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
24
frontend/src/api/http.js
Normal file
24
frontend/src/api/http.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: 'http://localhost:8080',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers['X-Token'] = token
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.response.use((resp) => {
|
||||
const data = resp.data
|
||||
if (data.code !== 0) {
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
return data.data
|
||||
})
|
||||
|
||||
export default http
|
||||
65
frontend/src/api/index.js
Normal file
65
frontend/src/api/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import http from './http'
|
||||
|
||||
export const api = {
|
||||
register: (payload) => http.post('/api/auth/register', payload),
|
||||
login: (payload) => http.post('/api/auth/login', payload),
|
||||
me: () => http.get('/api/auth/me'),
|
||||
updateMe: (payload) => http.put('/api/auth/me', payload),
|
||||
|
||||
banners: () => http.get('/api/public/banners'),
|
||||
products: (keyword = '') => http.get('/api/public/products', { params: { keyword } }),
|
||||
|
||||
customerCart: () => http.get('/api/customer/cart'),
|
||||
customerCartViews: () => http.get('/api/customer/cart/views'),
|
||||
addCart: (payload) => http.post('/api/customer/cart', payload),
|
||||
delCart: (productId) => http.delete(`/api/customer/cart/${productId}`),
|
||||
checkout: (payload) => http.post('/api/customer/orders/checkout', payload),
|
||||
customerBuyNow: (payload) => http.post('/api/customer/orders/buy-now', payload),
|
||||
customerOrders: () => http.get('/api/customer/orders'),
|
||||
refundOrder: (id, payload) => http.put(`/api/customer/orders/${id}/refund`, payload),
|
||||
updateOrderAddress: (id, payload) => http.put(`/api/customer/orders/${id}/address`, payload),
|
||||
deleteOrder: (id) => http.delete(`/api/customer/orders/${id}`),
|
||||
orderLogistics: (id) => http.get(`/api/customer/orders/${id}/logistics`),
|
||||
customerFavorites: () => http.get('/api/customer/favorites'),
|
||||
customerFavoriteViews: () => http.get('/api/customer/favorites/views'),
|
||||
addFavorite: (payload) => http.post('/api/customer/favorites', payload),
|
||||
deleteFavorite: (productId) => http.delete(`/api/customer/favorites/${productId}`),
|
||||
addReview: (payload) => http.post('/api/customer/reviews', payload),
|
||||
orderItems: (orderId) => http.get(`/api/customer/orders/${orderId}/items`),
|
||||
applyMerchant: (payload) => http.post('/api/customer/merchant-applications', payload),
|
||||
|
||||
merchantOverview: () => http.get('/api/merchant/overview'),
|
||||
merchantProducts: () => http.get('/api/merchant/products'),
|
||||
saveMerchantProduct: (payload) => http.post('/api/merchant/products', payload),
|
||||
deleteMerchantProduct: (id) => http.delete(`/api/merchant/products/${id}`),
|
||||
merchantOrders: () => http.get('/api/merchant/orders'),
|
||||
shipOrder: (id, payload) => http.put(`/api/merchant/orders/${id}/ship`, payload),
|
||||
merchantRefund: (id, payload) => http.put(`/api/merchant/orders/${id}/refund`, payload),
|
||||
merchantReviews: () => http.get('/api/merchant/reviews'),
|
||||
merchantLogistics: () => http.get('/api/merchant/logistics'),
|
||||
merchantInventory: () => http.get('/api/merchant/inventory'),
|
||||
deleteMerchantInventory: (id) => http.delete(`/api/merchant/inventory/${id}`),
|
||||
|
||||
adminOverview: () => http.get('/api/admin/overview'),
|
||||
adminUsers: () => http.get('/api/admin/users'),
|
||||
adminSaveUser: (payload) => http.post('/api/admin/users', payload),
|
||||
adminDeleteUser: (id) => http.delete(`/api/admin/users/${id}`),
|
||||
adminOrders: () => http.get('/api/admin/orders'),
|
||||
adminUpdateOrder: (id, payload) => http.put(`/api/admin/orders/${id}`, payload),
|
||||
adminOrderRisks: () => http.get('/api/admin/orders/risk'),
|
||||
adminAuditRefund: (id, payload) => http.put(`/api/admin/orders/${id}/refund-audit`, payload),
|
||||
adminAuditShipment: (id, payload) => http.put(`/api/admin/orders/${id}/ship-audit`, payload),
|
||||
adminMerchantApplications: () => http.get('/api/admin/merchant-applications'),
|
||||
adminAuditMerchantApplication: (id, payload) => http.put(`/api/admin/merchant-applications/${id}`, payload),
|
||||
adminBanners: () => http.get('/api/admin/banners'),
|
||||
adminSaveBanner: (payload) => http.post('/api/admin/banners', payload),
|
||||
adminDeleteBanner: (id) => http.delete(`/api/admin/banners/${id}`),
|
||||
adminProducts: () => http.get('/api/admin/products'),
|
||||
adminProductViews: () => http.get('/api/admin/products/views'),
|
||||
adminSaveProduct: (payload) => http.post('/api/admin/products', payload),
|
||||
adminApproveProduct: (id, payload) => http.put(`/api/admin/products/${id}/approve`, payload),
|
||||
adminDeleteProduct: (id) => http.delete(`/api/admin/products/${id}`),
|
||||
adminReviews: () => http.get('/api/admin/reviews'),
|
||||
adminLogistics: () => http.get('/api/admin/logistics'),
|
||||
adminInventory: () => http.get('/api/admin/inventory')
|
||||
}
|
||||
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
import '@arco-design/web-vue/dist/arco.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(ArcoVue).mount('#app')
|
||||
29
frontend/src/router/index.js
Normal file
29
frontend/src/router/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import CartView from '../views/CartView.vue'
|
||||
import OrdersView from '../views/OrdersView.vue'
|
||||
import FavoritesView from '../views/FavoritesView.vue'
|
||||
import ProfileView from '../views/ProfileView.vue'
|
||||
import AdminView from '../views/AdminView.vue'
|
||||
import MerchantView from '../views/MerchantView.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/products' },
|
||||
{ path: '/products', component: HomeView },
|
||||
{ path: '/cart', component: CartView },
|
||||
{ path: '/orders', component: OrdersView },
|
||||
{ path: '/favorites', component: FavoritesView },
|
||||
{ path: '/profile', component: ProfileView },
|
||||
{ path: '/login', component: LoginView },
|
||||
{ path: '/admin', component: AdminView },
|
||||
{ path: '/merchant', component: MerchantView },
|
||||
{ path: '/customer', redirect: '/products' }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
21
frontend/src/stores/user.js
Normal file
21
frontend/src/stores/user.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '../api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
profile: null
|
||||
}),
|
||||
getters: {
|
||||
role: (s) => s.profile?.role,
|
||||
loggedIn: (s) => !!s.profile
|
||||
},
|
||||
actions: {
|
||||
async fetchMe() {
|
||||
this.profile = await api.me()
|
||||
},
|
||||
async logout() {
|
||||
localStorage.removeItem('token')
|
||||
this.profile = null
|
||||
}
|
||||
}
|
||||
})
|
||||
13
frontend/src/style.css
Normal file
13
frontend/src/style.css
Normal file
@@ -0,0 +1,13 @@
|
||||
:root {
|
||||
--bg-main: linear-gradient(135deg, #fff8ed 0%, #f2f7ff 100%);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
background: var(--bg-main);
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
644
frontend/src/views/AdminView.vue
Normal file
644
frontend/src/views/AdminView.vue
Normal 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>
|
||||
86
frontend/src/views/CartView.vue
Normal file
86
frontend/src/views/CartView.vue
Normal 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>
|
||||
173
frontend/src/views/CustomerView.vue
Normal file
173
frontend/src/views/CustomerView.vue
Normal 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>
|
||||
86
frontend/src/views/FavoritesView.vue
Normal file
86
frontend/src/views/FavoritesView.vue
Normal 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>
|
||||
132
frontend/src/views/HomeView.vue
Normal file
132
frontend/src/views/HomeView.vue
Normal 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>
|
||||
57
frontend/src/views/LoginView.vue
Normal file
57
frontend/src/views/LoginView.vue
Normal 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>
|
||||
355
frontend/src/views/MerchantView.vue
Normal file
355
frontend/src/views/MerchantView.vue
Normal 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>
|
||||
154
frontend/src/views/OrdersView.vue
Normal file
154
frontend/src/views/OrdersView.vue
Normal 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>
|
||||
75
frontend/src/views/ProfileView.vue
Normal file
75
frontend/src/views/ProfileView.vue
Normal 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>
|
||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user