Flask笔记(2)快速web项目

内容梗概:
  1. 这是一个带登录认证、自动超时、响应式设计的完整全栈web示例项目。

  2. 后端使用python语言+flask框架,前端使用vue框架,数据库使用MySQL。

  3. 基本功能是对MySQL数据库的增查改删操作。

  4. 具备登录验证功能,只有登录状态才能进行增查改删。

  5. 具备超时检测功能,如果15分钟内无任何操作则自动退出到登录界面。

  6. 前端页面PC、手机自适应。

内容目录:
  1. 环境准备

  2. 创建项目目录结构

  3. 后端开发(Flask)

  4. 前端开发(Vue)

  5. 本地测试运行

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. 安装前端依赖并运行
  1. 安装依赖

powershell

cd D:\projects\mywebapp\frontend

npm install

  1. 启动开发服务器

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 测试登录
  1. 打开浏览器访问

http://localhost:5173

  1. 使用默认账号登录

用户名 admin

密码 admin123

  1. 登录成功

登录后会自动跳转到 Dashboard(用户管理界面),看到用户列表。

5.5 测试增删改查功能(略)

至此完成。

相关推荐
宵时待雨1 小时前
linux笔记归纳14:Socket编程TCP
linux·服务器·网络·笔记·tcp/ip
海盗12342 小时前
微软技术日报——2026-08-07
python·microsoft·flask
什仙2 小时前
Maxwell:提取实体表面,用于扫掠。
笔记·学习·ansys·maxwell
zyplayer-doc3 小时前
核心制度不该被随手修改:用zyplayer-doc锁定文档和全部子文档
大数据·数据库·人工智能·笔记·pdf·ocr
weixin_BYSJ19873 小时前
【java项目分享】springboot阅读推荐平台10600
java·javascript·spring boot·python·django·flask·php
啄缘之间4 小时前
5.1 uvm‑component 的层级结构管理? new (name,parent) 参数
开发语言·笔记·学习·ic·uvm·sv
江屿风4 小时前
【科普】【差集&交集概念落地云相册】流食般投喂
开发语言·c++·笔记·算法·云相册
一米阳光866114 小时前
软考(中级)软件设计师核心笔记(6)系统开发基础——需求分析、系统设计
笔记·职场发展·需求分析·软考·软件设计师·中级职称
雨田言炎18 小时前
四、关于Qt项目需要知道的
开发语言·笔记·qt