UML最终版

main
黄伊雯 7 months ago
parent a535347ba5
commit ef19f0f263

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,17 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
["@babel/preset-env", {
"modules": false
}]
],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk"
}
]
]
}

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,56 @@
{
"name": "bookstore",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"animate.css": "^4.1.1",
"bcryptjs": "^2.4.3",
"chart.js": "^4.4.7",
"core-js": "^3.8.3",
"cors": "^2.8.5",
"dayjs": "^1.11.6",
"element-ui": "^2.15.14",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.11.5",
"sequelize": "^6.37.5",
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vuex": "^3.6.2",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,39 @@
<template>
<div id="app">
<div class="router">
<router-view></router-view>
</div>
<!-- <Header v-show="!this.$route.meta.show"></Header>
<div class="router">
<router-view></router-view>
</div>
<Footer v-show="!this.$route.meta.show"></Footer> -->
</div>
</template>
<script>
// import Header from "./components/header";
// import Footer from "./components/footer";
export default {
name: "App",
// components: {
// Header,
// Footer,
// },
};
</script>
<style>
#app {
/* display: flex;
flex-direction: column; */
height: 100%;
margin: 0;
padding: 0;
min-width: 1366px;
}
.router {
height: 100%;
}
</style>

@ -0,0 +1,89 @@
// app.js
const express = require('express');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const sequelize = require('./config/database'); // 调整路径为相对路径
const { User, Book, CartItem } = require('./models/index');
const cartRoutes = require('./router/cart'); // 引入购物车路由模块
const app = express();
// 中间件
app.use(cors());
app.use(express.json());
app.use(cors({
origin: 'http://localhost:8081', // 替换为你的前端地址
credentials: true
}));
// 数据库同步
async function syncDatabase() {
try {
await sequelize.sync(); // 不使用 force 选项
console.log('Database & tables created!');
} catch (error) {
console.error('Error syncing database:', error);
process.exit(1); // 如果数据库同步失败,退出进程
}
}
syncDatabase();
// 设置关联关系(确保所有模型都被加载)
User.hasMany(CartItem, { foreignKey: 'user_id' });
CartItem.belongsTo(User, { foreignKey: 'user_id' });
Book.hasMany(CartItem, { foreignKey: 'book_id' });
CartItem.belongsTo(Book, { foreignKey: 'book_id' });
// 注册用户
app.post('/api/register', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '缺少用户名或密码' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({ username, password: hashedPassword });
res.status(201).json(user);
} catch (error) {
console.error('Error registering user:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 登录用户
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '缺少用户名或密码' });
}
const user = await User.findOne({ where: { username } });
if (!user) {
return res.status(400).json({ error: '用户不存在' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ error: '密码错误' });
}
const token = jwt.sign({ userId: user.id }, 'your_jwt_secret', { expiresIn: '24h' }); // 增加Token的有效期
// 返回用户的完整信息
res.json({ token, user: { id: user.id, username: user.username } });
} catch (error) {
console.error('Error logging in user:', error);
res.status(500).json({ error: '服务器错误' });
}
});
// 使用购物车路由模块
app.use('/api/cart', cartRoutes); // 注意这里使用了 '/api/cart' 前缀
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

@ -0,0 +1,79 @@
/* 清除内外边距 */
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote,
dl, dt, dd, ul, ol, li,
pre,
fieldset, lengend, button, input, textarea,
th, td {
margin: 0;
padding: 0;
}
html,body,#app {
height: 100%;
}
/* 设置默认字体 */
body,
button, input, select, textarea { /* for ie */
/*font: 12px/1 Tahoma, Helvetica, Arial, "宋体", sans-serif;*/
font: 12px/1.3 "Microsoft YaHei",Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif; /* 用 ascii 字符表示,使得在任何编码下都无问题 */
color: #333;
}
h1 { font-size: 18px; /* 18px / 12px = 1.5 */ }
h2 { font-size: 16px; }
h3 { font-size: 14px; }
h4, h5, h6 { font-size: 100%; }
address, cite, dfn, em, var, i{ font-style: normal; } /* 将斜体扶正 */
b, strong{ font-weight: normal; } /* 将粗体扶细 */
code, kbd, pre, samp, tt { font-family: "Courier New", Courier, monospace; } /* 统一等宽字体 */
small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
/* 重置列表元素 */
ul, ol { list-style: none; }
/* 重置文本格式元素 */
a { text-decoration: none; color: #666;}
/* 重置表单元素 */
legend { color: #000; } /* for ie6 */
fieldset, img { border: none; }
button, input, select, textarea {
font-size: 100%; /* 使得表单元素在 ie 下能继承字体大小 */
}
/* 重置表格元素 */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* 重置 hr */
hr {
border: none;
height: 1px;
}
.clearFix::after{
content:"";
display: block;
clear:both;
}
/* 让非ie浏览器默认也显示垂直滚动条防止因滚动条引起的闪烁 */
html { overflow-y: scroll; }
a:link:hover{
color : rgb(79, 76, 212) !important;
text-decoration: underline;
}
/* 清除浮动 */
.clearfix::after {
display: block;
height: 0;
content: "";
clear: both;
visibility: hidden;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -0,0 +1,49 @@
<template>
<div class="footer">
{{word}}
</div>
</template>
<script>
export default {
name: 'Footer',
data() {
return {
word: '',
wordList: [
'悟已往之不谏,知来者之可追。实迷途其未远,觉今是而昨非。',
'半山腰总是拥挤的,你要去山顶看看。',
'只有对自己不将就,自己才会变得更优秀。',
'拒绝摆烂,卷起来。',
'每一种热爱,都值得全力以赴。',
'人生如逆旅,我亦是行人。',
'生而无畏,人类的最高品质便是勇气。',
'少年心怀乌托邦,心仍向阳肆生长。',
'平静的大海培养不出优秀的水手。',
'长风破浪会有时,直挂云帆济沧海。',
],
}
},
methods: {
getWord() {
this.word = this.wordList[Math.floor(Math.random() * this.wordList.length)];
},
},
watch: {
$route() {
this.getWord();
}
},
}
</script>
<style scoped>
.footer {
text-align: center;
line-height: 50px;
font-size: 16px;
background-color: #1abc9c;
height: 50px;
color: #fff;
}
</style>

@ -0,0 +1,104 @@
<template>
<div>
<el-row class="row_box">
<el-col :span="15" :offset="2">
<div class="title" @click="toHome"></div>
</el-col>
<el-col :span="3">
<el-menu :default-active="activeIndex" mode="horizontal" @select="handleSelect"
background-color="#1abc9c" text-color="#fff" active-text-color="#ffd04b">
<el-menu-item index="home">书城</el-menu-item>
<el-menu-item index="shoppingCart">购物车</el-menu-item>
</el-menu>
</el-col>
<el-col :span="2">
<el-dropdown @command="handleDownBoxCommand">
<span class="el-dropdown-link">
{{username}}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
</el-row>
</div>
</template>
<script>
import {
mapState
} from "vuex";
export default {
name: 'Header',
data() {
return {
activeIndex: 'home'
};
},
methods: {
handleSelect(key, keyPath) {
this.$router.push(key)
},
handleDownBoxCommand(command) {
if (command === 'logout') {
this.logout();
}
},
logout() {
this.$message({
type:'success',
message:'退出成功'
})
this.$router.push('/welcome')
},
toHome() {
this.$router.push('/manager/home')
}
},
computed: {
...mapState({
username: (state) => state.username,
}),
},
watch: {
$route(to,from) {
if (to.name == 'Home') {
this.activeIndex = 'home'
} else if(to.name == 'ShoppingCart') {
this.activeIndex = 'shoppingCart'
} else {
this.activeIndex = ''
}
}
},
}
</script>
<style scoped>
.row_box {
background-color: #1abc9c;
}
.title {
letter-spacing: 2px;
font-size: 25px;
color: #F6F6F6;
float: left;
font-weight: 600;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 61px;
}
.el-dropdown-link {
cursor: pointer;
color: #fff;
font-size: 15px;
line-height: 61px;
}
.el-icon-arrow-down {
font-size: 12px;
}
</style>

@ -0,0 +1,22 @@
<template>
<div class="manage-layout">
<Header></Header>
<div class="router">
<router-view></router-view>
</div>
<Footer></Footer>
</div>
</template>
<script setup>
import Header from "./components/header/index.vue";
import Footer from "./components/footer/index.vue";
</script>
<style scoped>
.manage-layout {
display: flex;
flex-direction: column;
height: 100%;
}
</style>

@ -0,0 +1,23 @@
<!--src/components/Footer.vue-->
<template>
<footer class="footer">
<p>&copy;2024 商家模块</p>
</footer>
</template>
<script setup>
</script>
<style scoped>
.footer {
background-color: #1abc9c;
color: white;
text-align: center;
padding: 10px 0;
bottom: 0;
width: 100%;
position: fixed;
bottom: 0;
}
</style>

@ -0,0 +1,59 @@
<!--src/components/Header.vue-->
<template>
<header class="header">
<div class="logo">
<h1>商家管理系统</h1>
</div>
<nav>
<ul class="nav-links">
<li><router-link to="/seller/home">首页</router-link></li>
<li><router-link to="/seller/book-management">图书管理</router-link></li>
<li><router-link to="/seller/order-management">订单管理</router-link></li>
<li><router-link to="/seller/promotion-management">促销活动</router-link></li>
<li><router-link to="/seller/customer-feedback">客户反馈</router-link></li>
<li><router-link to="/seller/data-analytics">数据分析</router-link></li>
<li><router-link to="/seller/merchant-info">商家信息</router-link></li>
</ul>
</nav>
</header>
</template>
<script setup>
//
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #1abc9c;
color: white;
}
.logo h1 {
margin: 0;
}
.nav-links {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
}
.nav-links li {
margin: 0 10px;
}
.nav-links li a {
text-decoration: none;
color: white;
font-size: 16px;
}
.nav-links li a:hover {
color: #333;
}
</style>

@ -0,0 +1,14 @@
<template>
<div class="seller-layout">
<Header></Header>
<div class="router">
<router-view></router-view>
</div>
<Footer></Footer>
</div>
</template>
<script setup>
import Header from "./components/header/index.vue";
import Footer from "./components/footer/index.vue";
</script>

@ -0,0 +1,49 @@
<template>
<div class="footer">
{{word}}
</div>
</template>
<script>
export default {
name: 'Footer',
data() {
return {
word: '',
wordList: [
'悟已往之不谏,知来者之可追。实迷途其未远,觉今是而昨非。',
'半山腰总是拥挤的,你要去山顶看看。',
'只有对自己不将就,自己才会变得更优秀。',
'拒绝摆烂,卷起来。',
'每一种热爱,都值得全力以赴。',
'人生如逆旅,我亦是行人。',
'生而无畏,人类的最高品质便是勇气。',
'少年心怀乌托邦,心仍向阳肆生长。',
'平静的大海培养不出优秀的水手。',
'长风破浪会有时,直挂云帆济沧海。',
],
}
},
methods: {
getWord() {
this.word = this.wordList[Math.floor(Math.random() * this.wordList.length)];
},
},
watch: {
$route() {
this.getWord();
}
},
}
</script>
<style scoped>
.footer {
text-align: center;
line-height: 50px;
font-size: 16px;
background-color: #1abc9c;
height: 50px;
color: #fff;
}
</style>

@ -0,0 +1,104 @@
<template>
<div>
<el-row class="row_box">
<el-col :span="15" :offset="2">
<div class="title" @click="toHome"></div>
</el-col>
<el-col :span="3">
<el-menu :default-active="activeIndex" mode="horizontal" @select="handleSelect"
background-color="#1abc9c" text-color="#fff" active-text-color="#ffd04b">
<el-menu-item index="/user/home">书城</el-menu-item>
<el-menu-item index="/user/shoppingCart">购物车</el-menu-item>
</el-menu>
</el-col>
<el-col :span="2">
<el-dropdown @command="handleDownBoxCommand">
<span class="el-dropdown-link">
{{username}}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
</el-row>
</div>
</template>
<script>
import {
mapState
} from "vuex";
export default {
name: 'Header',
data() {
return {
activeIndex: 'home'
};
},
methods: {
handleSelect(key, keyPath) {
this.$router.push(key)
},
handleDownBoxCommand(command) {
if (command === 'logout') {
this.logout();
}
},
logout() {
this.$message({
type:'success',
message:'退出成功'
})
this.$router.push('/welcome')
},
toHome() {
this.$router.push('/user/home')
}
},
computed: {
...mapState({
username: (state) => state.username,
}),
},
watch: {
$route(to,from) {
if (to.name == 'Home') {
this.activeIndex = '/user/home'
} else if(to.name == 'ShoppingCart') {
this.activeIndex = '/user/shoppingCart'
} else {
this.activeIndex = ''
}
}
},
}
</script>
<style scoped>
.row_box {
background-color: #1abc9c;
}
.title {
letter-spacing: 2px;
font-size: 25px;
color: #F6F6F6;
float: left;
font-weight: 600;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 61px;
}
.el-dropdown-link {
cursor: pointer;
color: #fff;
font-size: 15px;
line-height: 61px;
}
.el-icon-arrow-down {
font-size: 12px;
}
</style>

@ -0,0 +1,22 @@
<template>
<div class="manage-layout">
<Header></Header>
<div class="router">
<router-view></router-view>
</div>
<Footer></Footer>
</div>
</template>
<script setup>
import Header from "./components/header/index.vue";
import Footer from "./components/footer/index.vue";
</script>
<style scoped>
.manage-layout {
display: flex;
flex-direction: column;
height: 100%;
}
</style>

@ -0,0 +1,25 @@
// config/database.js
const { Sequelize } = require('sequelize');
console.log('开始创建 sequelize 实例');
const sequelize = new Sequelize('bs', 'root', '123456', {
host: 'localhost',
dialect: 'mysql',
dialectModule: require('mysql2'), // 使用 mysql2 模块
logging: (msg) => {
console.log('数据库操作日志:', msg);
}
});
// 测试连接
(async () => {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();
console.log('sequelize 实例创建成功');
module.exports = sequelize;

@ -0,0 +1,169 @@
const { CartItem } = require('../models'); // 假设你已经定义了 CartItem 模型
const sequelize = require('../config/database');
// 添加商品到购物车的 POST 请求
const addToCart = async (req, res) => {
const { user_id, book_id, quantity, price } = req.body;
//接收前端发送的请求并从请求体中获取用户ID、图书ID、数量和价格信息。
// 添加调试信息
console.log('Request Body:', req.body);
console.log('User ID:', user_id);
console.log('Book ID:', book_id);
console.log('Quantity:', quantity);
console.log('Price:', price);
console.log('Cart data:', this.cart);
// 参数验证
if (!user_id || !book_id || !quantity || !price) {
return res.status(400).json({ error: '缺少必要的参数' });
}
// 验证数据类型
if (typeof user_id !== 'number' || typeof book_id !== 'number' || typeof quantity !== 'number' || typeof price !== 'number') {
return res.status(400).json({ error: '参数类型错误' });
}
try {
const transaction = await sequelize.transaction();
try {
// 检查购物车中是否已经存在相同商品
let cartItem = await CartItem.findOne({
where: { user_id, book_id },
transaction
});
if (cartItem) {
// 如果存在,更新数量
cartItem.quantity += quantity;
await cartItem.save({ transaction });
} else {
// 如果不存在,创建新记录
cartItem = await CartItem.create({
user_id,
book_id,
quantity,
price_at_purchase: price
}, { transaction });
}
await transaction.commit(); // 提交事务
// 返回成功响应
res.status(201).json({ message: '添加购物车成功', cartItem });
} catch (err) {
await transaction.rollback(); // 回滚事务,回滚操作将撤销所有已执行的数据库操作,确保数据库的一致性。
throw err;
}
} catch (err) {
// 错误处理
console.error('Error adding book to cart:', err);
res.status(500).json({ error: '添加购物车失败' });
}
};
// 更新购物车商品数量的 POST 请求
const updateQuantity = async (req, res) => {
const { cartItemId, quantity } = req.body;
if (!cartItemId || !quantity) {
return res.status(400).json({ error: '缺少必要的参数' });
}
try {
const cartItem = await CartItem.findByPk(cartItemId);
if (!cartItem) {
return res.status(404).json({ error: '购物车商品不存在' });
}
cartItem.quantity = quantity;
await cartItem.save();
res.status(200).json({ message: '更新购物车成功', cartItem });
} catch (err) {
console.error('Error updating cart item:', err);
res.status(500).json({ error: '更新购物车失败' });
}
};
// 删除购物车商品的 POST 请求
const deleteCartItem = async (req, res) => {
const { book_id } = req.body;
if (!book_id) {
return res.status(400).json({ error: '缺少必要的参数' });
}
try {
const cartItem = await CartItem.findOne({
where: { book_id: book_id }
});
if (!cartItem) {
return res.status(404).json({ error: '购物车商品不存在' });
}
await cartItem.destroy();
res.status(200).json({ message: '删除购物车成功' });
} catch (err) {
console.error('Error deleting cart item:', err);
res.status(500).json({ error: '删除购物车失败' });
}
};
// 支付购物车的 POST 请求
const payCart = async (req, res) => {
const { user_id, selectedBooks } = req.body;
if (!user_id || !selectedBooks || !Array.isArray(selectedBooks)) {
return res.status(400).json({ error: '缺少必要的参数' });
}
try {
const transaction = await sequelize.transaction();
try {
// 找到用户的所有购物车项
const cartItems = await CartItem.findAll({
where: {
user_id,
book_id: selectedBooks.map(book => book.book_id)
},
transaction
});
if (cartItems.length === 0) {
return res.status(404).json({ error: '购物车商品不存在' });
}
// 处理支付逻辑(这里假设支付成功)
// 实际应用中,你需要集成支付网关
// 删除用户的购物车项
await CartItem.destroy({
where: {
user_id,
book_id: selectedBooks.map(book => book.book_id)
},
transaction
});
await transaction.commit(); // 提交事务
// 返回成功响应
res.status(200).json({ message: '支付成功', cartItems });
} catch (err) {
await transaction.rollback(); // 回滚事务
console.error('Transaction error:', err); // 添加日志
throw err;
}
} catch (err) {
// 错误处理
console.error('Error processing payment:', err);
res.status(500).json({ error: '支付失败' });
}
};
module.exports = {
addToCart,
updateQuantity,
deleteCartItem,
payCart
};

@ -0,0 +1,18 @@
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
// import "./plugins/element";
import "./assets/css/reset.css";
import "animate.css";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.config.productionTip = false;
Vue.use(ElementUI);
/*Vue.prototype.$message = Message;*/
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");

@ -0,0 +1,38 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
/* const CartItem = require('./CartItem'); */
const Books = sequelize.define('Books', {
bookName: {
type: DataTypes.STRING(255),
allowNull: false
},
cover: {
type: DataTypes.STRING(255),
allowNull: true
},
type: {
type: DataTypes.STRING(100),
allowNull: true
},
author: {
type: DataTypes.STRING(100),
allowNull: true
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
stock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
description: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'books'
});
/* Books.hasMany(CartItem, { foreignKey: 'book_id' }); */
module.exports = Books;

@ -0,0 +1,45 @@
// models/CartItem.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const User = require('./User');
const Book = require('./Book');
const CartItem = sequelize.define('CartItem', {
user_id: { // 明确指定外键名称
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id'
}
},
book_id: { // 明确指定外键名称
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Book,
key: 'id'
}
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1
},
price_at_purchase: { // 确保字段名与数据库表一致
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
field: 'price_at_purchase' // 如果数据库中字段名为 price_at_purchase
}
}, {
tableName: 'cartitems',
timestamps: true, // 启用时间戳
createdAt: 'created_at', // 自定义创建时间戳字段名
updatedAt: 'updated_at' // 自定义更新时间戳字段名
});
// 关联模型
CartItem.belongsTo(User, { foreignKey: 'user_id' });
CartItem.belongsTo(Book, { foreignKey: 'book_id' });
module.exports = CartItem;

@ -0,0 +1,19 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
/* const CartItem = require('./CartItem');
*/
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING(255),
allowNull: false
}
}, {
tableName: 'user'
});
/* User.hasMany(CartItem, { foreignKey: 'user_id' }); */
module.exports = User;

