This commit is contained in:
王子琦
2026-01-13 13:55:40 +08:00
parent 6affd0c77e
commit f58e05d962
72 changed files with 3251 additions and 0 deletions

20
frontend/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "flower-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 8081",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^1.7.2",
"core-js": "^3.36.0",
"element-ui": "^2.15.14",
"vue": "^2.7.16",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-service": "^5.0.8"
}
}

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>植愈线上花店</title>
</head>
<body>
<noscript>需要启用 JavaScript 才能继续。</noscript>
<div id="app"></div>
</body>
</html>

19
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,19 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
};
</script>
<style>
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
background: #f7f7f7;
}
</style>

View File

@@ -0,0 +1,6 @@
import http from './http';
export const listAddresses = () => http.get('/addresses');
export const createAddress = (data) => http.post('/addresses', data);
export const updateAddress = (id, data) => http.put(`/addresses/${id}`, data);
export const deleteAddress = (id) => http.delete(`/addresses/${id}`);

21
frontend/src/api/admin.js Normal file
View File

@@ -0,0 +1,21 @@
import http from './http';
export const dashboard = () => http.get('/admin/dashboard');
export const adminProducts = {
list: () => http.get('/products/admin/all'),
create: (data) => http.post('/products', data),
update: (id, data) => http.put(`/products/${id}`, data),
remove: (id) => http.delete(`/products/${id}`)
};
export const adminOrders = {
list: () => http.get('/orders/admin/all'),
updateStatus: (id, data) => http.put(`/orders/admin/${id}/status`, data)
};
export const adminUsers = {
list: () => http.get('/users'),
updateStatus: (id, data) => http.put(`/users/${id}/status`, data)
};
export const adminReviews = {
list: () => http.get('/reviews/admin/all'),
updateStatus: (id, data) => http.put(`/reviews/admin/${id}/status`, data)
};

6
frontend/src/api/auth.js Normal file
View File

@@ -0,0 +1,6 @@
import http from './http';
export const login = (data) => http.post('/auth/login', data);
export const register = (data) => http.post('/auth/register', data);
export const me = () => http.get('/auth/me');
export const logout = () => http.post('/auth/logout');

View File

@@ -0,0 +1,7 @@
import http from './http';
export const createConfession = (data) => http.post('/confessions', data);
export const getConfession = (code) => http.get(`/confessions/${code}`);
export const listBarrages = (code) => http.get(`/confessions/${code}/barrages`);
export const sendBarrage = (code, data) => http.post(`/confessions/${code}/barrages`, data);
export const getQrUrl = (code) => `/api/confessions/${code}/qr`;

16
frontend/src/api/http.js Normal file
View File

@@ -0,0 +1,16 @@
import axios from 'axios';
const http = axios.create({
baseURL: '/api',
timeout: 10000
});
http.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default http;

View File

@@ -0,0 +1,7 @@
import http from './http';
export const createOrder = (data) => http.post('/orders', data);
export const listOrders = () => http.get('/orders');
export const getOrder = (id) => http.get(`/orders/${id}`);
export const payOrder = (id) => http.post(`/orders/${id}/pay`);
export const cancelOrder = (id) => http.put(`/orders/${id}/cancel`);

View File

@@ -0,0 +1,5 @@
import http from './http';
export const listProducts = (params) => http.get('/products', { params });
export const getProduct = (id) => http.get(`/products/${id}`);
export const listCategories = () => http.get('/categories');

13
frontend/src/main.js Normal file
View File

@@ -0,0 +1,13 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
Vue.use(ElementUI);
new Vue({
router,
render: (h) => h(App)
}).$mount('#app');

View File

