diff --git a/__init__.py b/__init__.py index a796667..2cd6a6e 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ db.init_app(app) login_manager = LoginManager() login_manager.init_app(app) -login_manager.login_view = 'index' +login_manager.login_view = 'login' @login_manager.user_loader def load_user(user_id): diff --git a/config.py b/config.py index 064281c..2f346ab 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,22 @@ import os +from datetime import timedelta class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard_to_guess_string_for_flask_app' SQLALCHEMY_DATABASE_URI = 'sqlite:///question_collector.db' - SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_ENGINE_OPTIONS = { + 'connect_args': {'check_same_thread': False} + } + +# 设置 UTC+8 时区 +import pytz +tz = pytz.timezone('Asia/Shanghai') + +def naive_now(): + from datetime import datetime + return datetime.now(tz) + +def utc_now(): + from datetime import datetime + return datetime.utcnow() \ No newline at end of file diff --git a/forms.py b/forms.py index d2c090b..c09f81e 100644 --- a/forms.py +++ b/forms.py @@ -1,7 +1,18 @@ from flask_wtf import FlaskForm -from wtforms import StringField, TextAreaField, SelectField, BooleanField, SubmitField +from wtforms import StringField, TextAreaField, SelectField, BooleanField, SubmitField, PasswordField from wtforms.validators import DataRequired, Length +class LoginForm(FlaskForm): + username = StringField('用户名', validators=[DataRequired(), Length(max=80)]) + password = PasswordField('密码', validators=[DataRequired()]) + submit = SubmitField('登录') + +class RegisterForm(FlaskForm): + username = StringField('用户名', validators=[DataRequired(), Length(max=80)]) + password = PasswordField('密码', validators=[DataRequired(), Length(min=6)]) + confirm_password = PasswordField('确认密码', validators=[DataRequired()]) + submit = SubmitField('注册') + class DemandForm(FlaskForm): title = StringField('需求标题', validators=[DataRequired(), Length(max=200)]) content = TextAreaField('需求内容', validators=[DataRequired(), Length(max=1000)]) @@ -14,7 +25,7 @@ class DemandForm(FlaskForm): ('finance_review', '经费审查委员会'), ('women', '女职工委员会') ], validators=[DataRequired()]) - contact = StringField('联系方式', validators=[DataRequired(), Length(max=100)]) + contact = StringField('联系方式') is_public = BooleanField('是否公开') submit = SubmitField('提交') diff --git a/init_db.py b/init_db.py index df9223b..a0e2214 100644 --- a/init_db.py +++ b/init_db.py @@ -1,19 +1,42 @@ from __init__ import app, db from models import User +from sqlalchemy import text with app.app_context(): db.create_all() - if not User.query.filter_by(username='管理员').first(): + # 检查并添加 password_hash 字段(如果不存在) + try: + db.session.execute(text("ALTER TABLE user ADD COLUMN password_hash VARCHAR(200)")) + db.session.commit() + print('已添加 password_hash 字段') + except Exception as e: + if 'duplicate column' in str(e).lower() or 'already exists' in str(e).lower(): + print('password_hash 字段已存在') + else: + pass # 字段可能已存在,忽略错误 + + # 创建默认管理员账号 + admin = User.query.filter_by(username='管理员').first() + if not admin: admin = User( - username='管理员', + username='admin', dingtalk_userid='admin', dingtalk_name='管理员', dingtalk_dept='', role='admin' ) + admin.set_password('admin123') # 设置默认密码 db.session.add(admin) db.session.commit() print('数据库初始化完成,已创建管理员账户') + print('用户名: admin') + print('密码: admin123') + print('请及时修改默认密码!') else: + # 确保管理员有密码 + if not admin.password_hash: + admin.set_password('admin123') + db.session.commit() + print('已为管理员设置默认密码: admin123') print('数据库已存在') diff --git a/models.py b/models.py index 5aa9ae0..f93e4dd 100644 --- a/models.py +++ b/models.py @@ -1,18 +1,33 @@ from datetime import datetime from flask_sqlalchemy import SQLAlchemy from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +import pytz db = SQLAlchemy() +def now_shanghai(): + """返回上海时区(UTC+8)的当前时间""" + return datetime.now(pytz.timezone('Asia/Shanghai')) + class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) + password_hash = db.Column(db.String(200)) role = db.Column(db.String(20), nullable=False, default='user') contact = db.Column(db.String(100)) - dingtalk_userid = db.Column(db.String(100), unique=True, nullable=False) + dingtalk_userid = db.Column(db.String(100), unique=True) dingtalk_name = db.Column(db.String(100)) dingtalk_dept = db.Column(db.String(200)) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=now_shanghai) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + if not self.password_hash: + return False + return check_password_hash(self.password_hash, password) def is_admin(self): return self.role == 'admin' @@ -27,8 +42,8 @@ class Demand(db.Model): answer = db.Column(db.Text) answered_at = db.Column(db.DateTime) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=now_shanghai) + updated_at = db.Column(db.DateTime, default=now_shanghai) user = db.relationship('User', backref=db.backref('demands', lazy=True)) diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..e4eeae5 --- /dev/null +++ b/package.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# 项目名称 +PROJECT_NAME="mashangban" + +# 打包输出目录 +OUTPUT_DIR="dist" + +# 创建输出目录 +mkdir -p $OUTPUT_DIR + +# 创建打包文件 +echo "开始打包..." + +# 使用 tar 打包项目文件 +tar -czvf "$OUTPUT_DIR/${PROJECT_NAME}_$(date +%Y%m%d_%H%M%S).tar.gz" \ + --exclude='__pycache__' \ + --exclude='*.pyc' \ + --exclude='.git' \ + --exclude='.codebuddy' \ + --exclude='node_modules' \ + --exclude='*.db' \ + --exclude='venv' \ + --exclude='env' \ + --exclude='.env' \ + -C /Users/justin/df_project \ + $PROJECT_NAME + +echo "打包完成!文件保存在 $OUTPUT_DIR 目录" +ls -lh $OUTPUT_DIR/ diff --git a/requirements.txt b/requirements.txt index cbbda08..9c46d86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ Flask-WTF==1.2.1 Flask-Login==0.6.3 requests==2.31.0 python-dotenv==1.0.0 +pytz==2024.1 diff --git a/routes.py b/routes.py index 543c446..6ec160e 100644 --- a/routes.py +++ b/routes.py @@ -1,9 +1,9 @@ -from datetime import datetime from flask import render_template, redirect, url_for, flash, request +from math import ceil from flask_login import login_user, logout_user, login_required, current_user from __init__ import app, db -from models import User, Demand -from forms import DemandForm, AnswerForm +from models import User, Demand, now_shanghai +from forms import DemandForm, AnswerForm, LoginForm, RegisterForm BRANCH_NAMES = { 'comprehensive': '综合分会', @@ -21,6 +21,53 @@ def utility_processor(): return BRANCH_NAMES.get(branch_key, branch_key) return dict(get_branch_name=get_branch_name) +@app.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + flash('登录成功', 'success') + next_page = request.args.get('next') + return redirect(next_page) if next_page else redirect(url_for('index')) + else: + flash('用户名或密码错误', 'error') + return render_template('login.html', form=form) + +@app.route('/logout') +@login_required +def logout(): + logout_user() + flash('已退出登录', 'info') + return redirect(url_for('index')) + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = RegisterForm() + if form.validate_on_submit(): + if form.password.data != form.confirm_password.data: + flash('两次输入的密码不一致', 'warning') + return render_template('register.html', form=form) + existing_user = User.query.filter_by(username=form.username.data).first() + if existing_user: + flash('用户名已存在', 'warning') + else: + user = User( + username=form.username.data, + role='user' + ) + user.set_password(form.password.data) + db.session.add(user) + db.session.commit() + flash('注册成功,请登录', 'success') + return redirect(url_for('login')) + return render_template('register.html', form=form) + @app.before_request def before_request(): user_id = request.args.get('userId') @@ -55,8 +102,12 @@ def before_request(): @app.route('/') def index(): - demands = Demand.query.filter_by(is_public=True).order_by(Demand.created_at.desc()).all() - return render_template('index.html', demands=demands) + page = request.args.get('page', 1, type=int) + per_page = 10 + pagination = Demand.query.filter_by(is_public=True).order_by(Demand.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + return render_template('index.html', demands=pagination.items, pagination=pagination) @app.route('/demand/new', methods=['GET', 'POST']) def new_demand(): @@ -74,7 +125,7 @@ def new_demand(): ) db.session.add(demand) db.session.commit() - flash('需求提交成功') + flash('需求提交成功', 'success') return redirect(url_for('index')) return render_template('demand_form.html', form=form, title='提交新需求') @@ -84,7 +135,7 @@ def edit_demand(id): return render_template('not_logged_in.html') demand = Demand.query.get_or_404(id) if not demand.can_edit(current_user): - flash('无权限编辑此需求') + flash('无权限编辑此需求', 'error') return redirect(url_for('index')) form = DemandForm(obj=demand) if form.validate_on_submit(): @@ -94,9 +145,9 @@ def edit_demand(id): demand.contact = form.contact.data if current_user.is_admin() or not demand.answer: demand.is_public = form.is_public.data - demand.updated_at = datetime.utcnow() + demand.updated_at = now_shanghai() db.session.commit() - flash('需求更新成功') + flash('需求更新成功', 'success') return redirect(url_for('index')) return render_template('demand_form.html', form=form, title='编辑需求', demand=demand) @@ -106,14 +157,14 @@ def answer_demand(id): return render_template('not_logged_in.html') demand = Demand.query.get_or_404(id) if not current_user.is_admin(): - flash('只有管理员可以回答需求') + flash('只有管理员可以回答需求', 'error') return redirect(url_for('index')) form = AnswerForm(data={'answer': demand.answer or ''}) if form.validate_on_submit(): demand.answer = form.answer.data - demand.answered_at = datetime.utcnow() + demand.answered_at = now_shanghai() db.session.commit() - flash('回答已保存') + flash('回答已保存', 'success') return redirect(url_for('index')) return render_template('answer_form.html', form=form, demand=demand) @@ -123,26 +174,34 @@ def toggle_public(id): return render_template('not_logged_in.html') demand = Demand.query.get_or_404(id) if not current_user.is_admin(): - flash('只有管理员可以修改公开状态') - return redirect(url_for('index')) + flash('只有管理员可以修改公开状态', 'error') + return redirect(url_for(endpoint='admin_demands')) demand.is_public = not demand.is_public db.session.commit() - flash('公开状态已更新') - return redirect(url_for('index')) + flash('公开状态已更新', 'success') + return redirect(url_for('admin_demands')) @app.route('/my_demands') def my_demands(): if not current_user.is_authenticated: return render_template('not_logged_in.html') - demands = Demand.query.filter_by(user_id=current_user.id).order_by(Demand.created_at.desc()).all() - return render_template('my_demands.html', demands=demands) + page = request.args.get('page', 1, type=int) + per_page = 10 + pagination = Demand.query.filter_by(user_id=current_user.id).order_by(Demand.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + return render_template('my_demands.html', demands=pagination.items, pagination=pagination) @app.route('/admin/demands') def admin_demands(): if not current_user.is_authenticated: return render_template('not_logged_in.html') if not current_user.is_admin(): - flash('无权限访问此页面') + flash('无权限访问此页面', 'error') return redirect(url_for('index')) - demands = Demand.query.order_by(Demand.created_at.desc()).all() - return render_template('admin_demands.html', demands=demands) + page = request.args.get('page', 1, type=int) + per_page = 10 + pagination = Demand.query.order_by(Demand.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + return render_template('admin_demands.html', demands=pagination.items, pagination=pagination) diff --git a/static/style.css b/static/style.css index 57dd8d6..3073044 100644 --- a/static/style.css +++ b/static/style.css @@ -433,6 +433,217 @@ h2 { color: #856404; } +.login-container { + max-width: 400px; + margin: 40px auto; + padding: 30px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.login-container h2 { + text-align: center; + margin-bottom: 30px; + color: #4a90d9; +} + +.login-container .form-group { + margin-bottom: 20px; +} + +.login-container .form-control { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.login-container .form-control:focus { + outline: none; + border-color: #4a90d9; +} + +.login-container .btn-primary { + width: 100%; + padding: 12px; + background-color: #4a90d9; + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; +} + +.login-container .btn-primary:hover { + background-color: #3a80c9; +} + +.login-link { + text-align: center; + margin-top: 20px; + color: #666; +} + +.login-link a { + color: #4a90d9; + text-decoration: none; +} + +.login-link a:hover { + text-decoration: underline; +} + +.dingtalk-login { + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #eee; + text-align: center; +} + +.dingtalk-login p { + color: #999; + margin-bottom: 15px; + font-size: 14px; +} + +.btn-dingtalk { + background-color: #1677ff; + color: white; + padding: 10px 20px; + border-radius: 4px; + text-decoration: none; + display: inline-block; +} + +.btn-dingtalk:hover { + background-color: #0958d9; +} + +.login-options { + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; + margin-top: 30px; +} + +.login-options .btn { + min-width: 200px; +} + +.btn-secondary { + background-color: #6c757d; + color: white; +} + +.btn-secondary:hover { + background-color: #5c636a; +} + +.error { + color: #dc3545; + font-size: 14px; + margin-top: 5px; +} + +.form-group .error { + color: #dc3545; + font-size: 14px; + margin-top: 5px; +} + +/* Toast 弹出提示 - Element UI 风格 */ +.toast-container { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + pointer-events: none; +} + +.toast { + background: white; + padding: 12px 20px; + border-radius: 4px; + box-shadow: 0 2px 12px rgba(0,0,0,0.15); + font-size: 14px; + min-width: 300px; + max-width: 500px; + text-align: center; + opacity: 0; + transform: translateY(-20px); + transition: all 0.3s ease; + pointer-events: auto; +} + +.toast.show { + opacity: 1; + transform: translateY(0); +} + +.toast-success { + background: #f0f9eb; + border: 1px solid #e1f3d8; + color: #67c23a; +} + +.toast-warning { + background: #fdf6ec; + border: 1px solid #faecd8; + color: #e6a23c; +} + +.toast-error { + background: #fef0f0; + border: 1px solid #fde2e2; + color: #f56c6c; +} + +.toast-info { + background: #edf2fc; + border: 1px solid #d3d4d6; + color: #909399; +} + +.toast .toast-icon { + margin-right: 8px; +} + +/* 分页样式 */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-top: 30px; + padding: 20px 0; +} + +.btn-page { + background-color: #4a90d9; + color: white; + padding: 8px 16px; + border-radius: 4px; + text-decoration: none; + font-size: 14px; +} + +.btn-page:hover { + background-color: #3a80c9; +} + +.page-info { + color: #666; + font-size: 14px; +} + .question-preview { background: #f8f9fa; padding: 20px; diff --git a/templates/admin_demands.html b/templates/admin_demands.html index 87bc03f..1692887 100644 --- a/templates/admin_demands.html +++ b/templates/admin_demands.html @@ -9,22 +9,26 @@ {% for demand in demands %}
- {{ get_branch_name(demand.branch) }} - {{ demand.created_at.strftime('%Y-%m-%d %H:%M') }} +

{{ demand.title }} {{ get_branch_name(demand.branch) }}

{% if demand.is_public %}公开{% else %}私有{% endif %} - 提交者: {{ demand.user.username }}
-

{{ demand.title }}

+ 提交者: {{ demand.user.username }}       提交时间: {{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}

{{ demand.content }}

+ +{% if pagination.pages > 1 %} + +{% endif %} {% else %}

暂无需求

{% endif %} -{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 3ce7004..2c60404 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,20 +23,48 @@
  • 管理后台
  • {% endif %}
  • {{ current_user.dingtalk_name or current_user.username }}
  • +
  • 退出
  • + {% else %} +
  • 登录
  • {% endif %}
    - {% with messages = get_flashed_messages() %} + {% block content %}{% endblock %} + +
    +