@ -0,0 +1,17 @@
//const sequelize = require('../config/database');
const User = require('./User');
const Book = require('./Book');
const CartItem = require('./CartItem');
// 设置关联关系
User.hasMany(CartItem, { foreignKey: 'user_id' });
CartItem.belongsTo(User, { foreignKey: 'user_id' });
Book.hasMany(CartItem, { foreignKey: 'book_id' });
CartItem.belongsTo(Book, { foreignKey: 'book_id' });
module.exports = {
User,
Book,
CartItem
};

@ -0,0 +1,128 @@
<template>
<div class="book">
<el-card class="book_card">
<el-divider content-position="left">{{ book.bookName }}</el-divider>
<el-row>
<el-col :span="5">
<img :src="book.cover" alt="图片加载出错" class="cover">
</el-col>
<el-col :span="19">
<div class="information">类型{{ book.type }}</div>
<div class="information">作者{{ book.author }}</div>
<div class="information">价格{{ book.price }}</div>
<div class="information">库存32</div>
</el-col>
</el-row>
<el-card class="description_box">
<div class="title">
内容简介
</div>
<div class="description">
{{ book.description }}
</div>
</el-card>
<el-button type="success" class="shoppingCart" @click="addCart" plain icon="el-icon-shopping-cart-2">加入购物车</el-button>
</el-card>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: 'Book',
data() {
return {
quantity: 1 // 1
};
},
methods: {
addCart() {
const user = this.user;
const book = this.book;
if (!user || !user.id) {
this.$message({
type: 'warning',
message: '请先登录后再添加购物车!'
});
return;
}
if (!book.book_id || !book.price) {
console.error('book对象缺少book_id或price属性无法添加购物车');
return;
}
this.$store.dispatch('addBookToCart', { ...book, quantity: this.quantity })
.then(() => {
this.$message({
type: 'success',
message: '添加购物车成功'
});
})
.catch(error => {
console.error('添加购物车失败:', error.message);
this.$message({
type: 'error',
message: `添加购物车失败,请稍后再试! ${error.message}`
});
});
}
},
computed: {
...mapState({
book: (state) => state.book,
user: (state) => state.user,
}),
},
}
</script>
<style scoped>
.book {
background-color: #eee;
display: flex;
justify-content: center;
}
.book_card {
margin: 30px 0px;
width: 900px;
}
.el-divider__text {
font-size: 20px;
font-weight: 600;
}
.cover {
width: 100%;
}
.information {
font-size: 16px;
padding: 16px 0px;
padding-left: 25px;
}
.description_box {
margin-top: 30px;
}
.description {
margin-top: 20px;
font-size: 15px;
line-height: 30px;
}
.title {
font-size: 20px;
color: #27ae60;
}
.shoppingCart {
margin: 25px 0px;
float: right;
}
</style>

@ -0,0 +1,239 @@
<template>
<div class="bookList">
<div class="bookList_box">
<div class="category_list">
<span @click="categoryChange(index, $event)" :class="{'active': activeIndex == index}"
v-for="(category, index) in categoryList" :key="index" class="category">{{category}}</span>
</div>
<div class="book_box">
<el-row>
<el-col :span="6" v-for="(book, index) in bookPage" :key="index" class="boox_col">
<div class="item">
<img :src="book.cover" @click="detail(book)" alt="图片加载出错" class="cover">
<div class="information">
<span>{{book.bookName}}</span>
<span>{{book.author}}</span>
</div>
<div class="information">
<el-tag type="success">{{book.price}}</el-tag>
<el-tooltip effect="light" content="加入购物车" placement="right">
<el-button type="success" @click="addCart(book)" size="mini"
icon="el-icon-shopping-cart-2"></el-button>
</el-tooltip>
</div>
</div>
</el-col>
</el-row>
</div>
<el-pagination class="pagination" :current-page="page.currentPage" :page-sizes="[8, 16, 24]"
:page-size="page.pageSize" @size-change="handleSizeChange" @current-change="handleCurrentChange"
layout="prev, pager, next, jumper, ->, sizes, total" :total="page.total">
</el-pagination>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: 'BookList',
data() {
return {
bookPage: [],
page: {
currentPage: 1,
pageSize: 8,
total: 0,
},
activeIndex: 0,
}
},
watch: {
user(newVal) {
console.log('user状态发生变化新值为:', newVal);
if (newVal && newVal.id) {
console.log('User ID is now available:', newVal.id);
}
}
},
methods: {
getBookPage() {
const start = (this.page.currentPage - 1) * this.page.pageSize;
const end = start + this.page.pageSize;
this.bookPage = this.filteredBookList.slice(start, end);
},
getTotal() {
this.page.total = this.filteredBookList.length;
},
handleSizeChange(pageSize) {
this.page.pageSize = pageSize;
this.getBookPage();
this.getTotal();
},
handleCurrentChange(currentPage) {
this.page.currentPage = currentPage;
this.getBookPage();
},
categoryChange(index, event) {
this.activeIndex = index
let type = event.target.innerHTML;
if (type == '全部分类') {
this.getBookPage();
this.getTotal();
} else {
let newBookList = this.bookList.filter((item) => {
if (item.type == type) return true
})
this.page.total = newBookList.length;
this.bookPage = newBookList;
}
},
detail(book) {
this.$router.push('/user/book');
this.$store.commit('DETAIL', book);
},
search(key) {
this.bookPage = this.bookList.filter(item => item.bookName.indexOf(key) === 0);
this.page.total = this.bookPage.length;
},
addCart(book) {
console.log('当前用户信息:', this.user); //
console.log('当前书籍ID:', book.book_id); // ID
console.log('User ID:', this.user.id);
console.log('Price:', book.price); //
const user = this.user;
if (!user || !user.id) {
console.error('用户未登录或用户信息异常无法获取用户id添加购物车失败');
this.$message({
type: 'warning',
message: '请先登录后再添加购物车!'
});
return;
}
if (!book.book_id || !book.price) {
console.error('book对象缺少book_id或price属性无法添加购物车');
return;
}
const quantity = 1; // 1
console.log('Quantity:', quantity);
this.$store.dispatch('addBookToCart', { ...book, quantity })
.then(() => {
this.$message({
type: 'success',
message: '添加购物车成功'
});
})
.catch(error => {
console.error('添加购物车失败:', error.message);
this.$message({
type: 'error',
message: `添加购物车失败,请稍后再试! ${error.message}`
});
});
}
},
mounted() {
console.log('开始执行mounted钩子函数');
this.getBookPage();
console.log('执行完getBookPage方法');
this.getTotal();
console.log('执行完getTotal方法');
console.log('当前用户信息:', this.user); //
this.filteredBookList = this.bookList; // filteredBookList
},
computed: {
...mapState([
'bookList',
'categoryList',
'user' // Vuex便使
]),
filteredBookList: {
get() {
return this._filteredBookList || this.bookList;
},
set(value) {
this._filteredBookList = value;
}
}
}
}
</script>
<style scoped>
.bookList {
display: flex;
justify-content: center;
text-align: center;
background: #eee;
}
.bookList_box {
background: white;
}
.category_list {
width: 1150px;
height: 50px;
}
.category {
font-size: 16px;
padding: 10px 30px;
line-height: 50px;
}
.category:hover {
color: #27ae60;
border-bottom: 2px solid #2ecc71;
cursor: pointer;
}
.active {
color: #27ae60;
border-bottom: 2px solid #2ecc71;
}
.book_box {
width: 1150px;
}
.boox_col {
margin-top: 20px;
display: flex;
justify-content: center;
text-align: center;
}
.item {
padding: 10px 15px;
border: 2px #eeeeee solid;
width: 200px;
}
.item:hover {
box-shadow: 0 0 10px #ddd;
}
.cover {
width: 200px;
height: 200px;
}
.information {
display: flex;
justify-content: space-between;
font-size: 16px;
padding-top: 20px;
}
.pagination {
text-align: center;
padding: 30px 100px;
}
</style>

@ -0,0 +1,48 @@
<template>
<div class="home">
<Search @search="search"></Search>
<BookList ref="bookList"></BookList>
<el-backtop>
<svg t="1652548555126" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="2205" width="32" height="32">
<path
d="M528 67.5l-16-16.7-15.9 16.7c-7.3 7.7-179.9 190.6-179.9 420.8 0 112 40 210.1 73.5 272.7l6.2 11.6H627l5.9-13c3.1-6.8 75-167.8 75-271.3 0-230.2-172.6-413.1-179.9-420.8z m-16 48.8c19 22.9 51.9 66.1 82.3 122.5H429.8c30.3-56.4 63.3-99.6 82.2-122.5z m86.3 612.2H422.5c-25.7-50.6-62.2-140.1-62.2-240.2 0-75 20.8-145.5 47.7-205.4h208.2c26.8 59.9 47.6 130.3 47.6 205.4-0.1 78.3-48.7 200.4-65.5 240.2z"
fill="#1E59E4" p-id="2206"></path>
<path
d="M834.7 623.9H643.3l6.7-27.3c9.1-37 13.7-73.4 13.7-108.2 0-44.8-7.7-92-22.9-140.3l-17-54 49.1 28.3c99.8 57.6 161.8 164.7 161.8 279.5v22z m-135.9-44.2h90.9c-5.7-71-38.8-137.2-91.3-184.6 6.3 31.7 9.4 62.9 9.4 93.2 0.1 29.7-3 60.3-9 91.4zM380.1 623.9H189.3v-22.1c0-114.8 62-221.9 161.8-279.5l49.1-28.3-17 54c-15.2 48.3-22.9 95.5-22.9 140.3 0 34.5 4.5 71 13.4 108.4l6.4 27.2z m-145.8-44.2H325c-5.9-31.3-8.8-61.9-8.8-91.4 0-30.3 3.2-61.5 9.4-93.2-52.5 47.5-85.6 113.6-91.3 184.6zM512 529.5c-45 0-81.6-36.6-81.6-81.6s36.6-81.6 81.6-81.6 81.6 36.6 81.6 81.6-36.6 81.6-81.6 81.6z m0-119c-20.7 0-37.5 16.8-37.5 37.5s16.8 37.5 37.5 37.5 37.5-16.8 37.5-37.5-16.8-37.5-37.5-37.5z"
fill="#1E59E4" p-id="2207"></path>
<path
d="M512 999.7l-20.3-20.3c-28.8-28.6-68.3-67.9-68.3-111.6 0-48.9 39.8-88.6 88.6-88.6 48.9 0 88.6 39.8 88.6 88.6 0 43.6-24.4 67.9-64.8 108.2L512 999.7z m0-176.4c-24.5 0-44.5 20-44.5 44.5 0 21.5 23.8 48.4 44.5 69.5 33.6-33.7 44.4-47 44.4-69.5 0.1-24.6-19.9-44.5-44.4-44.5z"
fill="#FF5A06" p-id="2208"></path>
</svg>
</el-backtop>
</div>
</template>
<script>
import Search from './search'
import BookList from './bookList'
export default {
name: 'Home',
components: {
Search,
BookList
},
methods: {
search(key) {
if (key) {
this.$refs.bookList.search(key)
} else {
this.$message.error('输入不能为空!');
}
}
}
}
</script>
<style scoped>
.goTop {
height: 100vh;
overflow-x: hidden;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

@ -0,0 +1,105 @@
<template>
<div class="search">
<div class="search_box">
<div class="title">Pick Your Perfect Literature</div>
<el-form :model="serachForm" class="search_form">
<el-form-item class="search_item">
<el-row>
<el-col :span="21">
<el-input v-model="serachForm.key" @keyup.enter.native="search" clearable placeholder="请输入书籍关键字"></el-input>
</el-col>
<el-col :span="3">
<el-button type="success" @click="search" icon="el-icon-search"></el-button>
</el-col>
</el-row>
</el-form-item>
</el-form>
<div class="hotSearch">
<span class="active" @click="detail(book)" v-for="(book,index) in hotSearch"
:key="index">{{book.bookName}}&nbsp;&nbsp;&nbsp;</span>
</div>
</div>
</div>
</template>
<script>
import {
mapState
} from "vuex";
export default {
name: 'Search',
data() {
return {
serachForm: {
key: ''
},
hotSearch: [],
}
},
methods: {
search() {
this.$emit('search',this.serachForm.key);
this.serachForm.key = '';
},
getHotSearch() {
this.hotSearch = [];
for (let index = 0; index < 4; index++) {
let book = this.bookList[Math.floor(Math.random() * this.bookList.length)]
this.hotSearch.push(book)
}
},
detail(book) {
this.$router.push('/user/book')
this.$store.commit('DETAIL', book)
}
},
mounted() {
this.getHotSearch();
},
computed: {
...mapState({
bookList: (state) => state.bookList,
}),
},
}
</script>
<style scoped>
.search {
height: 350px;
background: url(./images/beijing.jpg);
display: flex;
justify-content: center;
align-items: center;
}
.search_box {
width: 450px;
}
.title {
text-align: center;
color: #fff;
font-weight: 800;
font-size: 20px;
letter-spacing: 1.5px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding-bottom: 20px;
}
.search_item {
margin: 0px 0px;
}
.hotSearch {
cursor: pointer;
text-align: center;
color: #fff;
font-size: 15px;
padding-top: 15px;
}
.active:hover {
color: #ffd04b
}
</style>

@ -0,0 +1,159 @@
<template>
<div class="change-password-container">
<h2>修改密码</h2>
<form @submit.prevent="changePassword">
<div class="form-group">
<label for="oldPassword">当前密码</label>
<input
v-model="oldPassword"
type="password"
id="oldPassword"
placeholder="请输入当前密码"
required
/>
</div>
<div class="form-group">
<label for="newPassword">新密码</label>
<input
v-model="newPassword"
type="password"
id="newPassword"
placeholder="请输入新密码"
required
/>
</div>
<div class="form-group">
<label for="confirmPassword">确认新密码</label>
<input
v-model="confirmPassword"
type="password"
id="confirmPassword"
placeholder="确认新密码"
required
/>
</div>
<button type="submit" :disabled="isLoading">提交</button>
<div v-if="error" class="error-message">{{ error }}</div>
<div v-if="successMessage" class="success-message">{{ successMessage }}</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
oldPassword: '',
newPassword: '',
confirmPassword: '',
isLoading: false,
error: '',
successMessage: ''
};
},
methods: {
changePassword() {
//
if (this.newPassword !== this.confirmPassword) {
this.error = '新密码和确认密码不一致';
this.successMessage = '';
return;
}
this.isLoading = true;
this.error = '';
this.successMessage = '';
//
setTimeout(() => {
this.isLoading = false;
this.successMessage = '密码修改成功';
}, 2000);
}
}
};
</script>
<style scoped>
.change-password-container {
max-width: 400px;
margin: 50px auto;
padding: 40px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
font-size: 24px;
color: #333;
font-weight: bold;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-size: 14px;
color: #555;
margin-bottom: 8px;
}
input {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
color: #333;
background-color: #f9f9f9;
transition: all 0.3s ease;
}
input:focus {
border-color: #4CAF50;
background-color: #fff;
outline: none;
}
button {
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #ddd;
cursor: not-allowed;
}
.error-message {
color: #e74c3c;
font-size: 14px;
margin-top: 15px;
}
.success-message {
color: #2ecc71;
font-size: 14px;
margin-top: 15px;
}
</style>