@@ -0,0 +1,44 @@
import Vue from 'vue';
import Router from 'vue-router';
import Home from '../views/Home.vue';
import ProductDetail from '../views/ProductDetail.vue';
import Login from '../views/Login.vue';
import Register from '../views/Register.vue';
import Orders from '../views/Orders.vue';
import Profile from '../views/Profile.vue';
import ConfessionCreate from '../views/ConfessionCreate.vue';
import GiftPage from '../views/GiftPage.vue';
import AdminLayout from '../views/admin/AdminLayout.vue';
import AdminDashboard from '../views/admin/AdminDashboard.vue';
import AdminProducts from '../views/admin/AdminProducts.vue';
import AdminOrders from '../views/admin/AdminOrders.vue';
import AdminUsers from '../views/admin/AdminUsers.vue';
import AdminReviews from '../views/admin/AdminReviews.vue';
Vue.use(Router);
const router = new Router({
routes: [
{ path: '/', component: Home },
{ path: '/product/:id', component: ProductDetail },
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/orders', component: Orders },
{ path: '/profile', component: Profile },
{ path: '/confession/create/:orderId', component: ConfessionCreate },
{ path: '/gift/:code', component: GiftPage },
{
path: '/admin',
component: AdminLayout,
children: [
{ path: '', component: AdminDashboard },
{ path: 'products', component: AdminProducts },
{ path: 'orders', component: AdminOrders },
{ path: 'users', component: AdminUsers },
{ path: 'reviews', component: AdminReviews }
]
}
]
});
export default router;

View File

