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

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>