@ -0,0 +1,206 @@
<template>
<div class="book-management-container">
<!-- 搜索框 -->
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="输入书名搜索"
suffix-icon="el-icon-search"
clearable
@input="handleSearch"
style="width: 300px;"
/>
</div>
<!-- 书籍信息展示 -->
<div class="book-list">
<el-row :gutter="20">
<!-- 循环书籍数据展示书籍信息 -->
<el-col :span="8" v-for="book in filteredBooks" :key="book.book_id">
<el-card :body-style="{ padding: '20px' }">
<!-- 书籍图片 -->
<div class="book-image">
<img :src="book.cover" alt="书籍图片" />
</div>
<!-- 书籍信息 -->
<div class="book-info">
<h3>{{ book.bookName }}</h3>
<p><strong>价格</strong>{{ book.price }}</p>
<p><strong>库存</strong>{{ book.quantity }} </p>
<p><strong>介绍</strong>{{ book.description }}</p>
</div>
<!-- 编辑按钮 -->
<el-button @click="editBook(book)" type="primary" size="small" style="margin-top: 10px; width: 100%">
编辑
</el-button>
</el-card>
</el-col>
</el-row>
</div>
<!-- 搜索无结果时的提示 -->
<div v-if="filteredBooks.length === 0" class="no-results">
<p>没有找到相关书籍</p>
</div>
<!-- 编辑书籍的弹窗 -->
<el-dialog
title="编辑书籍"
:visible.sync="dialogVisible"
width="400px"
@close="resetForm"
>
<el-form :model="editingBook" label-width="120px">
<el-form-item label="书名" prop="bookName" :rules="[{ required: true, message: '书名不能为空', trigger: 'blur' }]">
<el-input v-model="editingBook.bookName" placeholder="请输入书名"></el-input>
</el-form-item>
<el-form-item label="价格" prop="price" :rules="[{ required: true, message: '价格不能为空', trigger: 'blur' }]">
<el-input v-model="editingBook.price" placeholder="请输入价格"></el-input>
</el-form-item>
<el-form-item label="库存" prop="quantity" :rules="[{ required: true, message: '库存不能为空', trigger: 'blur' }]">
<el-input v-model="editingBook.quantity" placeholder="请输入库存"></el-input>
</el-form-item>
<el-form-item label="介绍" prop="description" :rules="[{ required: true, message: '介绍不能为空', trigger: 'blur' }]">
<el-input v-model="editingBook.description" placeholder="请输入书籍介绍" type="textarea"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetForm"></el-button>
<el-button type="primary" @click="saveBook"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import { Button, Input, Dialog, Form, FormItem, Card, Row, Col } from 'element-ui';
export default {
components: {
ElButton: Button,
ElInput: Input,
ElDialog: Dialog,
ElForm: Form,
ElFormItem: FormItem,
ElCard: Card,
ElRow: Row,
ElCol: Col,
},
data() {
return {
filteredBooks: [],
searchQuery: '',
dialogVisible: false,
editingBook: {},
};
},
computed: {
...mapState(['bookList']),
},
methods: {
...mapMutations(['UPDATE_BOOK']),
handleSearch() {
this.filteredBooks = this.bookList.filter((book) =>
book.bookName.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
editBook(book) {
this.editingBook = {...book}; //
this.dialogVisible = true;
},
saveBook() {
const index = this.bookList.findIndex((book) => book.book_id === this.editingBook.book_id);
if (index !== -1) {
this.UPDATE_BOOK({index, book: {...this.editingBook}}); //
this.$message.success('书籍信息已保存');
} else {
this.$message.error('找不到该书籍');
}
this.dialogVisible = false;
this.handleSearch(); //
},
resetForm() {
this.editingBook = {};
this.dialogVisible = false;
},
},
mounted() {
this.filteredBooks = [...this.bookList];
this.handleSearch();
},
watch: {
searchQuery: 'handleSearch',
},
};
</script>
<style scoped>
.book-management-container {
margin: 20px;
background-color: #f7f7f7;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.search-bar {
margin-bottom: 20px;
display: flex;
justify-content: flex-start; /* 左对齐 */
align-items: center;
}
.book-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: flex-start; /* 确保排列方式为从左到右 */
overflow-y: auto; /* 如果超出高度会滚动 */
max-height: 80vh; /* 最大高度,避免过高导致页面过长 */
}
.book-image img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px;
}
.book-info {
margin-top: 10px;
}
.no-results {
text-align: center;
margin-top: 50px;
color: #ccc;
}
.dialog-footer {
text-align: right;
}
.el-button {
background-color: #409eff;
border-color: #409eff;
}
.el-dialog {
min-width: 400px;
}
.el-form-item {
margin-bottom: 20px;
}
.el-input,
.el-select {
width: 100%;
}
.el-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

@ -0,0 +1,224 @@
<template>
<div class="book-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索书籍(书名、作者)"
suffix-icon="el-icon-search"
@input="handleSearch"
clearable
style="width: 300px;"
/>
<el-button type="primary" @click="openAddDialog" style="margin-left: 10px;">新增书籍</el-button>
</div>
<!-- 书籍信息展示 -->
<el-table :data="currentPageBooks" stripe style="width: 100%">
<el-table-column label="书名" prop="title"></el-table-column>
<el-table-column label="作者" prop="author"></el-table-column>
<el-table-column label="ISBN" prop="isbn"></el-table-column>
<el-table-column label="价格" prop="price"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="{ row }">
<el-button @click="editBook(row)" type="text" size="small">编辑</el-button>
<el-button @click="deleteBook(row)" type="text" size="small" class="delete-btn">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="totalBooks > 10"
:current-page="currentPage"
:page-size="pageSize"
:total="totalBooks"
layout="prev, pager, next, jumper"
@current-change="handlePageChange"
></el-pagination>
<!-- 增加书籍的表单 -->
<el-dialog
:visible.sync="dialogVisible"
title="新增/编辑书籍"
@close="resetForm"
>
<el-form :model="newBook" label-width="120px">
<el-form-item label="书名" prop="title" :rules="[{ required: true, message: '书名不能为空', trigger: 'blur' }]">
<el-input v-model="newBook.title" placeholder="请输入书名"></el-input>
</el-form-item>
<el-form-item label="作者" prop="author" :rules="[{ required: true, message: '作者不能为空', trigger: 'blur' }]">
<el-input v-model="newBook.author" placeholder="请输入作者"></el-input>
</el-form-item>
<el-form-item label="ISBN" prop="isbn" :rules="[{ required: true, message: 'ISBN不能为空', trigger: 'blur' }]">
<el-input v-model="newBook.isbn" placeholder="请输入ISBN"></el-input>
</el-form-item>
<el-form-item label="价格" prop="price" :rules="[{ required: true, message: '价格不能为空', trigger: 'blur' }]">
<el-input v-model="newBook.price" placeholder="请输入价格"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status" :rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]">
<el-select v-model="newBook.status" placeholder="请选择状态">
<el-option label="上架" value="上架"></el-option>
<el-option label="下架" value="下架"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetForm"></el-button>
<el-button type="primary" @click="saveBook"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
/*import { ref, onMounted, watch } from 'vue';*/
/*import { ElMessage } from 'element-ui';*/
//
const dummyBooks = [
{ id: 1, title: '书籍 A', author: '作者 A', isbn: '978-1-23456-789-0', price: '20.00', status: '上架' },
{ id: 2, title: '书籍 B', author: '作者 B', isbn: '978-1-23456-789-1', price: '25.00', status: '下架' },
// ...
];
export default {
data() {
return {
books: [...dummyBooks], //
currentPageBooks: [], //
currentPage: 1, //
pageSize: 10, //
totalBooks: 0, //
searchQuery: '', //
dialogVisible: false, //
newBook: {
id: null,
title: '',
author: '',
isbn: '',
price: '',
status: '上架',
},
};
},
methods: {
//
fetchBooks() {
//
const filteredBooks = this.books.filter((book) => {
return (
book.title.includes(this.searchQuery) || book.author.includes(this.searchQuery)
);
});
//
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = this.currentPage * this.pageSize;
this.currentPageBooks = filteredBooks.slice(startIndex, endIndex);
this.totalBooks = filteredBooks.length;
},
//
handleSearch() {
this.currentPage = 1; //
this.fetchBooks(); //
},
//
handlePageChange(page) {
this.currentPage = page;
this.fetchBooks();
},
//
openAddDialog() {
this.resetForm();
this.dialogVisible = true;
},
//
saveBook() {
if (this.newBook.id === null) {
//
this.newBook.id = this.books.length + 1;
this.books.push(this.newBook);
} else {
//
const index = this.books.findIndex((book) => book.id === this.newBook.id);
if (index !== -1) {
this.books[index] = { ...this.newBook };
}
}
this.$message.success('操作成功');
this.dialogVisible = false;
this.fetchBooks(); //
},
//
editBook(book) {
this.newBook = { ...book };
this.dialogVisible = true;
},
//
deleteBook(book) {
const index = this.books.findIndex((b) => b.id === book.id);
if (index !== -1) {
this.books.splice(index, 1);
}
this.$message.success('删除成功');
this.fetchBooks(); //
},
//
resetForm() {
this.newBook = {
id: null,
title: '',
author: '',
isbn: '',
price: '',
status: '上架',
};
},
},
mounted() {
this.fetchBooks();
},
watch: {
searchQuery: 'fetchBooks',
},
};
</script>
<style scoped>
.book-container {
margin: 20px;
}
.search-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.delete-btn {
color: red;
font-size: 14px;
background-color: transparent;
border: none;
cursor: pointer;
}
.delete-btn:hover {
text-decoration: underline;
}
.dialog-footer {
text-align: right;
}
</style>

@ -0,0 +1,178 @@
<template>
<div class="user-container">
<!-- 新增用户按钮 -->
<el-button type="primary" @click="openAddDialog" style="margin-bottom: 20px;">新增用户</el-button>
<!-- 用户信息展示 -->
<el-table :data="allUsers" stripe style="width: 100%">
<el-table-column label="用户名" prop="username"></el-table-column>
<el-table-column label="邮箱" prop="email"></el-table-column>
<el-table-column label="电话" prop="phone"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="{ row }">
<el-button @click="editUser(row)" type="text" size="small">编辑</el-button>
<el-button @click="deleteUser(row)" type="text" size="small" class="delete-btn">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="totalUsers > 10"
:current-page="currentPage"
:page-size="pageSize"
:total="totalUsers"
layout="prev, pager, next, jumper"
@current-change="handlePageChange"
></el-pagination>
<!-- 增加用户的表单 -->
<el-dialog
:visible.sync="dialogVisible"
title="新增/编辑用户"
@close="resetForm"
>
<el-form :model="newUser" label-width="120px">
<el-form-item label="用户名" prop="username" :rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]">
<el-input v-model="newUser.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email" :rules="[{ required: true, message: '邮箱不能为空', trigger: 'blur' }]">
<el-input v-model="newUser.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item label="电话" prop="phone" :rules="[{ required: true, message: '电话不能为空', trigger: 'blur' }]">
<el-input v-model="newUser.phone" placeholder="请输入电话"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status" :rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]">
<el-select v-model="newUser.status" placeholder="请选择状态">
<el-option label="激活" value="激活"></el-option>
<el-option label="冻结" value="冻结"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetForm"></el-button>
<el-button type="primary" @click="saveUser"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
users: [
/*{ id: 1, username: '用户 A', email: 'usera@example.com', phone: '1234567890', status: '激活' },*/
// ...
],
currentPageUsers: [],
currentPage: 1,
pageSize: 10,
totalUsers: 0,
dialogVisible: false,
newUser: {
id: null,
username: '',
email: '',
phone: '',
status: '激活',
},
};
},
computed: {
...mapState(['registerUser']),
allUsers() {
//
return [...this.users, ...this.registerUser];
},
},
mounted() {
this.fetchUsers();
},
watch: {
allUsers: {
handler() {
this.fetchUsers();
},
deep: true,
},
},
methods: {
fetchUsers() {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = this.currentPage * this.pageSize;
const pageData = this.allUsers.slice(startIndex, endIndex);
this.currentPageUsers = pageData;
this.totalUsers = this.allUsers.length; //
},
handlePageChange(page) {
this.currentPage = page;
this.fetchUsers();
},
openAddDialog() {
this.newUser = { id: null, username: '', email: '', phone: '', status: '激活' }; //
this.dialogVisible = true;
},
saveUser() {
if (this.newUser.username && this.newUser.email && this.newUser.phone && this.newUser.status) {
const newId = this.allUsers.length + 1; // ID
this.newUser.id = newId;
//
this.$store.commit('REGISTER', this.newUser);
//
this.dialogVisible = false;
//
this.$message.success('用户信息保存成功');
this.fetchUsers(); //
this.resetForm(); //
} else {
this.$message.error('请填写完整信息');
}
},
editUser(row) {
this.newUser = { ...row }; //
this.dialogVisible = true; //
},
deleteUser(row) {
this.$store.commit('DELETE_USER', row.id); //
this.$message.success('用户删除成功');
this.fetchUsers(); //
},
resetForm() {
this.newUser = {
id: null,
username: '',
email: '',
phone: '',
status: '激活',
};
},
},
};
</script>
<style scoped>
.user-container {
margin: 20px;
}
.delete-btn {
color: red;
font-size: 14px;
background-color: transparent;
border: none;
cursor: pointer;
}
.delete-btn:hover {
text-decoration: underline;
}
</style>

@ -0,0 +1,190 @@
<template>
<div class="merchant-container">
<!-- 新增商家按钮 -->
<el-button type="primary" @click="openAddDialog" style="margin-bottom: 20px;">新增商家</el-button>
<!-- 商家信息展示 -->
<el-table :data="currentPageMerchants" stripe style="width: 100%">
<el-table-column label="商家名称" prop="name"></el-table-column>
<el-table-column label="商家地址" prop="address"></el-table-column>
<el-table-column label="联系方式" prop="phone"></el-table-column>
<el-table-column label="商家状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="{ row }">
<el-button @click="editMerchant(row)" type="text" size="small">编辑</el-button>
<el-button @click="deleteMerchant(row)" type="text" size="small" class="delete-btn">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="totalMerchants > 10"
:current-page.sync="currentPage"
:page-size="pageSize"
:total="totalMerchants"
layout="prev, pager, next, jumper"
@current-change="handlePageChange"
></el-pagination>
<!-- 增加商家的表单 -->
<el-dialog
:visible.sync="dialogVisible"
title="新增/编辑商家"
@close="resetForm"
>
<el-form :model="newMerchant" :rules="rules" ref="form" label-width="120px">
<el-form-item label="商家名称" prop="name">
<el-input v-model="newMerchant.name" placeholder="请输入商家名称"></el-input>
</el-form-item>
<el-form-item label="商家地址" prop="address">
<el-input v-model="newMerchant.address" placeholder="请输入商家地址"></el-input>
</el-form-item>
<el-form-item label="联系方式" prop="phone">
<el-input v-model="newMerchant.phone" placeholder="请输入联系方式"></el-input>
</el-form-item>
<el-form-item label="商家状态" prop="status">
<el-select v-model="newMerchant.status" placeholder="请选择商家状态">
<el-option label="正常" value="正常"></el-option>
<el-option label="暂停" value="暂停"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetForm"></el-button>
<el-button type="primary" @click="saveMerchant"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
/*import { ref, onMounted, watch } from 'vue';*/
/*import { ElTable, ElTableColumn, ElButton, ElPagination, ElDialog, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElMessage } from 'element-ui';*/
//
const dummyMerchants = [
{ id: 1, name: 'seller', address: '地址 A', phone: '1234567890', status: '正常' },
// ...
];
export default {
data() {
return {
merchants: [...dummyMerchants], //
currentPageMerchants: [], //
currentPage: 1, //
pageSize: 10, //
totalMerchants: dummyMerchants.length, //
dialogVisible: false, //
newMerchant: {
id: null,
name: '',
address: '',
phone: '',
status: '正常',
},
rules: {
name: [{ required: true, message: '商家名称不能为空', trigger: 'blur' }],
address: [{ required: true, message: '商家地址不能为空', trigger: 'blur' }],
phone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
status: [{ required: true, message: '商家状态不能为空', trigger: 'blur' }],
},
};
},
methods: {
//
fetchMerchants() {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = this.currentPage * this.pageSize;
const pageData = this.merchants.slice(startIndex, endIndex);
this.currentPageMerchants = pageData;
this.totalMerchants = this.merchants.length; //
},
//
openAddDialog() {
this.newMerchant = { id: null, name: '', address: '', phone: '', status: '正常' }; //
this.dialogVisible = true;
},
//
saveMerchant() {
if (this.newMerchant.name && this.newMerchant.address && this.newMerchant.phone && this.newMerchant.status) {
const newId = this.merchants.length + 1; // ID
this.newMerchant.id = newId;
//
this.merchants.push({ ...this.newMerchant });
//
this.dialogVisible = false;
//
this.$message.success('商家信息保存成功');
this.fetchMerchants(); //
this.resetForm(); //
} else {
this.$message.error('请填写完整信息');
}
},
//
editMerchant(row) {
this.newMerchant = { ...row }; //
this.dialogVisible = true; //
},
//
deleteMerchant(row) {
this.merchants = this.merchants.filter((merchant) => merchant.id !== row.id); //
this.$message.success('商家删除成功');
this.fetchMerchants(); //
},
//
resetForm() {
this.newMerchant = {
id: null,
name: '',
address: '',
phone: '',
status: '正常',
};
},
//
handlePageChange(page) {
this.currentPage = page;
this.fetchMerchants();
},
},
mounted() {
this.fetchMerchants();
},
watch: {
totalMerchants() {
this.fetchMerchants(); //
},
},
};
</script>
<style scoped>
.merchant-container {
margin: 20px;
}
.delete-btn {
color: red;
font-size: 14px;
background-color: transparent;
border: none;
cursor: pointer;
}
.delete-btn:hover {
text-decoration: underline;
}
</style>