@@ -0,0 +1,78 @@
<template>
<div class="page">
<el-card>
<el-button type="text" @click="$router.push('/orders')">返回订单</el-button>
</el-card>
<el-card class="content">
<h3>定制告白弹幕</h3>
<el-form :model="form">
<el-form-item label="标题">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="祝福文字">
<el-input type="textarea" v-model="form.message" />
</el-form-item>
<el-form-item label="配图链接">
<el-input v-model="form.imageUrl" placeholder="https://..." />
</el-form-item>
</el-form>
<el-button type="primary" @click="submit">生成页面</el-button>
<div v-if="confession" class="result">
<p>专属链接{{ giftUrl }}</p>
<img :src="qrUrl" class="qr" />
</div>
</el-card>
</div>
</template>
<script>
import { createConfession, getQrUrl } from '../api/confession';
export default {
data() {
return {
form: {
title: '',
message: '',
imageUrl: ''
},
confession: null
};
},
computed: {
giftUrl() {
return this.confession ? `${window.location.origin}/#/gift/${this.confession.code}` : '';
},
qrUrl() {
return this.confession ? getQrUrl(this.confession.code) : '';
}
},
methods: {
submit() {
createConfession({
orderId: this.$route.params.orderId,
...this.form
}).then((res) => {
this.confession = res.data.data;
this.$message.success('生成成功');
});
}
}
};
</script>
<style scoped>
.page {
padding: 20px;
}
.content {
margin-top: 12px;
}
.result {
margin-top: 16px;
}
.qr {
width: 200px;
height: 200px;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="page">
<el-card>
<h2>{{ confession.title || '告白弹幕' }}</h2>
<p>{{ confession.message }}</p>
<img v-if="confession.imageUrl" :src="confession.imageUrl" class="image" />
</el-card>
<el-card class="content">
<div class="barrage-wall">
<span
v-for="item in barrages"
:key="item.id"
class="barrage-item"
:style="{ top: item.top + 'px', animationDuration: item.duration + 's' }"
>
{{ item.sender || '匿名' }}{{ item.content }}
</span>
</div>
<div class="send">
<el-input v-model="form.sender" placeholder="你的名字" class="input" />
<el-input v-model="form.content" placeholder="送上一句祝福" class="input" />
<el-button type="primary" @click="send">发送</el-button>
</div>
</el-card>
</div>
</template>
<script>
import { getConfession, listBarrages, sendBarrage } from '../api/confession';
export default {
data() {
return {
confession: {},
barrages: [],
form: { sender: '', content: '' },
timer: null
};
},
created() {
this.loadAll();
this.timer = setInterval(this.loadBarrages, 4000);
},
beforeDestroy() {
clearInterval(this.timer);
},
methods: {
loadAll() {
const code = this.$route.params.code;
getConfession(code).then((res) => {
this.confession = res.data.data || {};
});
this.loadBarrages();
},
loadBarrages() {
const code = this.$route.params.code;
listBarrages(code).then((res) => {
const list = res.data.data || [];
this.barrages = list.map((item) => ({
...item,
top: Math.floor(Math.random() * 220),
duration: 8 + Math.random() * 4
}));
});
},
send() {
if (!this.form.content) {
this.$message.warning('请输入弹幕内容');
return;
}
sendBarrage(this.$route.params.code, this.form).then(() => {
this.$message.success('发送成功');
this.form.content = '';
this.loadBarrages();
});
}
}
};
</script>
<style scoped>
.page {
padding: 20px;
}
.content {
margin-top: 12px;
}
.image {
max-width: 100%;
margin-top: 12px;
border-radius: 6px;
}
.barrage-wall {
position: relative;
height: 260px;
overflow: hidden;
background: #fff7f7;
border-radius: 8px;
margin-bottom: 12px;
}
.barrage-item {
position: absolute;
white-space: nowrap;
left: 100%;
animation-name: fly;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.send {
display: flex;
align-items: center;
}
.input {
margin-right: 10px;
}
@keyframes fly {
from { left: 100%; }
to { left: -100%; }
}
</style>

129
frontend/src/views/Home.vue Normal file
View File

@@ -0,0 +1,129 @@
<template>
<div class="page">
<el-container>
<el-header class="header">
<div class="logo">植愈线上花店</div>
<div>
<el-button type="text" @click="$router.push('/orders')">我的订单</el-button>
<el-button type="text" @click="$router.push('/profile')">个人中心</el-button>
<el-button type="text" @click="$router.push('/admin')">后台管理</el-button>
<el-button type="primary" @click="$router.push('/login')">登录</el-button>
</div>
</el-header>
<el-main>
<el-card>
<div class="filters">
<el-select v-model="categoryId" clearable placeholder="全部分类" @change="loadProducts">
<el-option
v-for="item in categories"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-input v-model="keyword" placeholder="搜索花束" class="search" @keyup.enter.native="filterProducts" />
<el-button type="primary" @click="filterProducts">搜索</el-button>
</div>
</el-card>
<el-row :gutter="16" class="product-list">
<el-col v-for="item in filteredProducts" :key="item.id" :span="6">
<el-card :body-style="{ padding: '12px' }" class="product-card" @click.native="goDetail(item.id)">
<img :src="item.coverUrl || placeholder" class="product-cover" />
<div class="product-name">{{ item.name }}</div>
<div class="product-price">{{ item.price }}</div>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
</template>
<script>
import { listProducts, listCategories } from '../api/product';
export default {
data() {
return {
products: [],
filteredProducts: [],
categories: [],
categoryId: null,
keyword: '',
placeholder: 'https://via.placeholder.com/300x200?text=Flower'
};
},
created() {
this.loadCategories();
this.loadProducts();
},
methods: {
loadCategories() {
listCategories().then((res) => {
this.categories = res.data.data || [];
});
},
loadProducts() {
listProducts({ categoryId: this.categoryId }).then((res) => {
this.products = res.data.data || [];
this.filteredProducts = this.products;
});
},
filterProducts() {
const key = this.keyword.trim();
if (!key) {
this.filteredProducts = this.products;
return;
}
this.filteredProducts = this.products.filter((item) => item.name.includes(key));
},
goDetail(id) {
this.$router.push(`/product/${id}`);
}
}
};
</script>
<style scoped>
.page {
min-height: 100vh;
}
.header {
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-size: 20px;
font-weight: bold;
}
.filters {
display: flex;
align-items: center;
}
.search {
margin: 0 12px;
width: 240px;
}
.product-list {
margin-top: 16px;
}
.product-card {
cursor: pointer;
}
.product-cover {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 4px;
}
.product-name {
margin-top: 8px;
font-weight: 600;
}
.product-price {
color: #f56c6c;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="auth-page">
<el-card class="card">
<h3>登录</h3>
<el-form :model="form">
<el-form-item label="账号">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-button type="primary" @click="submit">登录</el-button>
<el-button type="text" @click="$router.push('/register')">没有账号注册</el-button>
</el-form>
</el-card>
</div>
</template>
<script>
import { login } from '../api/auth';
export default {
data() {
return {
form: { username: '', password: '' }
};
},
methods: {
submit() {
login(this.form).then((res) => {
const data = res.data.data;
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
this.$message.success('登录成功');
this.$router.push('/');
});
}
}
};
</script>
<style scoped>
.auth-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
width: 400px;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="page">
<el-card>
<el-button type="text" @click="$router.push('/')">返回首页</el-button>
</el-card>
<el-card class="content">
<el-table :data="orders">
<el-table-column prop="order.orderNo" label="订单号" />
<el-table-column prop="order.status" label="状态" />
<el-table-column prop="order.totalAmount" label="金额" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button v-if="scope.row.order.status === 'CREATED'" type="text" @click="pay(scope.row.order.id)">支付</el-button>
<el-button v-if="scope.row.order.status === 'CREATED'" type="text" @click="cancel(scope.row.order.id)">取消</el-button>
<el-button type="text" @click="goConfession(scope.row.order.id)">告白弹幕</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { listOrders, payOrder, cancelOrder } from '../api/order';
export default {
data() {
return {
orders: []
};
},
created() {
this.loadOrders();
},
methods: {
loadOrders() {
listOrders().then((res) => {
this.orders = res.data.data || [];
});
},
pay(id) {
payOrder(id).then(() => {
this.$message.success('支付成功');
this.loadOrders();
});
},
cancel(id) {
cancelOrder(id).then(() => {
this.$message.success('已取消');
this.loadOrders();
});
},
goConfession(orderId) {
this.$router.push(`/confession/create/${orderId}`);
}
}
};
</script>
<style scoped>
.page {
padding: 20px;
}
.content {
margin-top: 12px;
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div class="page">
<el-card>
<el-button type="text" @click="$router.push('/')">返回首页</el-button>
</el-card>
<el-card class="content">
<el-row :gutter="20">
<el-col :span="10">
<img :src="product.coverUrl || placeholder" class="cover" />
</el-col>
<el-col :span="14">
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<div class="price">{{ product.price }}</div>
<el-input-number v-model="quantity" :min="1" :max="product.stock || 99" />
<div class="section">
<el-select v-model="addressId" placeholder="选择收货地址">
<el-option
v-for="addr in addresses"
:key="addr.id"
:label="formatAddress(addr)"
:value="addr.id"
/>
</el-select>
<el-button type="primary" @click="submitOrder">立即购买</el-button>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
import { getProduct } from '../api/product';
import { listAddresses } from '../api/address';
import { createOrder } from '../api/order';
export default {
data() {
return {
product: {},
quantity: 1,
addresses: [],
addressId: null,
placeholder: 'https://via.placeholder.com/400x300?text=Flower'
};
},
created() {
this.loadDetail();
this.loadAddresses();
},
methods: {
loadDetail() {
getProduct(this.$route.params.id).then((res) => {
this.product = res.data.data || {};
});
},
loadAddresses() {
listAddresses().then((res) => {
this.addresses = res.data.data || [];
const def = this.addresses.find((item) => item.isDefault);
if (def) {
this.addressId = def.id;
}
});
},
formatAddress(addr) {
return `${addr.recipientName} ${addr.phone} ${addr.province || ''}${addr.city || ''}${addr.district || ''}${addr.detail || ''}`;
},
submitOrder() {
if (!this.addressId) {
this.$message.warning('请选择收货地址');
return;
}
createOrder({
addressId: this.addressId,
items: [{ productId: this.product.id, quantity: this.quantity }]
}).then((res) => {
this.$message.success('下单成功');
const order = res.data.data.order;
this.$router.push(`/orders?highlight=${order.id}`);
});
}
}
};
</script>
<style scoped>
.page {
padding: 20px;
}
.content {
margin-top: 16px;
}
.cover {
width: 100%;
border-radius: 8px;
object-fit: cover;
}
.price {
color: #f56c6c;
font-size: 20px;
margin: 12px 0;
}
.section {
margin-top: 20px;
display: flex;
align-items: center;
}
.section .el-select {
margin-right: 12px;
width: 300px;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="page">
<el-card>
<el-button type="text" @click="$router.push('/')">返回首页</el-button>
</el-card>
<el-card class="content">
<h3>个人信息</h3>
<el-form :model="profile">
<el-form-item label="昵称">
<el-input v-model="profile.nickname" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="profile.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="profile.email" />
</el-form-item>
</el-form>
<el-button type="primary" @click="saveProfile">保存</el-button>
</el-card>
<el-card class="content">
<div class="header-row">
<h3>收货地址</h3>
<el-button type="primary" @click="showDialog = true">新增地址</el-button>
</div>
<el-table :data="addresses">
<el-table-column prop="recipientName" label="收货人" />
<el-table-column prop="phone" label="电话" />
<el-table-column prop="detail" label="详细地址" />
<el-table-column label="默认">
<template slot-scope="scope">
<el-tag v-if="scope.row.isDefault" type="success">默认</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text" @click="editAddress(scope.row)">编辑</el-button>
<el-button type="text" @click="removeAddress(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="地址" :visible.sync="showDialog">
<el-form :model="addressForm">
<el-form-item label="收货人">
<el-input v-model="addressForm.recipientName" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="addressForm.phone" />
</el-form-item>
<el-form-item label="省市区">
<el-input v-model="addressForm.province" placeholder="省" />
<el-input v-model="addressForm.city" placeholder="市" />
<el-input v-model="addressForm.district" placeholder="区" />
</el-form-item>
<el-form-item label="详细地址">
<el-input v-model="addressForm.detail" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="addressForm.isDefault">设为默认</el-checkbox>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="saveAddress">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { me } from '../api/auth';
import { listAddresses, createAddress, updateAddress, deleteAddress } from '../api/address';
import http from '../api/http';
export default {
data() {
return {
profile: { nickname: '', phone: '', email: '' },
addresses: [],
showDialog: false,
addressForm: {}
};
},
created() {
this.loadProfile();
this.loadAddresses();
},
methods: {
loadProfile() {
me().then((res) => {
this.profile = res.data.data || {};
});
},
saveProfile() {
http.put('/users/me', this.profile).then(() => {
this.$message.success('保存成功');
});
},
loadAddresses() {
listAddresses().then((res) => {
this.addresses = res.data.data || [];
});
},
editAddress(row) {
this.addressForm = { ...row };
this.showDialog = true;
},
removeAddress(id) {
deleteAddress(id).then(() => {
this.$message.success('删除成功');
this.loadAddresses();
});
},
saveAddress() {
const api = this.addressForm.id ? updateAddress(this.addressForm.id, this.addressForm) : createAddress(this.addressForm);
api.then(() => {
this.$message.success('保存成功');
this.showDialog = false;
this.addressForm = {};
this.loadAddresses();
});
}
}
};
</script>
<style scoped>
.page {
padding: 20px;
}
.content {
margin-top: 12px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="auth-page">
<el-card class="card">
<h3>注册</h3>
<el-form :model="form">
<el-form-item label="账号">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="form.nickname" />
</el-form-item>
<el-button type="primary" @click="submit">注册</el-button>
<el-button type="text" @click="$router.push('/login')">已有账号登录</el-button>
</el-form>
</el-card>
</div>
</template>
<script>
import { register } from '../api/auth';
export default {
data() {
return {
form: { username: '', password: '', nickname: '' }
};
},
methods: {
submit() {
register(this.form).then((res) => {
const data = res.data.data;
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
this.$message.success('注册成功');
this.$router.push('/');
});
}
}
};
</script>
<style scoped>
.auth-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
width: 400px;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div>
<el-row :gutter="16">
<el-col :span="8">
<el-card>
<div>订单总数</div>
<div class="value">{{ stats.totalOrders }}</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<div>用户总数</div>
<div class="value">{{ stats.totalUsers }}</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<div>销售额</div>
<div class="value">{{ stats.totalSales }}</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { dashboard } from '../../api/admin';
export default {
data() {
return { stats: { totalOrders: 0, totalUsers: 0, totalSales: 0 } };
},
created() {
dashboard().then((res) => {
this.stats = res.data.data || this.stats;
});
}
};
</script>
<style scoped>
.value {
font-size: 22px;
margin-top: 6px;
color: #409eff;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<el-container class="layout">
<el-aside width="200px" class="aside">
<div class="title">后台管理</div>
<el-menu router>
<el-menu-item index="/admin">仪表盘</el-menu-item>
<el-menu-item index="/admin/products">商品管理</el-menu-item>
<el-menu-item index="/admin/orders">订单管理</el-menu-item>
<el-menu-item index="/admin/users">用户管理</el-menu-item>
<el-menu-item index="/admin/reviews">评价管理</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<el-button type="text" @click="$router.push('/')">返回前台</el-button>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.layout {
min-height: 100vh;
}
.aside {
background: #2d3a4b;
color: #fff;
}
.title {
padding: 20px;
font-size: 18px;
}
.header {
background: #fff;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div>
<el-table :data="orders">
<el-table-column prop="order.orderNo" label="订单号" />
<el-table-column prop="order.status" label="状态" />
<el-table-column prop="order.totalAmount" label="金额" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-select v-model="scope.row.order.status" placeholder="状态">
<el-option label="已创建" value="CREATED" />
<el-option label="已支付" value="PAID" />
<el-option label="已发货" value="SHIPPED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELED" />
</el-select>
<el-button type="text" @click="update(scope.row.order)">保存</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { adminOrders } from '../../api/admin';
export default {
data() {
return { orders: [] };
},
created() {
this.load();
},
methods: {
load() {
adminOrders.list().then((res) => {
this.orders = res.data.data || [];
});
},
update(order) {
adminOrders.updateStatus(order.id, { status: order.status }).then(() => {
this.$message.success('更新成功');
this.load();
});
}
}
};
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div>
<el-button type="primary" @click="openDialog()">新增商品</el-button>
<el-table :data="products" style="margin-top: 12px;">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="商品名" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="stock" label="库存" />
<el-table-column prop="status" label="状态" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text" @click="openDialog(scope.row)">编辑</el-button>
<el-button type="text" @click="remove(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="商品" :visible.sync="showDialog">
<el-form :model="form">
<el-form-item label="名称">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="价格">
<el-input v-model="form.price" />
</el-form-item>
<el-form-item label="库存">
<el-input v-model="form.stock" />
</el-form-item>
<el-form-item label="封面">
<el-input v-model="form.coverUrl" />
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="form.description" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option label="上架" value="ON" />
<el-option label="下架" value="OFF" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { adminProducts } from '../../api/admin';
export default {
data() {
return {
products: [],
showDialog: false,
form: {}
};
},
created() {
this.load();
},
methods: {
load() {
adminProducts.list().then((res) => {
this.products = res.data.data || [];
});
},
openDialog(row) {
this.form = row ? { ...row } : { status: 'ON', stock: 0 };
this.showDialog = true;
},
save() {
const action = this.form.id
? adminProducts.update(this.form.id, this.form)
: adminProducts.create(this.form);
action.then(() => {
this.$message.success('保存成功');
this.showDialog = false;
this.load();
});
},
remove(id) {
adminProducts.remove(id).then(() => {
this.$message.success('删除成功');
this.load();
});
}
}
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div>
<el-table :data="reviews">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="productId" label="商品ID" />
<el-table-column prop="rating" label="评分" />
<el-table-column prop="content" label="内容" />
<el-table-column prop="status" label="状态" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-select v-model="scope.row.status">
<el-option label="待审核" value="PENDING" />
<el-option label="通过" value="APPROVED" />
<el-option label="拒绝" value="REJECTED" />
</el-select>
<el-button type="text" @click="update(scope.row)">保存</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { adminReviews } from '../../api/admin';
export default {
data() {
return { reviews: [] };
},
created() {
this.load();
},
methods: {
load() {
adminReviews.list().then((res) => {
this.reviews = res.data.data || [];
});
},
update(row) {
adminReviews.updateStatus(row.id, { status: row.status }).then(() => {
this.$message.success('更新成功');
this.load();
});
}
}
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div>
<el-table :data="users">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="账号" />
<el-table-column prop="nickname" label="昵称" />
<el-table-column prop="role" label="角色" />
<el-table-column label="状态">
<template slot-scope="scope">
<el-tag v-if="scope.row.disabled" type="danger">禁用</el-tag>
<el-tag v-else type="success">正常</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text" @click="toggle(scope.row)">
{{ scope.row.disabled ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { adminUsers } from '../../api/admin';
export default {
data() {
return { users: [] };
},
created() {
this.load();
},
methods: {
load() {
adminUsers.list().then((res) => {
this.users = res.data.data || [];
});
},
toggle(user) {
adminUsers.updateStatus(user.id, { disabled: !user.disabled }).then(() => {
this.$message.success('更新成功');
this.load();
});
}
}
};
</script>

10
frontend/vue.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
};