内容梗概:
-
这是一个带登录认证、自动超时、响应式设计的完整全栈web示例项目。
-
后端使用python语言+flask框架,前端使用vue框架,数据库使用MySQL。
-
基本功能是对MySQL数据库的增查改删操作。
-
具备登录验证功能,只有登录状态才能进行增查改删。
-
具备超时检测功能,如果15分钟内无任何操作则自动退出到登录界面。
-
前端页面PC、手机自适应。
内容目录:
-
环境准备
-
创建项目目录结构
-
后端开发(Flask)
-
前端开发(Vue)
-
本地测试运行
1. 环境准备
确认已安装的软件
|-------------|------------------|-----------------------------------|
| 软件 | 检查命令 | 下载地址 |
| Python 3.8+ | python --version | https://www.python.org/downloads/ |
| Node.js 16+ | node --version | https://nodejs.org/ |
| MySQL 5.7+ | mysql --version | https://dev.mysql.com/downloads/ |
2. 创建目录结构
D:\PROJECTS\MYWEBAPP
├── backend
├── frontend
└── nginx
3. 后端开发(Flask)
3.1 创建 Python 虚拟环境
powershell
进入后端目录
cd D:\projects\mywebapp\backend
创建虚拟环境
python -m venv venv
激活虚拟环境(Windows)
venv\Scripts\activate
看到 (venv) 前缀表示激活成功
3.2 创建 requirements.txt
在 D:\projects\mywebapp\backend 目录下创建 requirements.txt 文件:
Flask==2.3.3
flask-cors==4.0.0
flask-jwt-extended==4.5.3
PyJWT==2.8.0
bcrypt==4.1.2
PyMySQL==1.1.0
Werkzeug==2.3.7
3.3 安装依赖
确保虚拟环境已激活
pip install -r requirements.txt
3.4 创建 config.py
在 D:\projects\mywebapp\backend 目录下创建 config.py:
python
# config.py
import os
from datetime import timedelta
class Config:
# ===== MySQL 数据库配置 =====
DB_HOST = 'localhost'
DB_USER = 'root'
DB_PASSWORD = '123456' # ⚠️ 改成你的 MySQL 密码
DB_NAME = 'mywebapp'
# ===== Flask 配置 =====
SECRET_KEY = 'your-super-secret-key-change-in-production'
DEBUG = False
# ===== JWT 配置 =====
JWT_SECRET_KEY = 'jwt-secret-key-change-in-production'
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=15) # 15分钟过期
JWT_TOKEN_LOCATION = ['headers']
JWT_HEADER_NAME = 'Authorization'
JWT_HEADER_TYPE = 'Bearer'
3.5 创建 app.py
在 D:\projects\mywebapp\backend 目录下创建 app.py:
python
# app.py
from flask import Flask, jsonify, request
from flask_cors import CORS
from flask_jwt_extended import (
JWTManager, create_access_token, jwt_required,
get_jwt_identity, get_jwt
)
import pymysql
from config import Config
import bcrypt
# ==================== 初始化 Flask ====================
app = Flask(__name__)
app.config.from_object(Config)
CORS(app, supports_credentials=True)
jwt = JWTManager(app)
# ==================== 数据库工具函数 ====================
def get_db_connection():
"""获取 MySQL 数据库连接"""
return pymysql.connect(
host=Config.DB_HOST,
user=Config.DB_USER,
password=Config.DB_PASSWORD,
database=Config.DB_NAME,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
# ==================== 初始化数据库 ====================
def init_database():
"""初始化数据库表,并创建默认管理员账号"""
conn = get_db_connection()
with conn.cursor() as cursor:
# 创建用户表
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 检查默认管理员是否存在
cursor.execute('SELECT id FROM users WHERE username = %s', ('admin',))
if not cursor.fetchone():
# 密码: admin123
password_hash = bcrypt.hashpw('admin123'.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
cursor.execute(
'INSERT INTO users (username, email, password_hash) VALUES (%s, %s, %s)',
('admin', 'admin@example.com', password_hash)
)
conn.commit()
print("✅ 默认管理员账号已创建: admin / admin123")
conn.close()
# 执行初始化
init_database()
# ==================== 认证接口 ====================
@app.route('/api/auth/login', methods=['POST'])
def login():
"""用户登录"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'code': -1, 'message': '用户名和密码不能为空'}), 400
conn = get_db_connection()
with conn.cursor() as cursor:
cursor.execute('SELECT id, username, password_hash FROM users WHERE username = %s', (username,))
user = cursor.fetchone()
conn.close()
if not user:
return jsonify({'code': -1, 'message': '用户名或密码错误'}), 401
if not bcrypt.checkpw(password.encode('utf-8'), user['password_hash'].encode('utf-8')):
return jsonify({'code': -1, 'message': '用户名或密码错误'}), 401
access_token = create_access_token(
identity=str(user['id']),
additional_claims={'username': user['username']}
)
return jsonify({
'code': 0,
'data': {
'token': access_token,
'user': {'id': user['id'], 'username': user['username']}
},
'message': '登录成功'
})
@app.route('/api/auth/me', methods=['GET'])
@jwt_required()
def get_current_user():
"""获取当前登录用户信息"""
user_id = get_jwt_identity()
claims = get_jwt()
return jsonify({
'code': 0,
'data': {
'id': int(user_id),
'username': claims.get('username')
}
})
@app.route('/api/auth/logout', methods=['POST'])
@jwt_required()
def logout():
"""登出"""
return jsonify({'code': 0, 'message': '已登出'})
# ==================== 健康检查 ====================
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查(无需登录)"""
return jsonify({'status': 'ok', 'message': 'Flask backend is running!'})
# ==================== 用户管理接口(需要登录) ====================
@app.route('/api/users', methods=['GET'])
@jwt_required()
def get_users():
"""获取所有用户"""
try:
conn = get_db_connection()
with conn.cursor() as cursor:
cursor.execute('SELECT id, username, email, created_at FROM users ORDER BY id')
users = cursor.fetchall()
conn.close()
return jsonify({'code': 0, 'data': users, 'message': 'success'})
except Exception as e:
return jsonify({'code': -1, 'message': str(e)}), 500
@app.route('/api/users', methods=['POST'])
@jwt_required()
def create_user():
"""创建新用户"""
data = request.get_json()
username = data.get('username')
email = data.get('email')
password = data.get('password', '123456')
if not username or not email:
return jsonify({'code': -1, 'message': '用户名和邮箱不能为空'}), 400
try:
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
conn = get_db_connection()
with conn.cursor() as cursor:
cursor.execute(
'INSERT INTO users (username, email, password_hash) VALUES (%s, %s, %s)',
(username, email, password_hash)
)
conn.commit()
new_id = cursor.lastrowid
conn.close()
return jsonify({'code': 0, 'data': {'id': new_id}, 'message': '创建成功'})
except pymysql.err.IntegrityError:
return jsonify({'code': -1, 'message': '用户名已存在'}), 400
except Exception as e:
return jsonify({'code': -1, 'message': str(e)}), 500
@app.route('/api/users/<int:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
"""更新用户信息"""
data = request.get_json()
username = data.get('username')
email = data.get('email')
if not username or not email:
return jsonify({'code': -1, 'message': '用户名和邮箱不能为空'}), 400
try:
conn = get_db_connection()
with conn.cursor() as cursor:
cursor.execute('SELECT id FROM users WHERE id = %s', (user_id,))
if not cursor.fetchone():
conn.close()
return jsonify({'code': -1, 'message': '用户不存在'}), 404
cursor.execute(
'UPDATE users SET username = %s, email = %s WHERE id = %s',
(username, email, user_id)
)
conn.commit()
conn.close()
return jsonify({'code': 0, 'message': '更新成功'})
except pymysql.err.IntegrityError:
return jsonify({'code': -1, 'message': '用户名已存在'}), 400
except Exception as e:
return jsonify({'code': -1, 'message': str(e)}), 500
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
@jwt_required()
def delete_user(user_id):
"""删除用户"""
try:
conn = get_db_connection()
with conn.cursor() as cursor:
affected = cursor.execute('DELETE FROM users WHERE id = %s', (user_id,))
conn.commit()
conn.close()
if affected:
return jsonify({'code': 0, 'message': '删除成功'})
else:
return jsonify({'code': -1, 'message': '用户不存在'}), 404
except Exception as e:
return jsonify({'code': -1, 'message': str(e)}), 500
# ==================== 启动入口 ====================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
3.6 创建启动脚本 start_backend.bat
在 D:\projects\mywebapp\backend 目录下创建 start_backend.bat:
python
@echo off
cd /d D:\projects\mywebapp\backend
call venv\Scripts\activate
python app.py
4. 前端开发(Vue)
4.1 创建 Vue 项目
powershell
进入前端目录
cd D:\projects\mywebapp\frontend
使用 Vite 创建 Vue 项目(选择 Vue 3)
npm create vite@latest . -- --template vue
安装依赖
npm install
安装额外依赖
npm install axios vue-router pinia
验证项目结构
dir
4.2 创建目录结构
在 frontend/src 目录下创建以下文件夹:
cd D:\projects\mywebapp\frontend\src
mkdir views
mkdir stores
mkdir utils
mkdir router
最终前端目录结构:
frontend/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.js
├── App.vue
├── style.css
├── views/
│ ├── Login.vue
│ └── Dashboard.vue
├── stores/
│ └── auth.js
├── utils/
│ └── axios.js
└── router/
└── index.js
4.3 创建 auth.js(Pinia 状态管理)
创建 frontend/src/stores/auth.js:
javascript
// src/stores/auth.js
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token') || null,
user: null,
lastActivity: localStorage.getItem('lastActivity') || Date.now()
}),
getters: {
isAuthenticated: (state) => !!state.token,
isSessionExpired: (state) => {
const now = Date.now()
const last = parseInt(state.lastActivity)
return now - last > 900000 // 15分钟
}
},
actions: {
setAuth(token, user) {
this.token = token
this.user = user
this.lastActivity = Date.now()
localStorage.setItem('token', token)
localStorage.setItem('lastActivity', this.lastActivity)
if (user) {
localStorage.setItem('user', JSON.stringify(user))
}
},
updateActivity() {
this.lastActivity = Date.now()
localStorage.setItem('lastActivity', this.lastActivity)
},
logout() {
this.token = null
this.user = null
this.lastActivity = 0
localStorage.removeItem('token')
localStorage.removeItem('lastActivity')
localStorage.removeItem('user')
},
restoreSession() {
const token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
const lastActivity = localStorage.getItem('lastActivity')
if (token && userStr) {
this.token = token
this.user = JSON.parse(userStr)
this.lastActivity = lastActivity || Date.now()
}
}
}
})
4.4 创建 axios.js(HTTP 请求)
创建 frontend/src/utils/axios.js:
javascript
// src/utils/axios.js
import axios from 'axios'
import router from '@/router'
// 开发环境直接请求后端,生产环境通过 Nginx 代理
const API_BASE = import.meta.env.DEV ? 'http://localhost:5000/api' : '/api'
const api = axios.create({
baseURL: API_BASE,
timeout: 10000,
withCredentials: true
})
// 请求拦截器:自动添加 Token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器:处理 Token 过期(401)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('lastActivity')
localStorage.removeItem('user')
router.push('/login')
}
return Promise.reject(error)
}
)
export default api
4.5 创建 router/index.js
创建 frontend/src/router/index.js:
javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
const routes = [
{ path: '/', redirect: '/dashboard' },
{ path: '/login', component: Login, meta: { requiresGuest: true } },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
const isAuthenticated = !!token
if (to.meta.requiresAuth && !isAuthenticated) {
return next('/login')
}
if (to.meta.requiresGuest && isAuthenticated) {
return next('/dashboard')
}
next()
})
export default router
4.6 创建 Login.vue
创建 frontend/src/views/Login.vue:
javascript
<!-- src/views/Login.vue -->
<template>
<div class="login-container">
<div class="login-card">
<h1>用户管理系统</h1>
<p class="subtitle">请登录以继续</p>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label>用户名</label>
<input
v-model="form.username"
type="text"
placeholder="请输入用户名"
required
/>
</div>
<div class="form-group">
<label>密码</label>
<input
v-model="form.password"
type="password"
placeholder="请输入密码"
required
/>
</div>
<button type="submit" :disabled="loading">
{{ loading ? '登录中...' : '登 录' }}
</button>
<p v-if="error" class="error-msg">{{ error }}</p>
<p class="hint">默认账号: admin / 密码: admin123</p>
</form>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import api from '@/utils/axios'
export default {
name: 'Login',
setup() {
const router = useRouter()
const authStore = useAuthStore()
const form = ref({ username: '', password: '' })
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
loading.value = true
error.value = ''
try {
const res = await api.post('/auth/login', form.value)
if (res.data.code === 0) {
const { token, user } = res.data.data
authStore.setAuth(token, user)
router.push('/dashboard')
} else {
error.value = res.data.message
}
} catch (err) {
error.value = err.response?.data?.message || '登录失败,请重试'
} finally {
loading.value = false
}
}
return { form, loading, error, handleLogin }
}
}
</script>
<style scoped>
* { box-sizing: border-box; }
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
background: white;
border-radius: 16px;
padding: 40px 36px;
width: 100%;
max-width: 400px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.login-card h1 {
text-align: center;
color: #333;
font-size: 28px;
margin-bottom: 4px;
}
.subtitle {
text-align: center;
color: #888;
font-size: 14px;
margin-bottom: 28px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 600;
color: #555;
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 12px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
button[type="submit"] {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
}
button[type="submit"]:hover:not(:disabled) {
transform: translateY(-2px);
}
button[type="submit"]:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.error-msg {
color: #e74c3c;
font-size: 14px;
margin-top: 12px;
text-align: center;
}
.hint {
text-align: center;
color: #aaa;
font-size: 13px;
margin-top: 16px;
background: #f5f5f5;
padding: 8px;
border-radius: 6px;
}
@media (max-width: 480px) {
.login-card {
padding: 28px 20px;
}
.login-card h1 {
font-size: 22px;
}
.form-group input {
padding: 10px 12px;
font-size: 15px;
}
button[type="submit"] {
font-size: 16px;
padding: 12px;
}
}
</style>
4.7 创建 Dashboard.vue
创建 frontend/src/views/Dashboard.vue:
javascript
<!-- src/views/Dashboard.vue -->
<template>
<div class="dashboard">
<!-- 顶部导航 -->
<header class="header">
<div class="header-left">
<h2>📊 用户管理</h2>
</div>
<div class="header-right">
<span class="user-info">👤 {{ currentUser }}</span>
<span class="session-timer">⏱️ {{ sessionTimeLeft }}</span>
<button class="logout-btn" @click="handleLogout">退出</button>
</div>
</header>
<!-- 主体 -->
<main class="main-content">
<!-- 表单 -->
<div class="form-area">
<input v-model="form.username" placeholder="用户名" />
<input v-model="form.email" placeholder="邮箱" />
<input v-if="!editingId" v-model="form.password" placeholder="密码(默认123456)" />
<button v-if="!editingId" class="btn-add" @click="addUser">添加用户</button>
<button v-else class="btn-edit" @click="updateUser">保存修改</button>
<button v-if="editingId" class="btn-cancel" @click="cancelEdit">取消</button>
</div>
<!-- 用户列表 -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td data-label="ID">{{ user.id }}</td>
<td data-label="用户名">{{ user.username }}</td>
<td data-label="邮箱">{{ user.email }}</td>
<td data-label="创建时间">{{ formatDate(user.created_at) }}</td>
<td data-label="操作">
<button class="btn-edit-sm" @click="startEdit(user)">编辑</button>
<button class="btn-delete" @click="deleteUser(user.id)">删除</button>
</td>
</tr>
</tbody>
</table>
<p v-if="loading" class="status">加载中...</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</main>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import api from '@/utils/axios'
export default {
name: 'Dashboard',
setup() {
const router = useRouter()
const authStore = useAuthStore()
const users = ref([])
const form = ref({ username: '', email: '', password: '' })
const editingId = ref(null)
const loading = ref(false)
const error = ref('')
const sessionTimer = ref(null)
const sessionTimeLeft = ref('15:00')
const currentUser = computed(() => authStore.user?.username || '用户')
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const d = new Date(dateStr)
return d.toLocaleString('zh-CN')
}
const fetchUsers = async () => {
loading.value = true
error.value = ''
try {
const res = await api.get('/users')
if (res.data.code === 0) {
users.value = res.data.data
}
} catch (err) {
error.value = err.message || '加载失败'
if (err.response?.status === 401) {
router.push('/login')
}
} finally {
loading.value = false
}
}
const addUser = async () => {
if (!form.value.username || !form.value.email) {
alert('请填写完整信息')
return
}
try {
const res = await api.post('/users', form.value)
if (res.data.code === 0) {
form.value = { username: '', email: '', password: '' }
fetchUsers()
} else {
alert(res.data.message)
}
} catch (err) {
alert(err.response?.data?.message || err.message)
}
}
const startEdit = (user) => {
editingId.value = user.id
form.value = { username: user.username, email: user.email, password: '' }
}
const updateUser = async () => {
if (!form.value.username || !form.value.email) {
alert('请填写完整信息')
return
}
try {
const res = await api.put(`/users/${editingId.value}`, form.value)
if (res.data.code === 0) {
form.value = { username: '', email: '', password: '' }
editingId.value = null
fetchUsers()
} else {
alert(res.data.message)
}
} catch (err) {
alert(err.response?.data?.message || err.message)
}
}
const cancelEdit = () => {
form.value = { username: '', email: '', password: '' }
editingId.value = null
}
const deleteUser = async (id) => {
if (!confirm('确认删除?')) return
try {
const res = await api.delete(`/users/${id}`)
if (res.data.code === 0) {
fetchUsers()
} else {
alert(res.data.message)
}
} catch (err) {
alert(err.response?.data?.message || err.message)
}
}
const handleLogout = async () => {
try {
await api.post('/auth/logout')
} catch (e) {}
authStore.logout()
router.push('/login')
}
const resetTimer = () => {
authStore.updateActivity()
updateTimerDisplay()
}
const updateTimerDisplay = () => {
const last = parseInt(localStorage.getItem('lastActivity') || Date.now())
const elapsed = Date.now() - last
const remaining = Math.max(0, 900000 - elapsed)
const minutes = Math.floor(remaining / 60000)
const seconds = Math.floor((remaining % 60000) / 1000)
sessionTimeLeft.value = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
if (remaining <= 0) {
handleLogout()
}
}
const setupActivityListeners = () => {
const events = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart']
events.forEach(ev => {
document.addEventListener(ev, resetTimer)
})
sessionTimer.value = setInterval(updateTimerDisplay, 5000)
}
const cleanupActivityListeners = () => {
const events = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart']
events.forEach(ev => {
document.removeEventListener(ev, resetTimer)
})
if (sessionTimer.value) {
clearInterval(sessionTimer.value)
}
}
onMounted(async () => {
if (authStore.isSessionExpired) {
handleLogout()
return
}
setupActivityListeners()
await fetchUsers()
updateTimerDisplay()
})
onUnmounted(() => {
cleanupActivityListeners()
})
return {
users,
form,
editingId,
loading,
error,
currentUser,
sessionTimeLeft,
formatDate,
fetchUsers,
addUser,
startEdit,
updateUser,
cancelEdit,
deleteUser,
handleLogout
}
}
}
</script>
<style scoped>
* { box-sizing: border-box; }
.dashboard {
min-height: 100vh;
background: #f5f7fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
flex-wrap: wrap;
gap: 10px;
}
.header-left h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.header-right {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.user-info {
font-size: 14px;
color: #555;
}
.session-timer {
font-size: 14px;
color: #667eea;
font-weight: 600;
background: #f0f2ff;
padding: 4px 12px;
border-radius: 20px;
}
.logout-btn {
background: #e74c3c;
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
cursor: pointer;
font-size: 14px;
}
.logout-btn:hover {
background: #c0392b;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.form-area {
display: flex;
gap: 10px;
flex-wrap: wrap;
background: white;
padding: 16px 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 20px;
align-items: center;
}
.form-area input {
flex: 1;
min-width: 120px;
padding: 10px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.form-area input:focus {
outline: none;
border-color: #667eea;
}
.form-area button {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: white;
}
.form-area button:hover {
transform: translateY(-2px);
}
.btn-add { background: #42b983; }
.btn-edit { background: #f39c12; }
.btn-cancel { background: #95a5a6; }
.table-wrapper {
background: white;
border-radius: 12px;
padding: 4px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background: #f8f9fa;
color: #555;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #eee;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
tr:hover td {
background: #fafbff;
}
.btn-edit-sm {
background: #f39c12;
color: white;
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
margin-right: 6px;
}
.btn-delete {
background: #e74c3c;
color: white;
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
}
.status { text-align: center; color: #999; padding: 20px; }
.error { text-align: center; color: #e74c3c; padding: 20px; }
/* ===== 手机自适应 ===== */
@media (max-width: 768px) {
.header {
padding: 10px 16px;
}
.header-left h2 {
font-size: 17px;
}
.header-right {
gap: 8px;
}
.user-info { font-size: 13px; }
.session-timer { font-size: 12px; padding: 2px 10px; }
.logout-btn { font-size: 13px; padding: 4px 12px; }
.form-area {
padding: 12px 14px;
gap: 8px;
}
.form-area input {
min-width: 80px;
padding: 8px 12px;
font-size: 13px;
flex-basis: 100%;
}
.form-area button {
padding: 8px 14px;
font-size: 13px;
flex: 1;
min-width: 60px;
}
/* 手机端表格转卡片 */
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
tr {
display: block;
padding: 12px 14px;
border-bottom: 2px solid #eee;
}
td {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border: none;
}
td:before {
content: attr(data-label);
font-weight: 600;
color: #888;
font-size: 13px;
}
td:last-child {
border-top: 1px solid #eee;
margin-top: 6px;
padding-top: 10px;
}
td:last-child:before { content: '操作'; }
}
@media (max-width: 480px) {
.main-content { padding: 12px; }
.form-area input { font-size: 12px; padding: 6px 10px; }
.form-area button { font-size: 12px; padding: 6px 10px; }
td { font-size: 13px; }
}
</style>
4.8 创建 main.js
创建 frontend/src/main.js:
javascript
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
4.9 创建 App.vue
创建 frontend/src/App.vue:
javascript
<!-- src/App.vue -->
<template>
<router-view />
</template>
<script>
import { useAuthStore } from './stores/auth'
export default {
name: 'App',
setup() {
// 应用启动时恢复会话
const authStore = useAuthStore()
authStore.restoreSession()
}
}
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f7fa;
}
</style>
4.10 创建 style.css
创建 frontend/src/style.css:
css
/* src/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f5f7fa;
color: #333;
}
#app {
min-height: 100vh;
}
/* 通用工具类 */
.text-center { text-align: center; }
.mt-20 { margin-top: 20px; }
.mb-20 { margin-bottom: 20px; }
4.11 创建 vite.config.js
创建 frontend/vite.config.js:
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path' // ⚠️ 需要引入 path
export default defineConfig({
plugins: [vue()],
// ===== 添加路径别名解析 =====
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
4.12 创建 package.json
创建 frontend/package.json:
javascript
{
"name": "mywebapp-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.0",
"pinia": "^2.1.7",
"vue": "^3.3.8",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"vite": "^4.5.0"
}
}
4.13 创建 index.html
创建 frontend/index.html:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>用户管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
至此,前端完整代码已全部生成
前端文件清单
frontend/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.js
├── App.vue
├── style.css
├── views/
│ ├── Login.vue ✅ 已生成
│ └── Dashboard.vue ✅ 已生成
├── stores/
│ └── auth.js ✅ 已生成
├── utils/
│ └── axios.js ✅ 已生成
└── router/
└── index.js ✅ 已生成
4.14. 安装前端依赖并运行
- 安装依赖
powershell
cd D:\projects\mywebapp\frontend
npm install
- 启动开发服务器
powershell
npm run dev
访问 http://localhost:5173,你应该能看到登录页面。
5. 本地测试步骤:
5.1 准备数据库
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS mywebapp;
USE mywebapp;
-- 创建用户表(后端会自动创建,但也可以手动创建确认)
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 查看表结构
DESC users;
-- 退出 MySQL
EXIT;
5.2 启动后端 (app.py)
1. 打开新终端,启动后端
powershell
进入后端目录
cd D:\projects\mywebapp\backend
激活虚拟环境
venv\Scripts\activate
确认依赖已安装(如果还没安装)
pip install -r requirements.txt
启动 Flask 应用
python app.py
2. 看到成功启动信息
你应该看到类似这样的输出:
✅ 默认管理员账号已创建: admin / admin123
* Serving Flask app 'app'
* Debug mode: off
* Running on http://0.0.0.0:5000
注意 :这个终端要保持运行,不要关闭!
3. 验证后端是否正常
打开浏览器访问 http://localhost:5000/api/health
应该返回:
json
{"message":"Flask backend is running!","status":"ok"}
5.3 启动前端
打开新终端,启动前端
cd D:\projects\mywebapp\frontend
如果还没安装依赖
npm install
启动开发服务器
npm run dev
看到类似输出:
VITE v4.5.0 ready in 500 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
5.4 测试登录
- 打开浏览器访问
- 使用默认账号登录
用户名 admin
密码 admin123
- 登录成功
登录后会自动跳转到 Dashboard(用户管理界面),看到用户列表。
5.5 测试增删改查功能(略)
至此完成。