@ -0,0 +1,208 @@
<template>
<div class="container">
<el-row class="top">
<el-col :span="20" class="col">个人信息</el-col>
</el-row>
<el-row class="body">
<el-col :span="6" class="avatar-col">
<el-image
class="user-img"
:src="userImgSrc"
@error="handleImageError"
fit="cover"
></el-image>
<el-button class="upload-btn" type="text" @click="triggerFileUpload"></el-button>
<input type="file" @change="handleFileUpload" style="display: none;" />
</el-col>
<el-col :span="14" class="form-col">
<el-tag class="phone">{{ phoneNumber }}</el-tag>
<el-form label-width="auto" class="user-form" style="max-width: 600px">
<el-form-item label="昵称">
<el-input v-model="form.nickname" placeholder="请输入昵称"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-select v-model="form.sex" placeholder="选择性别">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
</el-form-item>
<el-form-item label="生日">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="Pick a day"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="地区">
<el-input v-model="form.region" placeholder="请输入地区"></el-input>
</el-form-item>
<el-form-item label="电话号码">
<el-input v-model="form.phoneNumber" type="tel" placeholder="请输入电话号码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="save-btn" @click="saveForm"></el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
data() {
return {
userImgSrc: '/user.png', //
form: {
nickname: '',
sex: '',
birthday: null,
region: '',
profileImage: null,
phoneNumber: '' //
},
};
},
computed: {
phoneNumber() {
return this.form.phoneNumber;
}
},
created() {
this.fetchUserInfo();
},
methods: {
// localStorage
fetchUserInfo() {
const storedForm = localStorage.getItem('userInfo');
if (storedForm) {
const parsedForm = JSON.parse(storedForm);
this.form = parsedForm;
this.userImgSrc = parsedForm.profileImage || '/user.png';
}
},
//
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.userImgSrc = e.target.result;
this.form.profileImage = e.target.result;
};
reader.readAsDataURL(file);
}
},
//
triggerFileUpload() {
const fileInput = document.querySelector('input[type="file"]');
fileInput?.click();
},
//
saveForm() {
localStorage.setItem('userInfo', JSON.stringify(this.form));
this.$message({
message: '保存成功',
type: 'success'
});
},
//
handleImageError() {
this.userImgSrc = '/user.png'; // 使
},
},
};
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 30px;
background-color: #f4f7f6;
min-height: 100vh;
}
.top {
margin-bottom: 30px;
text-align: center;
font-size: 24px;
font-weight: 600;
}
.col {
font-size: 30px;
text-align: center;
}
.body {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 30px;
}
.avatar-col {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user-img {
width: 120px;
height: 120px;
border-radius: 50%;
margin-bottom: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.upload-btn {
font-size: 12px;
color: #409eff;
margin-top: 10px;
}
.form-col {
flex: 1;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.phone {
margin-top: 10px;
font-size: 14px;
color: #666;
}
.user-form .el-form-item {
margin-bottom: 15px;
}
.user-form .el-input, .user-form .el-select, .user-form .el-date-picker {
border-radius: 8px;
}
.save-btn {
width: 100%;
padding: 10px 0;
font-size: 16px;
}
.el-button--primary {
background-color: #409eff;
border-color: #409eff;
}
</style>

@ -0,0 +1,251 @@
<template>
<div class="order-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索订单(订单号、顾客姓名)"
suffix-icon="el-icon-search"
@input="handleSearch"
clearable
style="width: 300px;"
/>
<el-button type="primary" @click="openAddDialog" style="margin-left: 10px;">新增订单</el-button>
</div>
<!-- 订单信息展示 -->
<el-table :data="currentPageOrders" stripe style="width: 100%">
<el-table-column label="订单号" prop="orderId"></el-table-column>
<el-table-column label="顾客姓名" prop="customerName"></el-table-column>
<el-table-column label="商品" prop="product"></el-table-column>
<el-table-column label="数量" prop="quantity"></el-table-column>
<el-table-column label="总金额" prop="totalAmount"></el-table-column>
<el-table-column label="状态" prop="status"></el-table-column>
<el-table-column label="操作">
<template slot-scope="{ row }">
<el-button @click="editOrder(row)" type="text" size="small">编辑</el-button>
<el-button @click="deleteOrder(row)" type="text" size="small" class="delete-btn">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="totalOrders > 10"
:current-page="currentPage"
:page-size="pageSize"
:total="totalOrders"
layout="prev, pager, next, jumper"
@current-change="handlePageChange"
></el-pagination>
<!-- 增加订单的表单 -->
<el-dialog :visible.sync="dialogVisible" title="新增/编辑订单" @close="resetForm">
<el-form :model="newOrder" label-width="120px">
<el-form-item label="订单号" prop="orderId" :rules="[{ required: true, message: '订单号不能为空', trigger: 'blur' }]">
<el-input v-model="newOrder.orderId" placeholder="请输入订单号"></el-input>
</el-form-item>
<el-form-item label="顾客姓名" prop="customerName" :rules="[{ required: true, message: '顾客姓名不能为空', trigger: 'blur' }]">
<el-input v-model="newOrder.customerName" placeholder="请输入顾客姓名"></el-input>
</el-form-item>
<el-form-item label="商品" prop="product" :rules="[{ required: true, message: '商品不能为空', trigger: 'blur' }]">
<el-input v-model="newOrder.product" placeholder="请输入商品名称"></el-input>
</el-form-item>
<el-form-item label="数量" prop="quantity" :rules="[{ required: true, message: '数量不能为空', trigger: 'blur' }]">
<el-input v-model="newOrder.quantity" placeholder="请输入数量" type="number"></el-input>
</el-form-item>
<el-form-item label="总金额" prop="totalAmount" :rules="[{ required: true, message: '总金额不能为空', trigger: 'blur' }]">
<el-input v-model="newOrder.totalAmount" placeholder="请输入总金额" type="number"></el-input>
</el-form-item>
<el-form-item label="状态" prop="status" :rules="[{ required: true, message: '状态不能为空', trigger: 'blur' }]">
<el-select v-model="newOrder.status" placeholder="请选择状态">
<el-option label="待付款" value="待付款"></el-option>
<el-option label="已发货" value="已发货"></el-option>
<el-option label="已完成" value="已完成"></el-option>
<el-option label="已取消" value="已取消"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetForm"></el-button>
<el-button type="primary" @click="saveOrder"></el-button>
</span>
</el-dialog>
</div>
</template>
<script>
/*import { ref, onMounted, watch } from 'vue';*/
/*import { ElTable, ElTableColumn, ElButton, ElPagination, ElDialog, ElForm, ElFormItem, ElInput, ElSelect, ElOption, Message } from 'element-ui';*/
export default {
/*components: {
ElTable,
ElTableColumn,
ElButton,
ElPagination,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
}*/
data() {
return {
orders: [...dummyOrders], //
currentPageOrders: [], //
currentPage: 1, //
pageSize: 10, //
totalOrders: 0, //
searchQuery: '', //
dialogVisible: false, //
newOrder: {
orderId: '',
customerName: '',
product: '',
quantity: 1,
totalAmount: 0,
status: '待付款',
},
};
},
created() {
this.totalOrders = this.orders.length;
},
mounted() {
this.fetchOrders();
},
watch: {
searchQuery() {
this.fetchOrders();
},
},
methods: {
fetchOrders() {
const filteredOrders = this.orders.filter((order) => {
return (
order.orderId.includes(this.searchQuery) || order.customerName.includes(this.searchQuery)
);
});
//
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = this.currentPage * this.pageSize;
this.currentPageOrders = filteredOrders.slice(startIndex, endIndex);
this.totalOrders = filteredOrders.length;
},
handleSearch() {
this.currentPage = 1; //
this.fetchOrders(); //
},
handlePageChange(page) {
this.currentPage = page;
this.fetchOrders();
},
openAddDialog() {
this.resetForm();
this.dialogVisible = true;
},
saveOrder() {
if (!this.newOrder.orderId) {
this.$message.error('订单号不能为空');
return;
}
if (this.newOrder.orderId && !this.newOrder.orderId.startsWith('100')) {
this.$message.error('订单号格式不正确');
return;
}
//
if (!this.newOrder.id) {
this.newOrder.id = this.orders.length + 1;
this.orders.push(this.newOrder);
} else {
//
const index = this.orders.findIndex((order) => order.orderId === this.newOrder.orderId);
if (index !== -1) {
this.orders[index] = { ...this.newOrder };
}
}
this.$message.success('操作成功');
this.dialogVisible = false;
this.fetchOrders(); //
},
editOrder(order) {
this.newOrder = { ...order };
this.dialogVisible = true;
},
deleteOrder(order) {
const index = this.orders.findIndex((o) => o.orderId === order.orderId);
if (index !== -1) {
this.orders.splice(index, 1);
}
this.$message.success('删除成功');
this.fetchOrders(); //
},
resetForm() {
this.newOrder = {
orderId: '',
customerName: '',
product: '',
quantity: 1,
totalAmount: 0,
status: '待付款',
};
},
},
};
//
const dummyOrders = [
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
{ orderId: '1002', customerName: '李四', product: '书籍 B', quantity: 1, totalAmount: 25.00, status: '已发货' },
{ orderId: '1003', customerName: '王五', product: '书籍 C', quantity: 3, totalAmount: 54.00, status: '已完成' },
{ orderId: '1004', customerName: '赵六', product: '书籍 D', quantity: 1, totalAmount: 22.00, status: '已取消' },
{ orderId: '1005', customerName: '孙七', product: '书籍 E', quantity: 2, totalAmount: 60.00, status: '待付款' },
{ orderId: '1006', customerName: '周八', product: '书籍 F', quantity: 4, totalAmount: 60.00, status: '已发货' },
{ orderId: '1007', customerName: '吴九', product: '书籍 G', quantity: 1, totalAmount: 28.00, status: '已完成' },
{ orderId: '1008', customerName: '郑十', product: '书籍 H', quantity: 5, totalAmount: 85.00, status: '待付款' },
{ orderId: '1009', customerName: '钱一', product: '书籍 I', quantity: 1, totalAmount: 19.00, status: '已发货' },
{ orderId: '1010', customerName: '陈二', product: '书籍 J', quantity: 3, totalAmount: 72.00, status: '已完成' },
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
{ orderId: '1001', customerName: '张三', product: '书籍 A', quantity: 2, totalAmount: 40.00, status: '待付款' },
// ...
];
</script>
<style scoped>
.order-container {
margin: 20px;
}
.search-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.delete-btn {
color: red;
font-size: 14px;
background-color: transparent;
border: none;
cursor: pointer;
}
.delete-btn:hover {
text-decoration: underline;
}
.dialog-footer {
text-align: right;
}
</style>

@ -0,0 +1,180 @@
<template>
<el-container class="layout-container-demo" style="height: 100vh; width: 100%">
<el-header class="header">管理员</el-header>
<!-- 侧边 -->
<el-aside width="200px">
<el-scrollbar>
<el-menu :default-openeds="['2', '3']">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-setting"><img class="sign" src="@/assets/logo.png" alt="" /></i> 我的信息
</template>
<el-menu-item index="1-1">
<router-link to="/manager/myInfo">
<span>我的资料</span>
</router-link>
</el-menu-item>
<el-menu-item index="1-2">
<router-link to="/manager/accopass">
<span>修改密码</span>
</router-link>
</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-setting"><img class="sign" src="@/assets/merchant.png" alt="" /></i> 商家资料
</template>
<el-menu-item index="2-2">
<router-link to="/manager/merchant">
<span>商家信息</span>
</router-link>
</el-menu-item>
</el-submenu>
<el-submenu index="3">
<template slot="title">
<i class="el-icon-setting"><img class="sign" src="@/assets/users.png" alt="" /></i> 用户资料
</template>
<el-menu-item index="3-1">
<router-link to="/manager/client">
<span>用户信息</span>
</router-link>
</el-menu-item>
</el-submenu>
<el-submenu index="4">
<template slot="title">
<i class="el-icon-setting"><img class="sign" src="@/assets/goods.png" alt="" /></i> 商品管理
</template>
<el-menu-item index="4-1">
<router-link to="/manager/bookinfo">
<span>书籍信息</span>
</router-link>
</el-menu-item>
<!-- <el-menu-item index="4-2">
<router-link to="/manager/books">
<span>所有书籍</span>
</router-link>
</el-menu-item>-->
</el-submenu>
<el-submenu index="5">
<template slot="title">
<i class="el-icon-setting"><img class="sign" src="@/assets/orders.png" alt="" /></i> 订单管理
</template>
<el-menu-item index="5-1">
<router-link to="/manager/orders">
<span>所有订单</span>
</router-link>
</el-menu-item>
</el-submenu>
</el-menu>
</el-scrollbar>
</el-aside>
<!-- 主要内容区域 -->
<el-main>
<router-view></router-view> <!-- -->
</el-main>
<el-footer class="footer">励学敦行</el-footer>
</el-container>
</template>
<script>
export default {
data() {
return {
//
item: {
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
tableData: Array.from({ length: 20 }).fill(this.item),
};
},
};
</script>
<style scoped>
.layout-container-demo {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #1abc9c;
z-index: 1000;
letter-spacing: 2px;
font-size: 25px;
color: #F6F6F6;
float: left;
font-weight: 600;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 61px;
text-align: left;
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #1abc9c;
color: white;
text-align: center;
line-height: 50px; /* 根据需要调整高度 */
z-index: 1000;
}
.layout-container-demo .el-aside {
color: #1abc9c;
background: #1abc9c;
position: relative;
height: calc(100vh - 120px); /* 假设 header 和 footer 高度都是 60px */
}
.layout-container-demo .el-main {
padding: 20px; /* 添加一些内边距来保持内容不靠近边缘 */
height: calc(100% - 120px); /* 减去头部和底部的高度 */
overflow-y: auto; /* 如果内容过多,启用滚动条 */
}
.layout-container-demo .el-menu {
border-right: none;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
right: 20px;
}
.sign {
width: 20px;
height: 20px;
vertical-align: middle;
}
.layout-container-demo .el-menu-item.is-active {
background-color: #16ac8c; /* 设置选中状态时菜单项的背景颜色 */
}
.layout-container-demo .el-menu-item:hover {
background-color: #55c755; /* 设置菜单项 hover 时的背景颜色 */
}
.layout-container-demo .el-menu-item.is-opened {
background-color: #55c755; /* 设置展开后子菜单的背景色(选中状态) */
}
.title {
font-size: 45px;
}
</style>

@ -0,0 +1,228 @@
<template>
<div class="shoppingCart">
<el-card class="shopping_card">
<div class="title">
<span>购物车</span>
<i class="el-icon-shopping-cart-full"></i>
</div>
<el-table class="shopping_table" :data="cart" border style="width: 100%" @selection-change="selectChange" ref="table">
<el-table-column type="selection" label="全部" width="80" align="center"></el-table-column>
<el-table-column prop="cover" label="商品" width="150" align="center">
<template slot-scope="{row}">
<img :src="row.cover" alt="图片加载错误" class="cover">
<div class="info">
<p>{{ row.name }}</p>
<p>{{ row.author }}</p>
</div>
</template>
</el-table-column>
<el-table-column prop="price" label="单价(元)" width="169" align="center"></el-table-column>
<el-table-column prop="number" label="数量" width="200" align="center">
<template slot-scope="{row}">
<el-button plain icon="el-icon-minus" @click="subNumber(row)" size="mini"></el-button>
<span class="number">{{ row.number }}</span>
<el-button plain icon="el-icon-plus" @click="addNumber(row)" size="mini"></el-button>
</template>
</el-table-column>
<el-table-column prop="prop" label="小计(元)" width="180" align="center">
<template slot-scope="{row}">
{{ row.number * row.price }}
</template>
</el-table-column>
<el-table-column prop="date" label="操作" width="180" align="center">
<template slot-scope="{row}">
<div class="action-buttons">
<el-button type="success" @click="detail(row)"></el-button>
<el-tooltip effect="light" content="删除商品" placement="right">
<el-button type="danger" icon="el-icon-delete" @click="deleteBook(row)"></el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<div class="bottom">
<el-button class="selectall" type="primary" plain @click="selectAll"></el-button>
<span class="tips">合计{{ total }}</span>
<el-button type="success" plain @click="pay"></el-button>
</div>
</el-card>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
name: 'ShoppingCart',
data() {
return {
selectNumber: 0,
total: 0,
selectBook: [],
isAllSelected: false
}
},
computed: {
...mapState({
cart: (state) => state.cart,
}),
user() {
return this.$store.state.user; // Vuex state
}
},
methods: {
...mapActions(['deleteBookFromCart', 'addBook', 'subBook', 'pay']),
selectChange(selection) {
this.selectBook = selection;
this.selectNumber = selection.length;
this.total = selection.reduce((acc, item) => acc + item.number * item.price, 0);
this.isAllSelected = selection.length === this.cart.length;
},
selectAll() {
this.isAllSelected = !this.isAllSelected;
this.$refs.table.toggleAllSelection();
},
clearCart() {
if (!this.user || !this.user.id) {
console.error('用户未登录或用户信息异常无法获取用户id清空购物车失败');
return;
}
this.$store.dispatch('clearCart');
this.$message({
type: 'success',
message: '购物车已清空!'
});
},
deleteBook(book) {
if (!this.user || !this.user.id) {
console.error('用户未登录或用户信息异常无法获取用户id删除购物车失败');
return;
}
if (!book.book_id) {
console.error('book对象缺少book_id属性无法删除购物车');
return;
}
this.deleteBookFromCart(book);
},
detail(book) {
this.$router.push('/book');
this.$store.commit('DETAIL', book);
},
addNumber(book) {
if (!this.user || !this.user.id) {
console.error('用户未登录或用户信息异常无法获取用户id更新购物车失败');
return;
}
if (!book.book_id) {
console.error('book对象缺少book_id属性无法更新购物车');
return;
}
this.addBook(book);
},
subNumber(book) {
if (!this.user || !this.user.id) {
console.error('用户未登录或用户信息异常无法获取用户id更新购物车失败');
return;
}
if (!book.book_id) {
console.error('book对象缺少book_id属性无法更新购物车');
return;
}
this.subBook(book);
},
pay() {
if (!this.user || !this.user.id) {
console.error('用户未登录或用户信息异常无法获取用户id结算失败');
return;
}
// pay
const payload = {
user_id: this.user.id,
selectedBooks: this.selectBook.map(book => ({book_id: book.book_id, quantity: book.number})),
totalPrice: this.total
};
console.log('即将支付的参数:', payload);
this.$store.dispatch('pay', {
user_id: this.user.id,
selectedBooks: this.selectBook.map(book => ({book_id: book.book_id, quantity: book.number})),
totalPrice: this.total
}).then(() => {
console.log('支付成功,购物车已清空');
//
this.selectBook = [];
this.total = 0;
this.selectNumber = 0;
this.isAllSelected = false;
//
this.$router.push('/user/home');
}).catch(error => {
console.error('支付失败:', error);
});
}
}
}
</script>
<style scoped>
.shoppingCart {
display: flex;
justify-content: center;
text-align: center;
background-color: #eee;
}
.shopping_card {
margin: 30px 0px;
width: 1000px;
}
.title {
font-size: 20px;
font-weight: 600;
padding-left: 10px;
padding-bottom: 10px;
text-align: left;
}
.cover {
width: 100px;
height: 100px;
}
.selectall {
margin-right: 15px;
}
.info {
text-align: left;
margin-left: 10px;
}
.number {
padding: 0px 10px;
}
.bottom {
margin-top: 20px;
text-align: right;
}
.tips {
font-size: 16px;
padding-right: 20px;
}
.action-buttons {
display: flex;
gap: 10px;
}
.el-table__body-wrapper {
width: 100%;
}
.el-table__body-wrapper::-webkit-scrollbar {
display: none;
}
</style>

@ -0,0 +1,81 @@
<template>
<div>
<el-col :span="12" class="col_box">
<div class="title_1">{{title_1}}</div>
<div class="title_2">{{title_2}}</div>
<div class="title_3">
<span class="title_3_1">{{title_3_1}}</span>
<span class="title_3_2">{{title_3_2}}</span>
</div>
<img class="cover" :src="cover">
<div>
<el-button round class="to_login" @click="change" type="success" plain>{{buttonTitle}}</el-button>
</div>
</el-col>
</div>
</template>
<script>
export default {
name: 'Bookmark',
props: ['title_1', 'title_2', 'title_3_1', 'title_3_2', 'cover', 'buttonTitle'],
data() {
return {
}
},
methods: {
change() {
this.$emit('change');
}
}
}
</script>
<style scoped>
.col_box {
text-align: center;
padding: 30px 0px;
}
.title_1 {
line-height: 27px;
color: #62C989;
font-size: 27px;
letter-spacing: 2px;
font-family: 'Times New Roman', Times, serif;
}
.title_2 {
line-height: 27px;
color: #8E9AAF;
font-size: 27px;
letter-spacing: 2px;
font-family: 'Times New Roman', Times, serif;
}
.title_3 {
margin-top: 15px;
font-size: 15px;
letter-spacing: 2px;
font-family: 'Times New Roman', Times, serif;
}
.title_3_1 {
color: #8E9AAF;
}
.title_3_2 {
color: #62C989;
}
.cover {
margin-top: 15px;
width: 150px;
height: 150px;
}
.to_login {
margin-top: 15px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

@ -0,0 +1,92 @@
<template>
<div class="welcome">
<el-card class="background_box">
<BookMark ref="login" :title_1="leftTitle.title_1" :title_2="leftTitle.title_2"
:title_3_1="leftTitle.title_3_1" :title_3_2="leftTitle.title_3_2" :cover="leftTitle.cover"
:buttonTitle="leftTitle.buttonTitle" @change=toLogin>
</BookMark>
<BookMark ref="register" :title_1="rightTitle.title_1" :title_2="rightTitle.title_2"
:title_3_1="rightTitle.title_3_1" :title_3_2="rightTitle.title_3_2" :cover="rightTitle.cover"
:buttonTitle="rightTitle.buttonTitle" @change=toRegister>
</BookMark>
<Login ref="loginRef"></Login>
<Register ref="registerRef" @toLogin="toLogin"></Register>
</el-card>
</div>
</template>
<script>
import BookMark from './bookmark'
import Login from './login'
import Register from './register'
export default {
name: 'Welcome',
components: {
BookMark,
Login,
Register
},
data() {
return {
leftTitle: {
title_1: 'BOOKS&',
title_2: 'LITERARY',
title_3_1: 'Pick your perfect ',
title_3_2: 'literature',
cover: require('./images/cover1.jpg'),
buttonTitle: 'SIGN IN'
},
rightTitle: {
title_1: 'BOOKS&',
title_2: 'LITERARY',
title_3_1: 'Pick your perfect ',
title_3_2: 'literature',
cover: require('./images/cover2.jpg'),
buttonTitle: 'SIGN UP'
},
}
},
methods: {
toLogin() {
this.$refs.loginRef.show = true
this.$refs.registerRef.show = false
setTimeout(() => {
this.$refs.loginRef.moveStyle = {
transform: 'translateX(0px)'
}
this.$refs.registerRef.moveStyle = {
transform: 'translateX(0px)'
}
}, 10)
},
toRegister() {
this.$refs.loginRef.show = false
this.$refs.registerRef.show = true
setTimeout(() => {
this.$refs.loginRef.moveStyle = {
transform: 'translateX(300px)'
}
this.$refs.registerRef.moveStyle = {
transform: 'translateX(300px)'
}
}, 10)
},
}
}
</script>
<style scoped>
.welcome {
height: 100%;
background: url(./images/beijing.png);
}
.background_box {
width: 660px;
height: 420px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
overflow: visible;
}
</style>

@ -0,0 +1,239 @@
<template>
<div class="login_background" v-show="show" :style="moveStyle">
<div class="login_box">
<div class="title">登录</div>
<el-form
class="login_form"
:model="loginForm"
:rules="loginRules"
ref="loginRef"
>
<el-form-item prop="username">
<el-input
placeholder="用户名"
v-model="loginForm.username"
prefix-icon="el-icon-user"
clearable
>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
clearable
v-model="loginForm.password"
prefix-icon="el-icon-lock"
placeholder="密码"
>
</el-input>
</el-form-item>
<el-form-item prop="userType">
<el-radio-group v-model="loginForm.userType">
<el-radio label="user">用户</el-radio>
<el-radio label="seller">商家</el-radio>
<el-radio label="manager">管理员</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="login_button">
<el-button type="success" round plain @click="handleLogin"></el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
export default {
name: 'LoginPage',
data() {
return {
loginForm: {
username: '',
password: '',
userType: 'user',
},
loginRules: {
username: [
{
required: true,
message: '请输入用户名',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
},
],
userType: [
{
required: true,
message: '请选择用户类型',
trigger: 'change',
},
],
},
show: true,
moveStyle: {},
};
},
computed: {
...mapState({
username: (state) => state.username,
registerUser: (state) => state.registerUser,
}),
},
methods: {
...mapActions(['login']),
async handleLogin() {
this.$refs.loginRef.validate(async valid => {
if (!valid) return;
const currentUserType = this.loginForm.userType;
if (currentUserType === "seller") {
if (
this.loginForm.username !== "seller" ||
this.loginForm.password !== "123456"
) {
this.$message({
type: "error",
message: "用户名或密码错误",
});
return;
}
} else if (currentUserType === "manager") {
if (
this.loginForm.username !== "manager" ||
this.loginForm.password !== "123456"
) {
this.$message({
type: "error",
message: "用户名或密码错误",
});
return;
}
} else if (currentUserType === "user") {
const targetUser = this.registerUser.find((item) => {
return item.username === this.loginForm.username;
});
if (!targetUser) {
this.$message({
type: "error",
message: "该用户未注册",
});
return;
}
const target = this.registerUser.find((item) => {
return (
item.username === this.loginForm.username &&
item.password === this.loginForm.password
);
});
if (!target) {
this.$message({
type: "error",
message: "用户名或密码错误",
});
return;
}
}
try {
const response = await fetch('http://localhost:3000/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.loginForm),
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
const user = data.user;
if (user && user.id && user.username) {
this.login(user);
console.log('User logged in:', this.$store.state.user);
this.$refs['loginRef'].resetFields();
const routeMap = {
user: '/user/home',
seller: '/seller/home',
manager: '/manager/home',
};
const targetRoute = routeMap[currentUserType] || '/user/home';
this.$router.push(targetRoute);
this.$message({
type: 'success',
message: '登录成功',
});
} else {
this.$message({
type: 'error',
message: '用户信息不完整',
});
}
} else {
this.$message({
type: 'error',
message: data.error,
});
}
} catch (error) {
this.$message({
type: 'error',
message: '登录失败',
});
}
});
},
},
};
</script>
<style scoped>
.login_background {
background-color: #89d8a7;
border-radius: 7px;
width: 320px;
height: 500px;
position: fixed;
margin-top: -60px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: all 0.5s;
}
.login_box {
padding: 101px 0px;
}
.title {
text-align: center;
font-size: 30px;
font-weight: bold;
color: #f6f6f6;
letter-spacing: 10px;
padding-bottom: 40px;
}
.login_form {
padding: 0px 30px;
box-sizing: border-box;
}
.login_button {
padding-top: 30px;
display: flex;
justify-content: space-between;
}
.el-radio-group {
display: flex;
justify-content: center;
gap: 20px;
}
</style>

@ -0,0 +1,160 @@
<template>
<div class="register_background" v-show="show" :style="moveStyle">
<div class="register_box">
<div class="title">注册</div>
<el-form class="register_form" :model="registerForm" :rules="registerRules" ref="registerRef">
<el-form-item prop="username">
<el-input placeholder="用户名" v-model="registerForm.username" prefix-icon="el-icon-user" clearable>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input show-password clearable v-model="registerForm.password" prefix-icon="el-icon-lock"
placeholder="密码">
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input show-password clearable v-model="registerForm.confirmPassword" prefix-icon="el-icon-lock"
placeholder="确认密码">
</el-input>
</el-form-item>
<el-form-item class="register_button">
<el-button type="success" round plain @click="register()"></el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: 'RegisterPage',
data() {
var confirmPassword = (rule, value, callback) => {
if (value !== this.registerForm.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return {
registerForm: {
username: '',
password: '',
confirmPassword: ''
},
registerRules: {
username: [{
required: true,
message: '请输入用户名',
trigger: 'blur'
}],
password: [{
required: true,
message: '请输入密码',
trigger: 'blur'
}],
confirmPassword: [{
required: true,
message: '请再次输入密码',
trigger: 'blur'
},
{
validator: confirmPassword,
trigger: 'blur'
}
]
},
show: false,
moveStyle: {}
}
},
computed: {
...mapState(["registerUser"]),
},
methods: {
async register() {
this.$refs.registerRef.validate(async valid => {
if (!valid) return;
const target = this.registerUser.find((item) => {
return item.username === this.registerForm.username;
});
if (target) {
this.$message({
type: 'error',
message: '该用户已注册',
});
return;
}
try {
const response = await fetch('http://localhost:3000/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.registerForm)
});
const data = await response.json();
if (response.ok) {
this.$store.commit("REGISTER", this.registerForm);
this.$message({
type: 'success',
message: '注册成功'
});
this.$refs['registerRef'].resetFields();
this.$emit('toLogin');
} else {
this.$message({
type: 'error',
message: data.error
});
}
} catch (error) {
this.$message({
type: 'error',
message: '注册失败'
});
}
});
}
}
}
</script>
<style scoped>
.register_background {
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
background-color: #89D8A7;
border-radius: 7px;
width: 320px;
height: 500px;
position: fixed;
margin-top: -60px;
transition: all 0.5s;
}
.title {
text-align: center;
font-size: 30px;
font-weight: bold;
color: #F6F6F6;
letter-spacing: 10px;
padding-bottom: 40px;
}
.register_box {
padding: 72px 0px;
}
.register_form {
padding: 0px 30px;
box-sizing: border-box;
}
.register_button {
padding-top: 30px;
display: flex;
justify-content: space-between;
}
</style>

@ -0,0 +1,53 @@
import Vue from 'vue'
import {
Button,
Row,
Col,
Form,
FormItem,
Input,
Message,
Table,
TableColumn,
Pagination,
Card,
Menu,
MenuItem,
Dropdown,
DropdownMenu,
DropdownItem,
Breadcrumb,
BreadcrumbItem,
Tag,
Backtop,
Divider,
Tabs,
TabPane,
Tooltip,
} from 'element-ui'
Vue.use(Button)
Vue.use(Row)
Vue.use(Col)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Pagination)
Vue.use(Card)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Dropdown)
Vue.use(DropdownMenu)
Vue.use(DropdownItem)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Tag)
Vue.use(Backtop)
Vue.use(Divider)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.use(Tooltip)
Vue.prototype.$message = Message

@ -0,0 +1,20 @@
// router/cart.js
const express = require('express');
const router = express.Router();
const cartController = require('../controllers/cartController'); // 引入控制器
console.log('进入添加购物车路由配置');
// 添加商品到购物车的 POST 请求
router.post('/add', cartController.addToCart);
// 更新购物车商品数量的 POST 请求
router.post('/updateQuantity', cartController.updateQuantity);
// 删除购物车商品的 POST 请求
router.post('/delete', cartController.deleteCartItem);
// 支付购物车的 POST 请求
router.post('/pay', cartController.payCart);
module.exports = router;

@ -0,0 +1,169 @@
// router/index.js
import VueRouter from "vue-router";
import Vue from "vue";
import UserLayout from "@/components/userLayout/index.vue";
import SellerLayout from "@/components/sellerLayout/index.vue";
import ManagerLayout from "@/components/managerLayout/index.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
redirect: "/welcome",
},
{
path: "/welcome",
name: "Welcome",
meta: { show: true },
component: () => import("@/pages/welcome"),
},
{
path: "/user",
component: UserLayout,
redirect: "/user/home",
children: [
{
path: "home",
name: "UserHome",
component: () => import("@/pages/home"),
meta: { requiresAuth: true },
},
{
path: "book",
name: "UserBook",
component: () => import("@/pages/book"),
meta: { requiresAuth: true },
},
{
path: "shoppingCart",
name: "UserShoppingCart",
component: () => import("@/pages/shoppingCart"),
meta: { requiresAuth: true },
},
],
},
{
path: "/seller",
component: SellerLayout,
redirect: "/seller/home",
children: [
{
path: "home",
name: "SellerHome",
component: () => import("@/views/index.vue"),
meta: { requiresAuth: true },
},
{
path: "book-management",
name: "BookManagement",
component: () => import("@/views/BookManagement.vue"),
meta: { requiresAuth: true },
},
{
path: "order-management",
name: "OrderManagement",
component: () => import("@/views/OrderManagement.vue"),
meta: { requiresAuth: true },
},
{
path: "customer-feedback",
name: "CustomerFeedback",
component: () => import("@/views/CustomerFeedback.vue"),
meta: { requiresAuth: true },
},
{
path: "promotion-management",
name: "PromotionManagement",
component: () => import("@/views/PromotionManagement.vue"),
meta: { requiresAuth: true },
},
{
path: "data-analytics",
name: "DataAnalytics",
component: () => import("@/views/DataAnalytics.vue"),
meta: { requiresAuth: true },
},
{
path: "merchant-info",
name: "MerchantInfo",
component: () => import("@/views/MerchantInfo.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/manager",
component: ManagerLayout,
redirect: "/manager/home",
children: [
{
path: "home",
name: "ManagerHome",
component: () => import("@/pages/manager/home/ManagerHome.vue"),
meta: { requiresAuth: true },
},
{
path: "myInfo",
name: "AdminMyInfo",
component: () => import("@/pages/manager/AdminMyInfo.vue"),
meta: { requiresAuth: true },
},
{
path: "accopass",
name: "AdminAccoPass",
component: () => import("@/pages/manager/AdminAccopass.vue"),
meta: { requiresAuth: true },
},
{
path: "merchant",
name: "AdminMerchant",
component: () => import("@/pages/manager/AdminMerchant.vue"),
meta: { requiresAuth: true },
},
{
path: "client",
name: "AdminClient",
component: () => import("@/pages/manager/AdminClient.vue"),
meta: { requiresAuth: true },
},
{
path: "bookinfo",
name: "AdminBookInfo",
component: () => import("@/pages/manager/AdminBookinfo.vue"),
meta: { requiresAuth: true },
},
{
path: "books",
name: "AdminBooks",
component: () => import("@/pages/manager/AdminBooks.vue"),
meta: { requiresAuth: true },
},
{
path: "orders",
name: "AdminOrders",
component: () => import("@/pages/manager/AdminOrders.vue"),
meta: { requiresAuth: true },
},
],
},
];
const router = new VueRouter({
routes,
});
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token') !== null;
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated) {
next('/welcome'); // 未登录时重定向到欢迎页
} else {
next();
}
} else {
next();
}
});
export default router;

