Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de0d0cc6be |
@@ -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):
|
||||
|
||||
18
config.py
18
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
|
||||
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()
|
||||
15
forms.py
15
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('提交')
|
||||
|
||||
|
||||
27
init_db.py
27
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('数据库已存在')
|
||||
|
||||
23
models.py
23
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))
|
||||
|
||||
|
||||
30
package.sh
Executable file
30
package.sh
Executable file
@@ -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/
|
||||
@@ -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
|
||||
|
||||
101
routes.py
101
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)
|
||||
|
||||
211
static/style.css
211
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;
|
||||
|
||||
@@ -9,22 +9,26 @@
|
||||
{% for demand in demands %}
|
||||
<div class="question-item">
|
||||
<div class="question-header">
|
||||
<span class="branch">{{ get_branch_name(demand.branch) }}</span>
|
||||
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<h3 class="demand-title">{{ demand.title }} <span class="branch">{{ get_branch_name(demand.branch) }}</span></h3>
|
||||
<span class="status {% if demand.is_public %}public{% else %}private{% endif %}">
|
||||
{% if demand.is_public %}公开{% else %}私有{% endif %}
|
||||
</span>
|
||||
<span class="user">提交者: {{ demand.user.username }}</span>
|
||||
</div>
|
||||
<h3 class="demand-title">{{ demand.title }}</h3>
|
||||
<span class="time">提交者: {{ demand.user.username }} 提交时间: {{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<p class="content">{{ demand.content }}</p>
|
||||
<div class="question-footer">
|
||||
<span class="contact">联系方式: {{ demand.contact }}</span>
|
||||
{% if demand.answer %}
|
||||
<div class="answer">
|
||||
<strong>回答:</strong>
|
||||
<p>{{ demand.answer }}</p>
|
||||
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<div class="answer-toggle" onclick="toggleAnswer(this)">
|
||||
<span class="toggle-icon">▶</span>
|
||||
<span class="toggle-text">查看回答</span>
|
||||
</div>
|
||||
<div class="answer-content" style="display: none;">
|
||||
<div class="answer">
|
||||
<strong>回答:</strong>
|
||||
<p>{{ demand.answer }}</p>
|
||||
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="status waiting">待回答</span>
|
||||
@@ -42,7 +46,41 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('admin_demands', page=pagination.prev_num) }}" class="btn btn-page">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="page-info">
|
||||
共 {{ pagination.total }} 条,第 {{ pagination.page }} / {{ pagination.pages }} 页
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('admin_demands', page=pagination.next_num) }}" class="btn btn-page">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="empty">暂无需求</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
function toggleAnswer(element) {
|
||||
var content = element.nextElementSibling;
|
||||
var icon = element.querySelector('.toggle-icon');
|
||||
var text = element.querySelector('.toggle-text');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.textContent = '▼';
|
||||
text.textContent = '收起回答';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.textContent = '▶';
|
||||
text.textContent = '查看回答';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,20 +23,48 @@
|
||||
<li><a href="{{ url_for('admin_demands') }}">管理后台</a></li>
|
||||
{% endif %}
|
||||
<li class="user-name">{{ current_user.dingtalk_name or current_user.username }}</li>
|
||||
<li><a href="{{ url_for('logout') }}">退出</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for('login') }}">登录</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
<script>
|
||||
const toastIcons = {
|
||||
success: '✓',
|
||||
warning: '⚠',
|
||||
error: '✕',
|
||||
info: 'ℹ'
|
||||
};
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `<span class="toast-icon">${toastIcons[type]}</span>${message}`;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => toast.classList.add('show'), 10);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 显示 Flask flash messages
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% for category, message in messages %}
|
||||
showToast('{{ message }}', '{{ category }}');
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</script>
|
||||
</main>
|
||||
<footer>
|
||||
<div class="container">
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
{{ form.branch(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ form.contact.label.text }} *</label>
|
||||
{{ form.contact(class="form-control") }}
|
||||
<label>{{ form.contact.label.text }}</label>
|
||||
{{ form.contact(class="form-control") }}
|
||||
</div>
|
||||
{{ form.is_public() }} <label for="is_public">{{ form.is_public.label.text }}</label>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-cancel">取消</a>
|
||||
|
||||
@@ -27,6 +27,22 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('index', page=pagination.prev_num) }}" class="btn btn-page">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="page-info">
|
||||
共 {{ pagination.total }} 条,第 {{ pagination.page }} / {{ pagination.pages }} 页
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('index', page=pagination.next_num) }}" class="btn btn-page">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="empty">暂无公开需求</p>
|
||||
{% endif %}
|
||||
@@ -48,4 +64,4 @@ function toggleAnswer(element) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
32
templates/login.html
Normal file
32
templates/login.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}登录 - 人事共享服务中心"码"上办{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-container">
|
||||
<h2>登录</h2>
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{ form.username.label }}
|
||||
{{ form.username(class="form-control") }}
|
||||
{% if form.username.errors %}
|
||||
<div class="error">{{ form.username.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password.label }}
|
||||
{{ form.password(class="form-control") }}
|
||||
{% if form.password.errors %}
|
||||
<div class="error">{{ form.password.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</div>
|
||||
</form>
|
||||
<p class="login-link">
|
||||
还没有账号?<a href="{{ url_for('register') }}">立即注册</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -36,6 +36,22 @@
|
||||
<p class="empty">暂无需求</p>
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('my_demands', page=pagination.prev_num) }}" class="btn btn-page">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="page-info">
|
||||
共 {{ pagination.total }} 条,第 {{ pagination.page }} / {{ pagination.pages }} 页
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('my_demands', page=pagination.next_num) }}" class="btn btn-page">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleAnswer(element) {
|
||||
var content = element.nextElementSibling;
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
{% block content %}
|
||||
<div class="login-required">
|
||||
<div class="login-icon">🔒</div>
|
||||
<h2>请先通过钉钉登录</h2>
|
||||
<p>本系统需要通过钉钉账号登录才能使用</p>
|
||||
<h2>请先登录</h2>
|
||||
<p>本系统需要登录才能使用</p>
|
||||
<div class="login-options">
|
||||
<a href="{{ url_for('login') }}" class="btn btn-primary">账号密码登录</a>
|
||||
<a href="{{ url_for('register') }}" class="btn btn-secondary">注册账号</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
39
templates/register.html
Normal file
39
templates/register.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}注册 - 人事共享服务中心"码"上办{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-container">
|
||||
<h2>注册账号</h2>
|
||||
<form method="POST" action="{{ url_for('register') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{ form.username.label }}
|
||||
{{ form.username(class="form-control") }}
|
||||
{% if form.username.errors %}
|
||||
<div class="error">{{ form.username.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.password.label }}
|
||||
{{ form.password(class="form-control") }}
|
||||
{% if form.password.errors %}
|
||||
<div class="error">{{ form.password.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.confirm_password.label }}
|
||||
{{ form.confirm_password(class="form-control") }}
|
||||
{% if form.confirm_password.errors %}
|
||||
<div class="error">{{ form.confirm_password.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</div>
|
||||
</form>
|
||||
<p class="login-link">
|
||||
已有账号?<a href="{{ url_for('login') }}">立即登录</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user