add
This commit is contained in:
20
frontend/package.json
Normal file
20
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
13
frontend/public/index.html
Normal file
13
frontend/public/index.html
Normal 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
19
frontend/src/App.vue
Normal 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>
|
||||
6
frontend/src/api/address.js
Normal file
6
frontend/src/api/address.js
Normal 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
21
frontend/src/api/admin.js
Normal 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
6
frontend/src/api/auth.js
Normal 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');
|
||||
7
frontend/src/api/confession.js
Normal file
7
frontend/src/api/confession.js
Normal 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
16
frontend/src/api/http.js
Normal 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;
|
||||
7
frontend/src/api/order.js
Normal file
7
frontend/src/api/order.js
Normal 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`);
|
||||
5
frontend/src/api/product.js
Normal file
5
frontend/src/api/product.js
Normal 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
13
frontend/src/main.js
Normal 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');
|
||||
44
frontend/src/router/index.js
Normal file
44
frontend/src/router/index.js
Normal 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;
|
||||
78
frontend/src/views/ConfessionCreate.vue
Normal file
78
frontend/src/views/ConfessionCreate.vue
Normal 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>
|
||||
120
frontend/src/views/GiftPage.vue
Normal file
120
frontend/src/views/GiftPage.vue
Normal 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
129
frontend/src/views/Home.vue
Normal 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>
|
||||
52
frontend/src/views/Login.vue
Normal file
52
frontend/src/views/Login.vue
Normal 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>
|
||||
67
frontend/src/views/Orders.vue
Normal file
67
frontend/src/views/Orders.vue
Normal 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>
|
||||
114
frontend/src/views/ProductDetail.vue
Normal file
114
frontend/src/views/ProductDetail.vue
Normal 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>
|
||||
141
frontend/src/views/Profile.vue
Normal file
141
frontend/src/views/Profile.vue
Normal 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>
|
||||
55
frontend/src/views/Register.vue
Normal file
55
frontend/src/views/Register.vue
Normal 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>
|
||||
47
frontend/src/views/admin/AdminDashboard.vue
Normal file
47
frontend/src/views/admin/AdminDashboard.vue
Normal 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>
|
||||
39
frontend/src/views/admin/AdminLayout.vue
Normal file
39
frontend/src/views/admin/AdminLayout.vue
Normal 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>
|
||||
47
frontend/src/views/admin/AdminOrders.vue
Normal file
47
frontend/src/views/admin/AdminOrders.vue
Normal 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>
|
||||
92
frontend/src/views/admin/AdminProducts.vue
Normal file
92
frontend/src/views/admin/AdminProducts.vue
Normal 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>
|
||||
47
frontend/src/views/admin/AdminReviews.vue
Normal file
47
frontend/src/views/admin/AdminReviews.vue
Normal 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>
|
||||
49
frontend/src/views/admin/AdminUsers.vue
Normal file
49
frontend/src/views/admin/AdminUsers.vue
Normal 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
10
frontend/vue.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user