@ -0,0 +1,219 @@
let bookList = [{
book_id: 1,
cover: require('../assets/images/1.png'),
bookName: '三体',
author: '刘慈欣',
price: 50,
type: '文学',
description: '《三体》的幻想源于经典物理中的三体问题,即三个体积质量相当的天体,在远离其它星系以致其它星系的引力影响可忽略不计的情况下,三个天体在互相引力 的作用下互相围绕运行,其运行轨迹将产生不可预测的混沌。很多年来,数学家们一直希望能建立三体乃至多体问题的数学模型,可遗憾的是,得到的结果仅仅是三 体问题在非限制条件下的不可解。'
}, {
book_id: 2,
cover: require('../assets/images/2.png'),
bookName: '云边有个小卖部',
author: '张嘉佳',
price: 28.7,
type: '文学',
description: '1. 张嘉佳全新作品。畅销千万现象级作品《从你的全世界路过》后暌违五年写给离开我们的人写给陪伴我们的人写给每个人心中的山和海。2. 精美手绘插画。陪你度过云边镇的日与夜。3. 惊喜彩蛋。张嘉佳作词单曲《刘十三》带你来到书中现场。4. 希望和悲伤,都是一缕光。总有一天,我们会再相遇。'
}, {
book_id: 3,
cover: require('../assets/images/3.png'),
bookName: '我们仨',
author: '杨绛',
price: 29.8,
type: '文学',
description: '《我们仨》是钱钟书夫人杨绛撰写的家庭生活回忆录。1998年钱钟书逝世而他和杨绛专享的女儿钱瑗已于此前1997年先他们而去。在人生的伴侣离去四年后杨绛在92岁高龄的时候用心记述了他们这个特殊家庭63年的风风雨雨、点点滴滴结成回忆录《我们仨》。'
}, {
book_id: 4,
cover: require('../assets/images/4.png'),
bookName: '教养在生活的细节里',
author: '洪兰',
price: 39,
type: '教育',
description: '《教养在生活的细节里》洪兰与蔡颖卿对父母、老师关注的27个典型问题进行了温柔而智慧的对谈我们从中可以看到教养不是方法论而是生活本身大人与孩子相处的分分秒秒里都是对孩子潜移默化的教育。关注细节用平等、尊重、温柔、爱、智慧与孩子相处彼此关照共同成长。'
}, {
book_id: 5,
cover: require('../assets/images/5.png'),
bookName: '教育常识',
author: '李政涛',
price: 36,
type: '教育',
description: '教育常识的原点,就是人性常识,也就是对人的天性的认识。衡量一位教师和教育理论研究者是否进入了智慧的境界,不仅要看他有没有与教育有关的实践智慧和思想智慧,更要看他有没有直捣人心、洞察人性的智慧,有没有形成自己独特的对人性的理解。'
}, {
book_id: 6,
cover: require('../assets/images/6.png'),
bookName: '博赞儿童思维导图',
author: '东尼·博赞',
price: 60,
type: '教育',
description: ' 描述孩子成长过程中、在理想状态下会喜欢的东西,帮助孩子完全开发自己思维、身体和精神的潜能。只要给予孩子们合适的激励和正确的培养环境,每一个孩子都可以成为非凡的天才。而重要的是,他们一定会成长为一个内心丰富而幸福的人。这是一本能够带给所有孩子成功与幸福的书,也是给予家长的教育指南!'
}, {
book_id: 7,
cover: require('../assets/images/7.png'),
bookName: '花卉动物速写描摹本',
author: '孔祥彦',
price: 50,
type: '绘画',
description: '零基础画画入门新手自学教程书籍线描花卉临摹手绘初学者学绘画教材铅笔画素描速写画稿美院名师范画,具有各种花卉图片供给临摹和观赏。'
}, {
book_id: 8,
cover: require('../assets/images/8.png'),
bookName: 'A Bigger Book',
author: 'Hockney',
price: 95,
type: '绘画',
description: '这本SUMO出版的《A Bigger Book》是对David Hockney艺术的一次宏大的视觉考察。在这本书中Hockney回顾了他60多年的作品从他十几岁在艺术学校的日子到他最近大量的肖像画、iPad绘画和约克郡风景画系列。Hockney的作品从来没有以如此大的范围、如此大的投入、如此惊人的规模出版。'
}, {
book_id: 9,
cover: require('../assets/images/9.png'),
bookName: '绘画之道',
author: 'Susan Herbert',
price: 91,
type: '绘画',
description: '猫咪以沉思沉思的表情完美地重塑了印象派大师的伟大作品无论是漫步在克劳德·莫奈的野生罂粟丛中坐在玛丽·卡萨特的歌剧中还是在奥古斯特·雷诺阿的Bougival享受周日舞蹈。他们可能会陷入沉闷绝望低俗或母亲资产阶级或知识分子的迷人或沉迷但它们总是印象派的猫自发地毫无准备地被照相机抓住。印象派猫是艺术猫系列中的绝妙补充一定会吸引任何猫迷。'
}, {
book_id: 10,
cover: require('../assets/images/10.png'),
bookName: '摄影的艺术',
author: '杨建飞',
price: 49.9,
type: '摄影',
description: '这部著作被认为是最具可读性、最易理解和最全面的摄影教科书。现在它已经更新了数码摄影的内容。这本书有超过100幅黑白和彩色摄影的插图还有各种图表可以帮助初学者、进阶者乃至高级摄影师找到自己的摄影表达方式。Bruce没有故作高深也没有以高人一等的姿态进行论述而是以清晰、简明、易懂的方式介绍了数码和传统摄影的创作技术。他还超越了技术层面深入地讨论了有关摄影的哲学、表达方式以及创意的内容这往往是其他摄影书避而不谈的内容。'
}, {
book_id: 11,
cover: require('../assets/images/11.png'),
bookName: '摄影基础教程',
author: '杨建飞',
price: 108,
type: '摄影',
description: '数码单反手机摄影书 人物风景静物艺术旅行婚礼儿童成长记录摄影构图取景光线后期处理摄影学院级入门自学教材,本教材着重于艺术角度的阐释。此外,随着近年来 数码摄影的崛起,本教材也顺应了这一趋势,偏重在数码摄影方面,偶尔涉及传统的胶片摄影,也是为了做比较之用。编写过程中,参考了很多前辈的相关著述,也选取了许多著名摄影师的作品作为图例。'
}, {
book_id: 12,
cover: require('../assets/images/12.png'),
bookName: '今日摄影',
author: '马克·德登',
price: 45.8,
type: '摄影',
description: '作为一名拥有多重学术背景作者系艺术学士艺术史与理论硕士以及摄影研究博士的作家和艺术家马克·德登Mark Durden对摄影写作的兴趣并不仅仅局限于上述某一类的讨论之上 而是以其丰富的知识结构作为依托,将当代艺术摄影、摄影理论和哲学的复杂话题融汇贯通于摄影史的书写之中。《今日摄影》全书以十一个主题章节作为框架,包含了街拍、肖像、风景和纪实摄影等相关主题,重现了摄影在每个类别中的非线性发展过程,同时通过介绍各个摄影类别之间的联系,描述了这一时期重要艺术家和摄影师多样的实践方法和形式。'
}, {
book_id: 13,
cover: require('../assets/images/13.png'),
bookName: '中国通史',
author: '中国通史',
price: 128,
type: '历史',
description: '傅乐成《中国通史》上起旧石器时代下讫1912年清帝退位台湾版还包括《民国的忧患》和《中国的现代化》两章大陆版则无凡六十余万言。文字浅近平易不做繁征博引叙说清晰见解持正数千年中国史事之此伏彼起重要节点前因后果俱在目前。*初由台湾大中国图书公司于1960年出版1968年增订1977年再次增订。自增订以来已出版近四十次在台湾出版的中国通史之中被大家公认为很好的一部作品。'
}, {
book_id: 14,
cover: require('../assets/images/14.png'),
bookName: '春秋战国',
author: '梁启超',
price: 56,
type: '历史',
description: '《春秋战国:争霸九州》内容简介:春秋时代,大国争霸,小国争胜,强者存,弱者亡。善良和邪恶明争暗斗,英雄和美女惺惺相惜。作者提炼古今史料,以写实的手法,生动地描绘了齐桓公、宋襄公、晋文公、秦穆公、楚庄王五位春秋霸主励精图治,广招贤才,争霸天下的雄略,塑造了管仲、鲍叔牙、百里奚、孙叔敖等人满腹谋略,勤政爱民的光辉形象,同时也刻画了卫姬、隗后、弄玉、樊姬等一系列宫廷女性的不同命运……《春秋战国(上卷)争霸九州》以其宏大的结构,深刻的内涵,艺术地再现了这个被视为中国历史上最具活力、最伟大的时代。'
}, {
book_id: 15,
cover: require('../assets/images/15.png'),
bookName: '中国史纲',
author: '张荫麟',
price: 99,
type: '历史',
description: '文辞优美,能给读者带来阅读享受的历史作品:别人写历史都引用史籍,张荫麟却不囿于史籍,他甚至把《诗经》《楚辞》《论语》词句用的神出鬼没,给一部严肃的学术作品,披上了华丽丽的文学外衣,使得这部《中国史纲》华彩奕奕。'
}, {
book_id: 16,
cover: require('../assets/images/16.png'),
bookName: '格局',
author: '何权峰',
price: 36,
type: '哲学',
description: '格局指一个人的眼界、胸襟、胆识等心理要素的内在布局。再大的烙饼也大不过烙它的锅。只会盯着树皮里的虫子不放的鸟儿是不可能飞到白云之上的,只有眼里和心中装满了山河天地的雄鹰才能自由地在天地间翱翔!'
}, {
book_id: 17,
cover: require('../assets/images/17.png'),
bookName: '了凡四训',
author: '袁了凡',
price: 24,
type: '哲学',
description: '《了凡四训》为明代袁了凡所著训子善书,阐明“命自我立,福自己求”的思想,指出一切祸福休咎皆自当人掌握,行善则积福,作恶则招祸;并现身说法,结合儒释道三家思想以自身经历体会阐明此理,鼓励向善立身,慎独立品,自求多福,远避祸殃。该书自明末以来流行甚广,影响较大,此白话绘图本的出版当有助于阅读了解,于个人品德修养与世道人心改善或许不无小补。'
}, {
book_id: 18,
cover: require('../assets/images/18.png'),
bookName: '宇宙',
author: '卡尔·萨根',
price: 68,
type: '科学',
description: ' 宇宙一直是这个样子,过去如此,将来也如此。对宇宙不够有力的思考唤醒我们对周围的感觉——脊柱传来的一阵刺痛,捕捉到的一个声音,好似来自遥远的记忆,我们常常有一种从高空坠落的微弱感觉。我们知道我们正接近那个最终的秘密。'
}, {
book_id: 19,
cover: require('../assets/images/19.png'),
bookName: '认识星座',
author: '阿斯达',
price: 268,
type: '科学',
description: '星座是占星学中不可或缺的组成部分之一,由天上的恒星组合而成。星座是占星学中不可或缺的组成部分之一,由天上的恒星组合而成。在西方文化中,星座的地位几乎相当于生肖在我国的地位,就像我们利用自己的生辰测算自己一生的运数一样,西方世界则利用星座来测算吉凶。”'
}, {
book_id: 20,
cover: require('../assets/images/20.png'),
bookName: '学会表达',
author: '丁艳丽',
price: 88,
type: '语言',
description: '卡耐基魅力口才与说话技巧提高情商语言能力高情商训练图书籍,不能完全表达言者的本意,要透过语言揣摩本质,也要提高表达能力,能让自己表达得更准确完备。自从明确地提出:“要会表达”。'
}, {
book_id: 21,
cover: require('../assets/images/21.png'),
bookName: '高情商聊天术',
author: '张超',
price: 36.8,
type: '语言',
description: '让你一开口就能说到对方心里去 有礼有节才能说话周全口才说话技巧书籍畅销书 职场生活口才训练书好好说话情商99%的人际问题出在不会说话上,本书教你做一个真正会聊天的人!听说两种能力训练+3大场景模拟对话+10年表达经验锤炼。影响说服沟通谈判谈笑间搞定'
}, {
book_id: 22,
cover: require('../assets/images/22.png'),
bookName: '口才三绝',
author: '歌诗达',
price: 158,
type: '语言',
description: '本书是一剂向大家介绍提高口才能力和说话技巧的良方!主要从口才必备的三个方面来阐述提升口才的技巧:第一篇主要介绍了敏捷的思维是措辞的关键;第二篇主要阐述了赞美的话要说到点子上;第三篇与读者一起分享了不同场景下与不同人沟通、交流、说话的技巧等。 本书口才的基础入手,将科学性与技巧性、理论指导与实际场景融于一体。只要你精心阅读,坚持训练,本书定会逐渐引导你走向捷径,最后轻松带你步入成功口才的殿堂。'
}, {
book_id: 23,
cover: require('../assets/images/23.png'),
bookName: '孙子兵法',
author: '孙武',
price: 49.8,
type: '军事',
description: '《孙子兵法》是中国古代汉族军事文化遗产中的璀璨瑰宝汉族优秀传统文化的重要组成部分其内容博大精深思想精邃富赡逻辑缜密严谨是古代汉族军事思想精华的集中体现。作者为春秋时祖籍齐国乐安的吴国将军孙武。《孙子兵法》被称为镇国之宝在中国被奉为兵家经典。诞生至今已有2500年历史历代都有研究。李世民说“观诸兵书无出孙武”。兵法是谋略谋略不是小花招而是大战略、大智慧。如今孙子兵法已经走向世界。它也被翻译成多种语言在世界军事史上也具有重要的地位。'
}, {
book_id: 24,
cover: require('../assets/images/24.png'),
bookName: '世界战列舰全史',
author: '罗伯特',
price: 68,
type: '军事',
description: '作为曾经辉煌的日不落帝国,英国依靠强大的皇家海军控制着整个海洋。进入铁甲蒸汽时代之后,拥有大口径主炮、厚重装甲和高功率发动机的战列舰成为英国海上力量的象征。在战列舰建造领域,英国不断推出具有跨时代意义的作品,其中包括“君权”级、“无畏”号、“伊丽莎白女王”级等,这些强大的战舰成为引领世界海军发展的风向标。'
}, {
book_id: 25,
cover: require('../assets/images/25.png'),
bookName: '本草纲目',
author: '李时珍',
price: 48,
type: '医药',
description: ' 《本草纲目》是李时珍所著的集中国16世纪前本草学大成的中医典籍属《四库全书》子部医家类。该典籍于明代万历六年定稿万历二十四年在南京正式刊行。《本草纲目》共有52卷载有药物1892种其中载有新药374种收集医方11096个约190万字分为16部、60类。'
}, {
book_id: 26,
cover: require('../assets/images/26.png'),
bookName: '奇效偏方',
author: '李春深',
price: 68,
type: '医药',
description: '古人说用药如用兵,如果把那些出于名医之手和载入各种医典之中的医药方剂视为正规部队就可以把偏方看作是游击队,同样为病人解除痛苦。偏方是原生态,是现代方剂和新生药物取之不尽的源泉,是祖国医药宝库中光彩夺目的明珠。'
}, {
book_id: 27,
cover: require('../assets/images/27.png'),
bookName: '中医急诊学',
author: '姜良铎',
price: 78,
type: '医药',
description: '是新中国成立以来第yi次对中医急诊学科发展进行的全面、系统总结。 《中医急诊学(精)》由上篇总论、下篇各论和附篇三部分组成。首次梳理了中医急诊科学术发展史,系统阐述了中医急诊科基本理论、临证思路和学习方法,介绍急诊科病症的诊疗方法、临床经验和研究新进展'
}, ]
export default bookList

