@ -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"
|
||||
]
|
||||
}
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
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}`);
|
||||
});
|
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 310 KiB |
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 292 KiB |
After Width: | Height: | Size: 221 KiB |
After Width: | Height: | Size: 334 KiB |
After Width: | Height: | Size: 344 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 394 KiB |
After Width: | Height: | Size: 295 KiB |
After Width: | Height: | Size: 241 KiB |
After Width: | Height: | Size: 234 KiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 322 KiB |
After Width: | Height: | Size: 233 KiB |
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 303 KiB |
After Width: | Height: | Size: 199 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 260 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 255 KiB |
After Width: | Height: | Size: 260 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 4.3 KiB |
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>©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,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,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,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>
|
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}} </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,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,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>
|
After Width: | Height: | Size: 718 KiB |
After Width: | Height: | Size: 269 KiB |
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,15 @@
|
||||
let categoryList = [
|
||||
'全部分类',
|
||||
'文学',
|
||||
'教育',
|
||||
'绘画',
|
||||
'摄影',
|
||||
'历史',
|
||||
'哲学',
|
||||
'科学',
|
||||
'语言',
|
||||
'军事',
|
||||
'医药',
|
||||
]
|
||||
|
||||
export default categoryList;
|
@ -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,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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|