@ -0,0 +1,15 @@
let categoryList = [
'全部分类',
'文学',
'教育',
'绘画',
'摄影',
'历史',
'哲学',
'科学',
'语言',
'军事',
'医药',
]
export default categoryList;

@ -0,0 +1,342 @@
// store/index.js
import Vuex from 'vuex';
import Vue from 'vue';
import VuexPersist from 'vuex-persist';
import bookList from './bookList';
import categoryList from './categoryList';
import { Message } from 'element-ui';
Vue.use(Vuex);
const MUTATION_TYPES = {
LOGIN: 'LOGIN',
LOGOUT: 'LOGOUT',
DETAIL: 'DETAIL',
ADDCART: 'ADDCART',
ADD: 'ADD',
SUB: 'SUB',
DELETE: 'DELETE',
PAY: 'PAY',
CLEAR: 'CLEAR',
SET_USER: 'SET_USER',
REGISTER: 'REGISTER',
ADDBOOK: 'ADDBOOK',
UPDATE_BOOK: 'UPDATE_BOOK',
DELETE_BOOK: 'DELETE_BOOK',
DELETE_USER: 'DELETE_USER'
};
const store = new Vuex.Store({
state: {
username: '',
isAuthenticated: false,
user: null,
bookList: bookList,
categoryList: categoryList,
book: null,
cart: [],
registerUser: []
},
mutations: {
/* [MUTATION_TYPES.REGISTER](state, { username, password }) {
state.registerUser.push({ username, password });
},*/
[MUTATION_TYPES.REGISTER](state, { username, password }) {
// 生成一个新的用户ID
const newId = state.registerUser.length + 1;
// 将新的用户信息添加到 registerUser 数组中
state.registerUser.push({
id: newId,
username: username,
password: password,
email: 'default@example.com', // 静态邮箱
phone: '1234567890', // 静态电话
status: '激活' // 静态状态
});
},
[MUTATION_TYPES.LOGIN](state, { username, user }) {
state.username = username;
state.isAuthenticated = true;
state.user = user;
},
[MUTATION_TYPES.LOGOUT](state) {
state.username = '';
state.isAuthenticated = false;
state.user = null;
},
[MUTATION_TYPES.DETAIL](state, book) {
state.book = book;
},
[MUTATION_TYPES.ADDCART](state, book) {
if (!book || !book.book_id || !book.bookName) {
console.error('传递给ADDCART mutation的book参数不完整或不符合要求');
return;
}
const existingItemIndex = state.cart.findIndex(item => item.book_id === book.book_id);
if (existingItemIndex >= 0) {
Vue.set(state.cart[existingItemIndex], 'number', state.cart[existingItemIndex].number + 1);
} else {
Vue.set(book, 'number', 1);
state.cart.push(book);
}
},
[MUTATION_TYPES.ADD](state, book) {
const item = state.cart.find(item => item.book_id === book.book_id);
if (item) {
Vue.set(item, 'number', item.number + 1);
}
},
[MUTATION_TYPES.SUB](state, book) {
const item = state.cart.find(item => item.book_id === book.book_id);
if (item && item.number > 1) {
Vue.set(item, 'number', item.number - 1);
}
},
UPDATE_CART_ITEM(state, book) {
const itemIndex = state.cart.findIndex(item => item.book_id === book.book_id);
if (itemIndex >= 0) {
Vue.set(state.cart[itemIndex], 'number', book.number);
}
},
[MUTATION_TYPES.DELETE](state, book) {
state.cart = state.cart.filter(item => item.book_id !== book.book_id);
},
[MUTATION_TYPES.PAY](state, selectBook) {
state.cart = state.cart.filter(item => !selectBook.some(selected => selected.book_id === item.book_id));
},
[MUTATION_TYPES.CLEAR](state) {
state.cart = [];
},
[MUTATION_TYPES.SET_USER](state, user) {
state.user = user;
},
[MUTATION_TYPES.ADDBOOK](state, book) {
state.bookList.push(book);
},
[MUTATION_TYPES.UPDATE_BOOK](state, { index, book }) {
Vue.set(state.bookList, index, book);
},
[MUTATION_TYPES.DELETE_BOOK](state, book_id) {
state.bookList = state.bookList.filter(book => book.book_id !== book_id);
},
[MUTATION_TYPES.DELETE_USER](state, userId) {
state.registerUser = state.registerUser.filter(user => user.id !== userId);
}
},
actions: {
login({ commit }, user) {
if (user && user.id && user.username) {
commit(MUTATION_TYPES.SET_USER, user);
commit(MUTATION_TYPES.LOGIN, { username: user.username, user });
} else {
console.error('用户信息不完整');
}
},
logout({ commit }) {
commit(MUTATION_TYPES.LOGOUT);
},
getBookList() {
console.log('当前使用静态的bookList数据如需更新请另行添加逻辑');
},
deleteBookFromCart({ commit }, book) {
const { id: user_id } = this.state.user;
const { book_id } = book;
if (!user_id || !book_id) {
console.error('缺少必要的参数');
return;
}
fetch(`/api/cart/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id, book_id })
})
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('删除失败:', data.error);
Message.error('删除失败');
} else {
commit(MUTATION_TYPES.DELETE, book);
Message.success('删除成功');
}
})
.catch(error => {
console.error('Error deleting book from cart:', error);
Message.error('删除失败');
});
},
addBookToCart({ commit, state }, book) {
return new Promise((resolve, reject) => {
if (!state.isAuthenticated || !state.user || !state.user.id) {
reject(new Error('用户未登录或用户信息异常'));
} else {
const { id: user_id } = state.user;
const { book_id, price } = book;
const quantity = 1;
if (!user_id || !book_id || !quantity || !price) {
console.error('缺少必要的参数');
reject(new Error('缺少必要的参数'));
return;
}
fetch('/api/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id, book_id, quantity, price })//请求体包含用户ID、图书ID、数量和价格信息
})
.then(response => response.json())
.then(data => {
if (data.error) {
reject(new Error(data.error));
} else {
commit(MUTATION_TYPES.ADDCART, book);
resolve();
}
})
.catch(error => {
console.error('Error adding book to cart:', error);
reject(error);
});
}
});
},
clearCart({ commit, state }) {
const { id: user_id } = state.user;
if (!user_id) {
console.error('缺少必要的参数');
return;
}
fetch('/api/cart/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id })
})
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('清空购物车失败:', data.error);
Message.error('清空购物车失败');
} else {
commit(MUTATION_TYPES.CLEAR);
Message.success('购物车已清空');
}
})
.catch(error => {
console.error('Error clearing cart:', error);
Message.error('清空购物车失败');
});
},
pay({ commit, state }, { user_id, selectedBooks, totalPrice }) {
if (!state.user.id || !selectedBooks || !totalPrice) {
console.error('缺少必要的参数:', { user_id, selectedBooks, totalPrice });
Message.error('缺少必要的参数');
return;
}
if (!Array.isArray(selectedBooks) || selectedBooks.length === 0) {
console.error('selectedBooks 必须是一个包含有效 book_id 的数组');
Message.error('selectedBooks 必须是一个包含有效 book_id 的数组');
return;
}
if (typeof totalPrice !== 'number' || isNaN(totalPrice)) {
console.error('totalPrice 必须是一个数字');
Message.error('totalPrice 必须是一个数字');
return;
}
console.log('支付请求参数:', { user_id, selectedBooks, totalPrice });
fetch('/api/cart/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id,
selectedBooks,
totalPrice
})
})
.then(response => {
console.log('支付响应:', response);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('支付响应数据:', data);
if (!data || data.error) {
console.error('支付失败:', data?.error || '无效的响应数据');
Message.error(`支付失败: ${data?.error || '无效的响应数据'}`);
throw new Error('支付失败');
}
commit(MUTATION_TYPES.CLEAR);
console.log('购物车已清空:', state.cart);
Message.success('支付成功');
/*this.$router.push('/user/home');*/
})
.catch(error => {
console.error('Error paying:', error);
Message.error('支付失败');
});
},
register({ commit }, { username, password }) {
commit(MUTATION_TYPES.REGISTER, { username, password });
},
addBook({ commit }, book) {
commit(MUTATION_TYPES.ADDBOOK, book);
},
updateBook({ commit }, { index, book }) {
commit(MUTATION_TYPES.UPDATE_BOOK, { index, book });
},
deleteBook({ commit }, id) {
commit(MUTATION_TYPES.DELETE_BOOK, id);
}
},
plugins: [
new VuexPersist({
storage: window.localStorage,
reducer: state => ({
cart: state.cart,
username: state.username,
isAuthenticated: state.isAuthenticated,
user: state.user,
registerUser: state.registerUser,
bookList: state.bookList // 添加 bookList 状态
}),
filter: (mutation) => {
return [
MUTATION_TYPES.ADDCART, MUTATION_TYPES.DELETE, MUTATION_TYPES.PAY, MUTATION_TYPES.CLEAR,
MUTATION_TYPES.LOGIN, MUTATION_TYPES.LOGOUT, MUTATION_TYPES.SET_USER, MUTATION_TYPES.REGISTER,
MUTATION_TYPES.ADDBOOK, MUTATION_TYPES.UPDATE_BOOK, MUTATION_TYPES.DELETE_BOOK
].includes(mutation.type);
}
}).plugin
]
});
store.subscribe((mutation, state) => {
if (mutation.type === MUTATION_TYPES.SET_USER) {
console.log('用户信息发生变化:', state.user);
}
});
export default store;

@ -0,0 +1,290 @@
<template>
<div class="book-management">
<!-- 页面头部 -->
<header class="header">
<h2>图书管理</h2>
<button @click="showAddForm = true" class="btn add-btn">添加图书</button>
</header>
<!-- 添加图书表单弹窗 -->
<div v-if="showAddForm" class="modal-overlay">
<div class="form-modal">
<h3>{{ editMode ? '编辑图书' : '添加图书' }}</h3>
<form @submit.prevent="handleSubmit">
<label for="bookName">图书标题</label>
<input id="bookName" v-model="bookForm.bookName" type="text" required />
<label for="bookAuthor">作者</label>
<input id="bookAuthor" v-model="bookForm.author" type="text" required />
<label for="bookPrice">价格</label>
<input id="bookPrice" v-model="bookForm.price" type="number" required />
<label for="bookQuantity">库存</label>
<input id="bookQuantity" v-model="bookForm.quantity" type="number" required />
<label for="bookCover">封面图片路径</label>
<input id="bookCover" v-model="bookForm.cover" type="text" required />
<label for="bookType">类型</label>
<input id="bookType" v-model="bookForm.type" type="text" required />
<label for="bookDescription">描述</label>
<textarea id="bookDescription" v-model="bookForm.description" required></textarea>
<div class="form-buttons">
<button type="submit" class="btn confirm-btn">{{ editMode ? '确认修改' : '确认添加' }}</button>
<button type="button" class="btn cancel-btn" @click="closeForm"></button>
</div>
</form>
</div>
</div>
<!-- 图书列表 -->
<div class="book-list">
<div class="book-card" v-for="book in bookList" :key="book.book_id">
<div class="book-details">
<h3>{{ book.bookName }}</h3>
<p>作者: {{ book.author }}</p>
<p>价格: ¥{{ book.price }}</p>
<p>库存: {{ book.quantity }}</p>
<p>类型: {{ book.type }}</p>
<p>描述: {{ book.description }}</p>
</div>
<div class="card-actions">
<button @click="editBook(book)" class="btn edit-btn">编辑</button>
<button @click="deleteBook(book.book_id)" class="btn delete-btn">删除</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'BookManagement',
data() {
return {
showAddForm: false,
editMode: false,
bookForm: {
bookName: '',
author: '',
price: 0,
quantity: 0,
cover: '',
type: '',
description: ''
}
};
},
computed: {
...mapState(['bookList'])
},
methods: {
...mapMutations(['ADDBOOK', 'UPDATE_BOOK', 'DELETE_BOOK']),
closeForm() {
this.showAddForm = false;
this.bookForm = {
bookName: '',
author: '',
price: 0,
quantity: 0,
cover: '',
type: '',
description: ''
};
this.editMode = false;
},
handleSubmit() {
if (this.editMode) {
const index = this.bookList.findIndex(book => book.book_id === this.bookForm.book_id);
this.UPDATE_BOOK({ index, book: { ...this.bookForm } });
} else {
this.ADDBOOK({ ...this.bookForm, book_id: Date.now() });
}
this.closeForm();
},
editBook(book) {
this.bookForm = { ...book };
this.editMode = true;
this.showAddForm = true;
},
deleteBook(book_id) {
this.DELETE_BOOK(book_id);
}
},
mounted() {
console.log('showAddForm:', this.showAddForm);
}
};
</script>
<style scoped>
.book-management {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
font-family: Arial, sans-serif;
background-color: #f4f4f9;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h2 {
font-size: 2.5rem;
margin: 0;
color: #333;
}
.book-list {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
}
.book-card {
background-color: white;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 300px;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.book-card:hover {
transform: translateY(-10px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.book-details h3 {
font-size: 1.5rem;
margin: 0 0 10px;
color: #333;
}
.book-details p {
margin: 5px 0;
color: #666;
}
.card-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.card-actions button {
margin: 0;
padding: 10px 20px;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
border: none;
}
.card-actions button:hover {
opacity: 0.9;
}
.add-btn {
background-color: #1abc9c;
color: white;
}
.edit-btn {
background-color: #3498db;
color: white;
}
.delete-btn {
background-color: #e74c3c;
color: white;
}
.form-modal {
background-color: white;
border-radius: 12px;
padding: 40px;
width: 500px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.form-modal h3 {
font-size: 1.8rem;
margin-bottom: 30px;
color: #333;
}
.form-modal label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-modal input,
.form-modal textarea {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
color: #333;
}
.form-modal textarea {
resize: vertical;
min-height: 100px;
}
.form-buttons {
display: flex;
justify-content: space-between;
}
.form-buttons button {
padding: 12px 24px;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
border: none;
}
.form-buttons button:hover {
opacity: 0.9;
}
.confirm-btn {
background-color: #27ae60;
color: white;
}
.cancel-btn {
background-color: #95a5a6;
color: white;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
</style>

@ -0,0 +1,200 @@
<template>
<div class="customer-feedback">
<header class="header">
<h1>客户反馈</h1>
<p>查看客户的反馈内容并及时处理</p>
</header>
<div class="toolbar">
<input
type="text"
v-model="searchQuery"
class="search-bar"
placeholder="搜索反馈..."
/>
</div>
<div v-if="filteredFeedbacks.length > 0" class="feedback-list">
<div
class="feedback-card"
v-for="feedback in filteredFeedbacks"
:key="feedback.id"
>
<div class="feedback-info">
<h3>反馈人: {{ feedback.name }}</h3>
<p>反馈内容: {{ feedback.content }}</p>
</div>
<div class="card-actions">
<button class="btn delete-btn" @click="deleteFeedback(feedback.id)"></button>
</div>
</div>
</div>
<div v-else class="empty-state">
<p>没有找到匹配的反馈</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
//
const feedbacks = ref([]);
const searchQuery = ref('');
//
const loadFeedbackData = () => {
feedbacks.value = [
{ id: 1, name: '张三', content: '非常满意!服务很周到。' },
{ id: 2, name: '李四', content: '产品质量不错,但物流有点慢。' },
{ id: 3, name: '王五', content: '客服态度很好,问题解决得很快。' },
];
};
//
const filteredFeedbacks = computed(() =>
feedbacks.value.filter(
(feedback) =>
feedback.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
feedback.content.toLowerCase().includes(searchQuery.value.toLowerCase())
)
);
//
const deleteFeedback = (id) => {
feedbacks.value = feedbacks.value.filter((feedback) => feedback.id !== id);
};
//
onMounted(() => {
loadFeedbackData();
});
</script>
<style scoped>
/* 全局背景和字体设置 */
body {
font-family: 'Roboto', Arial, sans-serif;
background: linear-gradient(135deg, #f0f8ff, #e0f7fa);
margin: 0;
color: #333;
}
.customer-feedback {
padding: 20px;
max-width: 900px;
margin: 40px auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
}
/* 页头样式 */
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
color: #007bff;
margin-bottom: 10px;
letter-spacing: 1px;
}
.header p {
font-size: 1rem;
color: #6c757d;
}
/* 工具栏样式 */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.search-bar {
flex: 1;
padding: 10px 15px;
font-size: 1rem;
border: 1px solid #ddd;
border-radius: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.search-bar:focus {
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
border-color: #007bff;
outline: none;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 30px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
/* 反馈列表样式 */
.feedback-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.feedback-card {
background: #ffffff;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feedback-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.feedback-info h3 {
margin: 0;
font-size: 1.2rem;
color: #343a40;
margin-bottom: 10px;
}
.feedback-info p {
margin: 0;
color: #6c757d;
}
.card-actions {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
.delete-btn {
background-color: #e74c3c;
color: white;
font-weight: bold;
}
.delete-btn:hover {
background-color: #c0392b;
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.2);
}
/* 空状态 */
.empty-state {
text-align: center;
color: #6c757d;
font-size: 1.2rem;
margin-top: 30px;
}
</style>

@ -0,0 +1,218 @@
<template>
<div class="data-analytics">
<header class="header">
<h2>数据分析</h2>
<p>实时监控订单概况帮助商家做出明智决策</p>
</header>
<div class="analytics-summary">
<div class="summary-card">
<h3>订单总数</h3>
<p class="value">{{ totalOrders }}</p>
</div>
<div class="summary-card">
<h3>订单总金额</h3>
<p class="value">¥{{ totalAmount.toFixed(2) }}</p>
</div>
</div>
<!-- 订单状态分布图 -->
<div class="order-status">
<h3>订单状态分布</h3>
<div class="status-cards">
<div v-for="(count, status) in orderStatusCounts" :key="status" class="status-card">
<div class="status-name">{{ status }}</div>
<div class="status-count">{{ count }}</div>
</div>
</div>
</div>
<!-- 订单趋势图 -->
<div class="order-trends">
<h3>订单趋势</h3>
<!-- Chart.js 渲染区域 -->
<canvas id="orderTrendsChart" width="400" height="200"></canvas>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { Chart } from 'chart.js';
// 使 API
const orders = ref([
{ id: 1, status: '待发货', totalPrice: 150, date: '2024-01-01' },
{ id: 2, status: '已发货', totalPrice: 300, date: '2024-01-02' },
{ id: 3, status: '待发货', totalPrice: 450, date: '2024-01-03' },
{ id: 4, status: '已完成', totalPrice: 200, date: '2024-01-04' },
{ id: 5, status: '已发货', totalPrice: 350, date: '2024-01-05' },
{ id: 6, status: '已取消', totalPrice: 100, date: '2024-01-06' },
]);
//
const totalOrders = computed(() => orders.value.length);
//
const totalAmount = computed(() => orders.value.reduce((sum, order) => sum + order.totalPrice, 0));
//
const orderStatusCounts = computed(() => {
return orders.value.reduce((counts, order) => {
counts[order.status] = (counts[order.status] || 0) + 1;
return counts;
}, {});
});
//
const orderTrendsData = computed(() => {
const dates = orders.value.map(order => order.date);
const amounts = orders.value.map(order => order.totalPrice);
return { labels: dates, data: amounts };
});
// Chart.js
onMounted(() => {
const ctx = document.getElementById('orderTrendsChart').getContext('2d');
new Chart(ctx, {
type: 'line', // 线
data: {
labels: orderTrendsData.value.labels,
datasets: [{
label: '订单总金额',
data: orderTrendsData.value.data,
borderColor: '#27ae60', // 线
backgroundColor: 'rgba(39, 174, 96, 0.2)', // 线
fill: true, //
tension: 0.3, //
}]
},
options: {
responsive: true,
scales: {
x: {
title: {
display: true,
text: '日期'
}
},
y: {
title: {
display: true,
text: '金额 (¥)'
},
beginAtZero: true
}
}
}
});
});
</script>
<style scoped>
/* 数据分析页面样式 */
.data-analytics {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: 'Arial', sans-serif;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h2 {
font-size: 2rem;
color: #2c3e50;
margin: 0;
}
.header p {
font-size: 1rem;
color: #7f8c8d;
margin-top: 10px;
}
.analytics-summary {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
}
.summary-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
width: 45%;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.summary-card h3 {
font-size: 1.2rem;
color: #34495e;
margin-bottom: 10px;
}
.summary-card .value {
font-size: 2rem;
color: #27ae60;
}
.order-status {
margin-bottom: 40px;
}
.order-status h3 {
font-size: 1.5rem;
color: #34495e;
margin-bottom: 20px;
}
.status-cards {
display: flex;
justify-content: space-between;
gap: 20px;
}
.status-card {
background-color: #fff;
border-radius: 8px;
padding: 15px;
width: 20%;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.status-name {
font-size: 1.1rem;
color: #34495e;
margin-bottom: 10px;
}
.status-count {
font-size: 1.5rem;
color: #2980b9;
}
.order-trends {
margin-top: 40px;
}
.order-trends h3 {
font-size: 1.5rem;
color: #34495e;
margin-bottom: 20px;
}
canvas {
width: 100%;
height: 300px;
max-width: 800px;
margin: 0 auto;
border-radius: 8px;
}
</style>

@ -0,0 +1,213 @@
<template>
<div id="app">
<!-- 商家信息卡片 -->
<div class="merchant-info">
<h1>商家信息</h1>
<p class="info-item"><strong>名称:</strong> {{ merchantInfo.name }}</p>
<p class="info-item"><strong>地址:</strong> {{ merchantInfo.address }}</p>
<p class="info-item"><strong>电话:</strong> {{ merchantInfo.phone }}</p>
<p class="info-item"><strong>邮箱:</strong> {{ merchantInfo.email }}</p>
<button @click="showUpdateForm = true" class="update-button">更新您的信息</button>
<!-- 弹出更新表单 -->
<div v-if="showUpdateForm" class="modal-overlay">
<div class="update-form-modal">
<form @submit.prevent="updateMerchantInfo">
<label for="name">名称</label>
<input id="name" v-model="updateForm.name" type="text" required placeholder="请输入商家名称" />
<label for="address">地址</label>
<input id="address" v-model="updateForm.address" type="text" required placeholder="请输入商家地址" />
<label for="phone">电话</label>
<input id="phone" v-model="updateForm.phone" type="text" required placeholder="请输入商家电话" pattern="^[0-9]{3}-[0-9]{8}$" />
<label for="email">邮箱</label>
<input id="email" v-model="updateForm.email" type="email" required placeholder="请输入商家邮箱" />
<div class="form-buttons">
<button type="submit" :disabled="!isFormValid" class="confirm-btn">确认更新</button>
<button type="button" @click="showUpdateForm = false" class="cancel-btn">取消</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
//
const merchantInfo = ref({
name: "商家001",
address: "福建省福州市",
phone: "123-45678901",
email: "example@example.com",
});
//
const showUpdateForm = ref(false);
//
const updateForm = ref({
name: merchantInfo.value.name,
address: merchantInfo.value.address,
phone: merchantInfo.value.phone,
email: merchantInfo.value.email,
});
//
const isFormValid = computed(() => {
return updateForm.value.name && updateForm.value.address && updateForm.value.phone && updateForm.value.email;
});
//
const updateMerchantInfo = () => {
merchantInfo.value = { ...updateForm.value };
showUpdateForm.value = false;
};
</script>
<style scoped>
#app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1abc9c, #fff); /* 渐变背景 */
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-start; /* 向上对齐 */
align-items: center;
padding: 40px 20px;
}
.merchant-info {
padding: 40px;
background-color: #fff;
border-radius: 15px;
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
width: 450px;
text-align: left;
margin-top: 50px; /* 卡片稍微偏上 */
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.merchant-info:hover {
transform: translateY(-5px); /* 增加悬浮效果 */
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.15);
}
.merchant-info h1 {
color: #2c3e50;
font-size: 30px;
margin-bottom: 20px;
text-align: center;
}
.info-item {
font-size: 18px;
margin: 10px 0;
color: #34495e;
}
.update-button {
margin-top: 20px;
padding: 14px 25px;
background-color: #1abc9c;
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s;
}
.update-button:hover {
background-color: #16a085;
}
.update-button:active {
transform: scale(0.95);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.update-form-modal {
background-color: #fff;
padding: 40px;
border-radius: 12px;
width: 480px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.update-form-modal label {
font-size: 16px;
color: #2c3e50;
margin-bottom: 5px;
display: block;
}
.update-form-modal input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.update-form-modal input:focus {
border-color: #1abc9c;
outline: none;
}
.form-buttons {
display: flex;
justify-content: space-between;
}
.confirm-btn {
padding: 12px 25px;
background-color: #1abc9c;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s;
}
.confirm-btn:hover {
background-color: #16a085;
}
.confirm-btn:active {
transform: scale(0.95);
}
.cancel-btn {
padding: 12px 25px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s;
}
.cancel-btn:hover {
background-color: #c0392b;
}
.cancel-btn:active {
transform: scale(0.95);
}
</style>

@ -0,0 +1,274 @@
<template>
<div class="order-management">
<header>
<h2>订单管理</h2>
</header>
<!-- 订单列表 -->
<div class="order-list">
<div class="order-card" v-for="order in orders" :key="order.id">
<div class="order-details">
<h3>订单ID: {{ order.id }}</h3>
<p><strong>客户:</strong> {{ order.customerName }}</p>
<p><strong>总价:</strong> ¥{{ order.totalPrice }}</p>
<!-- 发货人信息 -->
<p><strong>发货人:</strong> {{ order.senderName }}</p>
<!-- 发货人电话 -->
<p><strong>发货人电话:</strong> {{ order.senderPhone }}</p>
<!-- 发货地址 -->
<p><strong>发货地址:</strong> {{ order.shippingAddress }}</p>
<!-- 订单状态 -->
<p><strong>订单状态:</strong> {{ order.status }}</p>
</div>
<div class="card-actions">
<button @click="editOrder(order)" class="btn edit-btn">编辑</button>
<button @click="deleteOrder(order.id)" class="btn delete-btn">删除</button>
</div>
</div>
</div>
<!-- 编辑订单模态框 -->
<div v-if="editingOrder" class="modal-overlay">
<div class="modal-content">
<h3>编辑订单 {{ editingOrder.id }}</h3>
<!-- 发货人信息 -->
<label for="senderName">发货人</label>
<input v-model="editingOrder.senderName" type="text" id="senderName" placeholder="发货人姓名" />
<!-- 发货人电话 -->
<label for="senderPhone">发货人电话</label>
<input v-model="editingOrder.senderPhone" type="text" id="senderPhone" placeholder="发货人电话" />
<!-- 发货地址 -->
<label for="shippingAddress">发货地址</label>
<input v-model="editingOrder.shippingAddress" type="text" id="shippingAddress" placeholder="发货地址" />
<!-- 订单状态 -->
<label for="status">订单状态</label>
<select v-model="editingOrder.status" id="status">
<option v-for="status in orderStatuses" :key="status" :value="status">{{ status }}</option>
</select>
<!-- 保存与取消按钮 -->
<div class="modal-buttons">
<button @click="saveOrder" class="btn save-btn">保存</button>
<button @click="cancelEdit" class="btn cancel-btn">取消</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
//
const orderStatuses = ['待发货', '已发货', '已完成', '已取消'];
//
const orders = ref([
{ id: 1, customerName: '张三', totalPrice: 150, senderName: '李四', senderPhone: '13800000001', shippingAddress: '北京市朝阳区', status: '待发货' },
{ id: 2, customerName: '王五', totalPrice: 300, senderName: '赵六', senderPhone: '13900000002', shippingAddress: '上海市浦东新区', status: '已发货' },
]);
//
const editingOrder = ref(null);
//
const editOrder = (order) => {
editingOrder.value = { ...order }; //
};
//
const saveOrder = () => {
const index = orders.value.findIndex(order => order.id === editingOrder.value.id);
if (index !== -1) {
orders.value[index] = { ...editingOrder.value }; //
}
cancelEdit(); //
};
//
const cancelEdit = () => {
editingOrder.value = null; //
};
//
const deleteOrder = (id) => {
orders.value = orders.value.filter(order => order.id !== id);
};
</script>
<style scoped>
/* 样式 */
.order-management {
max-width: 1100px;
margin: 0 auto;
padding: 20px;
font-family: 'Roboto', sans-serif;
}
header {
text-align: center;
margin-bottom: 20px;
font-size: 1.8rem;
font-weight: bold;
color: #34495e;
}
.order-list {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
}
.order-card {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 280px;
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.order-card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.2);
}
.order-details h3 {
font-size: 1.4rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 10px;
}
.order-details p {
color: #7f8c8d;
font-size: 1rem;
line-height: 1.5;
}
.card-actions {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
.card-actions .btn {
padding: 12px 20px;
font-size: 1.1rem;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.3s ease;
width: 48%;
box-sizing: border-box;
}
.edit-btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
}
.delete-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.card-actions .btn:hover {
transform: translateY(-2px);
}
.card-actions .btn:active {
transform: translateY(0);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
opacity: 0;
animation: fadeIn 0.3s forwards;
}
.modal-content {
background-color: #ffffff;
border-radius: 12px;
padding: 30px;
width: 450px;
max-width: 80%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.modal-content h3 {
color: #34495e;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 20px;
}
input, select {
width: 100%;
padding: 12px;
margin: 12px 0;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 1rem;
}
.modal-buttons {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
.save-btn {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
border-radius: 8px;
padding: 12px 20px;
font-size: 1.1rem;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.cancel-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
border-radius: 8px;
padding: 12px 20px;
font-size: 1.1rem;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.modal-buttons .btn:hover {
transform: translateY(-2px);
}
.modal-buttons .btn:active {
transform: translateY(0);
}
/* 动画 */
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

@ -0,0 +1,271 @@
<template>
<div class="promotion-management">
<header class="header">
<h1>促销活动管理</h1>
<p>管理您的促销活动添加编辑或删除促销信息</p>
</header>
<!-- 添加促销按钮 -->
<div class="toolbar">
<button @click="showAddForm = true" class="btn add-btn">添加促销活动</button>
</div>
<!-- 添加促销表单 -->
<div v-if="showAddForm" class="modal-overlay">
<div class="form-modal">
<h2>添加促销活动</h2>
<form @submit.prevent="addPromotionSubmit">
<label for="addTitle">促销标题</label>
<input id="addTitle" v-model="newPromotion.title" type="text" required />
<label for="addDiscount">折扣 (%)</label>
<input
id="addDiscount"
v-model="newPromotion.discount"
type="number"
min="0"
max="100"
required
/>
<div class="form-buttons">
<button type="submit" class="btn confirm-btn">确认添加</button>
<button type="button" class="btn cancel-btn" @click="showAddForm = false">取消</button>
</div>
</form>
</div>
</div>
<!-- 促销活动列表 -->
<div class="promotion-list">
<div class="promotion-card" v-for="promo in promotions" :key="promo.id">
<div class="promo-details">
<h3>{{ promo.title }}</h3>
<p>折扣: <strong>{{ promo.discount }}%</strong></p>
</div>
<div class="card-actions">
<button @click="startEditPromotion(promo)" class="btn edit-btn">编辑</button>
<button @click="showDeleteConfirm = promo.id" class="btn delete-btn">删除</button>
</div>
</div>
</div>
<!-- 编辑促销表单 -->
<div v-if="showEditForm" class="modal-overlay">
<div class="form-modal">
<h2>编辑促销活动</h2>
<form @submit.prevent="editPromotionSubmit">
<label for="editTitle">促销标题</label>
<input id="editTitle" v-model="editPromotionData.title" type="text" required />
<label for="editDiscount">折扣 (%)</label>
<input
id="editDiscount"
v-model="editPromotionData.discount"
type="number"
min="0"
max="100"
required
/>
<div class="form-buttons">
<button type="submit" class="btn confirm-btn">确认修改</button>
<button type="button" class="btn cancel-btn" @click="showEditForm = false">取消</button>
</div>
</form>
</div>
</div>
<!-- 删除确认提示 -->
<div v-if="showDeleteConfirm" class="modal-overlay">
<div class="confirm-modal">
<p>确定要删除促销活动 "<strong>{{ getPromoTitle(showDeleteConfirm) }}</strong>" </p>
<div class="form-buttons">
<button @click="deletePromotion(showDeleteConfirm)" class="btn confirm-btn">确定</button>
<button @click="showDeleteConfirm = false" class="btn cancel-btn">取消</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
//
const promotions = ref([
{ id: 1, title: "双十一促销", discount: 50 },
{ id: 2, title: "年终特惠", discount: 30 },
]);
//
const showAddForm = ref(false);
const newPromotion = ref({ title: "", discount: 0 });
//
const showEditForm = ref(false);
const editPromotionData = ref({ title: "", discount: 0 });
//
const showDeleteConfirm = ref(false);
//
const addPromotionSubmit = () => {
promotions.value.push({
id: Date.now(),
title: newPromotion.value.title,
discount: newPromotion.value.discount,
});
newPromotion.value = { title: "", discount: 0 };
showAddForm.value = false;
};
//
const startEditPromotion = (promo) => {
editPromotionData.value = { ...promo };
showEditForm.value = true;
};
//
const editPromotionSubmit = () => {
const index = promotions.value.findIndex((p) => p.id === editPromotionData.value.id);
promotions.value[index] = { ...editPromotionData.value };
editPromotionData.value = { title: "", discount: 0 };
showEditForm.value = false;
};
//
const deletePromotion = (id) => {
promotions.value = promotions.value.filter((p) => p.id !== id);
showDeleteConfirm.value = false;
};
//
const getPromoTitle = (id) => {
const promo = promotions.value.find((p) => p.id === id);
return promo ? promo.title : "";
};
</script>
<style scoped>
/* 页面整体布局 */
.promotion-management {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2rem;
margin-bottom: 10px;
color: #2c3e50;
}
.header p {
font-size: 1rem;
color: #7f8c8d;
}
.toolbar {
text-align: center;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
margin: 5px;
transition: background-color 0.3s ease;
}
.add-btn {
background-color: #1abc9c;
color: white;
}
.edit-btn {
background-color: #3498db;
color: white;
}
.delete-btn {
background-color: #e74c3c;
color: white;
}
.confirm-btn {
background-color: #27ae60;
color: white;
}
.cancel-btn {
background-color: #95a5a6;
color: white;
}
.btn:hover {
opacity: 0.9;
}
/* 卡片样式 */
.promotion-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.promotion-card {
background: white;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
flex: 1 1 calc(33.333% - 20px);
}
.promo-details h3 {
margin: 0 0 10px;
color: #34495e;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.form-modal,
.confirm-modal {
background-color: white;
border-radius: 10px;
padding: 20px;
width: 400px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
form label {
display: block;
margin-bottom: 5px;
color: #2c3e50;
}
form input {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>

@ -0,0 +1,159 @@
<template>
<div class="dashboard">
<!-- 顶部欢迎语 -->
<header class="dashboard-header">
<h1>欢迎商家</h1>
<p>高效管理您的商家业务从这里开始</p>
</header>
<!-- 快速访问模块 -->
<main class="quick-access">
<h2>快速访问功能</h2>
<div class="card-container">
<div
class="card"
v-for="link in links"
:key="link.name"
@click="navigateTo(link.path)"
>
<div class="card-content">
<h3>{{ link.name }}</h3>
<p>{{ link.description }}</p>
</div>
</div>
</div>
</main>
</div>
</template>
<script>
export default {
name: "Index",
data() {
return {
links: [
{
name: "图书管理",
path: "/seller/book-management",
description: "管理商店的图书库存",
},
{
name: "订单管理",
path: "/seller/order-management",
description: "查看和处理订单",
},
{
name: "促销活动",
path: "/seller/promotion-management",
description: "策划并管理促销活动",
},
{
name: "客户关系",
path: "/seller/customer-feedback",
description: "维护客户关系,提高满意度",
},
{
name: "数据分析",
path: "/seller/data-analytics",
description: "分析数据以驱动决策",
},
{
name: "商家信息",
path: "/seller/merchant-info",
description: "管理您的商家信息",
},
],
};
},
methods: {
navigateTo(path) {
this.$router.push(path);
},
},
};
</script>
<style scoped>
/* 通用样式 */
body {
margin: 0;
font-family: "Arial", sans-serif;
color: #333;
}
/* 顶部样式 */
.dashboard-header {
text-align: center;
padding: 20px;
background-color: #2c3e50;
color: white;
border-bottom: 5px solid #1abc9c;
}
.dashboard-header h1 {
font-size: 2.5rem;
margin: 0;
}
.dashboard-header p {
font-size: 1.2rem;
margin: 10px 0 0;
}
/* 快速访问模块 */
.quick-access {
text-align: center;
margin: 30px auto;
}
.quick-access h2 {
font-size: 1.8rem;
margin-bottom: 20px;
color: #34495e;
}
/* 卡片容器 */
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
padding: 0 20px;
}
/* 卡片样式 */
.card {
width: 200px;
background: white;
border-radius: 10px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
padding: 20px;
text-align: center;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.2);
}
.card-content h3 {
font-size: 1.2rem;
color: #2c3e50;
}
.card-content p {
font-size: 0.9rem;
color: #7f8c8d;
}
</style>

@ -0,0 +1,24 @@
/*
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
})
*/
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
}
});
Loading…
Cancel
Save