├── .gitattributes ├── .gitignore ├── .idea └── vcs.xml ├── README.md ├── app ├── __init__.py ├── auth │ ├── __init__.py │ ├── forms.py │ ├── permission.py │ └── views.py ├── helpers │ ├── __init__.py │ └── email.py ├── lib │ ├── __init__.py │ ├── constant.py │ ├── mail │ │ ├── __init__.py │ │ └── email.py │ └── pagination.py ├── main │ ├── __init__.py │ ├── errors.py │ ├── forms.py │ └── views.py ├── models │ ├── OperateModel.py │ ├── __init__.py │ └── models.py ├── static │ ├── favicon.ico │ ├── images │ │ ├── default.jpg │ │ ├── e82bab09c_s.jpg │ │ ├── logo.6837e927.png │ │ ├── logo_black_trans.png │ │ ├── new_logo.ede2316d.png │ │ ├── new_logo@2x.9187366b.png │ │ ├── sprites-1.9.2.4c54885a.png │ │ ├── sprites.auto.2bb79a7e.png │ │ └── user_images.jpg │ ├── js │ │ ├── 01-jquery-1.11.3.min.js │ │ ├── demo.js │ │ ├── main.js │ │ └── require.js │ └── style │ │ ├── errors.css │ │ ├── index_side.css │ │ ├── main.app.css │ │ ├── main.css │ │ ├── mycss.css │ │ ├── style.css │ │ └── zheye.css └── templates │ ├── 404.html │ ├── 500.html │ ├── _macros.html │ ├── alluser_follow_question.html │ ├── alluser_follow_topic.html │ ├── answer_questions.html │ ├── auth │ ├── add_category.html │ ├── add_topic.html │ ├── admin_base.html │ ├── admin_edit_profile.html │ ├── admin_index.html │ ├── email │ │ ├── change_email.html │ │ ├── change_email.txt │ │ ├── confirm.html │ │ └── confirm.txt │ ├── login.html │ ├── manage_category.html │ ├── manage_topic.html │ ├── manage_users.html │ └── unconfirmed.html │ ├── base.html │ ├── base_user.html │ ├── edit_profile.html │ ├── email_settings.html │ ├── explore.html │ ├── password_settings.html │ ├── question_detail.html │ ├── question_follow_all.html │ ├── topic.html │ ├── topic_detail.html │ ├── topics.html │ ├── user.html │ ├── user_answers.html │ ├── user_asks.html │ ├── user_follow_base.html │ ├── user_followers.html │ ├── user_following.html │ └── zheye.html ├── buildout.cfg ├── config.py ├── manage.py ├── requirements.txt ├── setup.py └── versions.cfg /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-language=Python 2 | *.html linguist-language=Python -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.swp 4 | .installed.cfg 5 | *.sqlite 6 | bin 7 | develop-eggs 8 | dist 9 | eggs 10 | parts 11 | *.egg-info 12 | .idea 13 | .coverage 14 | coverage.xml 15 | site/ 16 | docs/build/ 17 | build/ 18 | 19 | old-eggs/ 20 | 21 | venv/ 22 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | > 在线问答社区:者也 3 | 4 | ## 系统设计概述 5 | > 系统采用flask作为web开发框架,sqlalchemy实现数据库的操作。服务端推送消息采用twisted&websocket实现。tornado作为web服务器进行系统的部署。 6 | 7 | ## 系统功能概述 8 | ### 已完成的功能 9 | - 用户信息 10 | - 邮件确认 11 | - 基本资料修改 12 | - 密码重置 13 | - 用户动态 14 | - 我的回答 15 | - 我的提问 16 | - 我的关注 17 |   18 | - 问答 19 | - 提问 20 | - 回答问题 21 | - 评论回答 22 |   23 | - 关注 24 | - 关注话题 25 | - 关注问题 26 | - 关注用户 27 |   28 | - 首页显示 29 | - 关注话题下的优秀问题 30 | - 关注的用户的动态 31 |   32 | 33 | 34 | - 权限管理 35 | 36 | - 管理员对于话题类别的管理 37 | - 管理员对于话题的管理 38 | - 管理员对于用户的管理 39 |   40 | ### 未完成的功能 41 | 42 | - 服务端消息的推送 43 | 44 | - 被关注的通知 45 | - 关注问题被回答的通知 46 | 47 | ## 系统页面展示 48 | ![登录界面](http://i.imgur.com/H6wsWBm.png) 49 | 50 | 51 | ![全局](http://i.imgur.com/7KqQoeA.png) 52 | 53 | ![提问](http://i.imgur.com/hJzN3yQ.png) 54 | 55 | ![个人主页](http://i.imgur.com/l8NvR7k.png) 56 | 57 | ![话题动态](http://i.imgur.com/IAVBJVq.png) 58 | 59 | ![话题广场](http://i.imgur.com/8PJKxea.png) 60 |   61 | ![问题界面](http://i.imgur.com/MACeAiR.png) 62 | 63 | ## 其他问题 64 | 注册时尽量采用网易邮箱注册。若收不到确认邮件,在垃圾邮件里找一下。 -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from flask import Flask 3 | from flask_bootstrap import Bootstrap 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_login import LoginManager 6 | from flask_mail import Mail 7 | from config import config 8 | 9 | bootstrap = Bootstrap() 10 | db = SQLAlchemy() 11 | mail = Mail() 12 | 13 | login_manager = LoginManager() 14 | login_manager.session_protection = "strong" 15 | login_manager.login_view = "auth.login_register" 16 | 17 | 18 | def create_app(config_name): 19 | # 创建Flask实例,并进行初始化 20 | app = Flask(__name__) 21 | app.config.from_object(config[config_name]) 22 | config[config_name].init_app(app) 23 | 24 | bootstrap.init_app(app) 25 | db.init_app(app) 26 | mail.init_app(app) 27 | login_manager.init_app(app) 28 | 29 | # 注册蓝本 30 | from .main import main as main_blueprint 31 | app.register_blueprint(main_blueprint) 32 | 33 | from .auth import auth as auth_blueprint 34 | app.register_blueprint(auth_blueprint) 35 | 36 | return app 37 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | auth = Blueprint('auth', __name__) 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from flask_wtf import Form 3 | from wtforms import StringField, PasswordField, BooleanField, SubmitField, FileField, SelectField 4 | from wtforms.validators import Required, Length, Email, Regexp, EqualTo, ValidationError 5 | 6 | from app.models.models import User, TopicCategory, Role 7 | 8 | 9 | class LoginForm(Form): 10 | email = StringField('Email', validators=[Required(), Length(1, 64), 11 | Email()]) 12 | password = PasswordField('Password', validators=[Required()]) 13 | remember_me = BooleanField('Keep me logged in') 14 | submit1 = SubmitField('Log In') 15 | 16 | 17 | class RegistrationForm(Form): 18 | email = StringField('Email', validators=[Required(), Length(1, 64), 19 | Email()]) 20 | username = StringField('Username', validators=[ 21 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 22 | 'Usernames must have only letters, ' 23 | 'numbers, dots or underscores')]) 24 | password = PasswordField('Password', validators=[ 25 | Required(), EqualTo('password2', message='Passwords must match.')]) 26 | password2 = PasswordField('Confirm password', validators=[Required()]) 27 | submit2 = SubmitField('Register') 28 | 29 | def validate_email(self, field): 30 | if User.query.filter_by(email=field.data).first(): 31 | raise ValidationError('Email already registered.') 32 | 33 | def validate_username(self, field): 34 | if User.query.filter_by(username=field.data).first(): 35 | raise ValidationError('Username already in use.') 36 | 37 | 38 | class ChangepasswordForm(Form): 39 | """修改密码""" 40 | oldpassword = PasswordField(u'原始密码', validators=[Required()]) 41 | password = PasswordField(u'新密码', validators=[ 42 | Required(), EqualTo('password2', message='Passwords must match.')]) 43 | password2 = PasswordField(u'再次输入', validators=[Required()]) 44 | submit = SubmitField(u'保存') 45 | 46 | 47 | class ChangeEmailForm(Form): 48 | """修改邮箱""" 49 | email = StringField(u'新邮箱', validators=[Required(), Length(1, 64), 50 | Email()]) 51 | password = PasswordField(u'密码', validators=[Required()]) 52 | submit = SubmitField(u'保存') 53 | 54 | def validate_email(self, field): 55 | if User.query.filter_by(email=field.data).first(): 56 | raise ValidationError('Email already registered.') 57 | 58 | 59 | class InsertCategory(Form): 60 | category_name = StringField(u'类别名称', validators=[Required(), Length(0, 30)]) 61 | category_desc = StringField(u'描述', validators=[Length(0, 300)]) 62 | submit = SubmitField(u'保存') 63 | 64 | 65 | class InsertTopic(Form): 66 | topic_name = StringField(u'话题名称', validators=[Required(), Length(1, 30)]) 67 | topic_desc = StringField(u'话题描述', validators=[Length(0, 300)]) 68 | topic_cate = SelectField(u'话题类别', coerce=int, validators=[Required()]) 69 | submit = SubmitField(u'保存') 70 | 71 | def __init__(self, *args, **kwargs): 72 | super(InsertTopic, self).__init__(*args, **kwargs) 73 | self.topic_cate.choices = [(cate.id, cate.category_name) for cate in TopicCategory.query.all()] 74 | 75 | 76 | class EditProfileAdminForm(Form): 77 | email = StringField(u'邮箱', validators=[Required(), Length(1, 64), 78 | Email()]) 79 | username = StringField(u'用户名', validators=[ 80 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 81 | 'Usernames must have only letters, ' 82 | 'numbers, dots or underscores')]) 83 | name = StringField(u'姓名', validators=[Length(0, 64)]) 84 | confirmed = BooleanField(u'验证') 85 | role = SelectField(u'角色', coerce=int) 86 | submit = SubmitField(u'提交') 87 | 88 | def __init__(self, user, *args, **kwargs): 89 | super(EditProfileAdminForm, self).__init__(*args, **kwargs) 90 | self.role.choices = [(role.id, role.name) 91 | for role in Role.query.order_by(Role.name).all()] 92 | self.user = user 93 | 94 | def validate_email(self, field): 95 | if field.data != self.user.email and \ 96 | User.query.filter_by(email=field.data).first(): 97 | raise ValidationError('Email already registered.') 98 | 99 | def validate_username(self, field): 100 | if field.data != self.user.username and \ 101 | User.query.filter_by(username=field.data).first(): 102 | raise ValidationError('Username already in use.') 103 | 104 | def validate_name(self, field): 105 | if field.data != self.user.name and \ 106 | User.query.filter_by(name=field.data).first(): 107 | raise ValidationError('name already in use.') -------------------------------------------------------------------------------- /app/auth/permission.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from functools import wraps 3 | from flask import abort 4 | from flask_login import current_user 5 | from app.models.models import Permission 6 | 7 | """权限的认证""" 8 | 9 | 10 | def permission_required(permission): 11 | def decorator(f): 12 | @wraps(f) 13 | def decorated_function(*args, **kwargs): 14 | if not current_user.can(permission): 15 | abort(403) 16 | return f(*args, **kwargs) 17 | return decorated_function 18 | return decorator 19 | 20 | 21 | def admin_required(f): 22 | return permission_required(Permission.ADMINISTER)(f) 23 | -------------------------------------------------------------------------------- /app/auth/views.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import base64 3 | 4 | from flask import current_app 5 | from flask_login import login_user, login_required, logout_user, current_user 6 | from flask import render_template, redirect, url_for, request, flash 7 | 8 | from app import db 9 | from app.auth import auth 10 | from app.auth.forms import LoginForm, RegistrationForm, ChangepasswordForm, ChangeEmailForm, InsertCategory, InsertTopic, \ 11 | EditProfileAdminForm 12 | from app.lib.mail.email import send_email 13 | from app.lib.pagination import base_pagination 14 | from app.models.models import User, TopicCategory, Topic, Role 15 | from app.lib import constant 16 | from app.auth.permission import admin_required, permission_required 17 | 18 | 19 | @auth.before_app_request 20 | def before_request(): 21 | if current_user.is_authenticated: 22 | if not current_user.confirmed \ 23 | and request.endpoint[:5] != 'auth.'\ 24 | and request.endpoint != 'static': 25 | return redirect(url_for('auth.unconfirmed')) 26 | 27 | 28 | @auth.route('/login', methods=['GET', 'POST']) 29 | def login_register(): 30 | login_form = LoginForm() 31 | register_form = RegistrationForm() 32 | # 进行登录表单的验证 33 | if login_form.submit1.data and login_form.validate_on_submit(): 34 | user = User.query.filter_by(email=login_form.email.data).first() 35 | if user is not None and user.verify_password(login_form.password.data): 36 | login_user(user, login_form.remember_me.data) 37 | return redirect(url_for('main.index')) 38 | flash(constant.WRONG) 39 | 40 | # 进行注册表单的验证 41 | if register_form.submit2.data and register_form.validate_on_submit(): 42 | user = User(email=register_form.email.data, 43 | username=register_form.username.data, 44 | password=register_form.password.data, 45 | name=register_form.username.data) 46 | db.session.add(user) 47 | db.session.commit() 48 | try: 49 | token = user.generate_confirmation_token() 50 | send_email(user.email, 'Confirm Your Account', 51 | 'auth/email/confirm', user=user, token=token) 52 | flash(constant.SEND_EMIAL) 53 | except Exception as e: 54 | print e 55 | return redirect(url_for('auth.login_register')) 56 | 57 | return render_template('auth/login.html', form1=login_form, form2=register_form) 58 | 59 | 60 | @auth.route('/logout') 61 | @login_required 62 | def logout(): 63 | logout_user() 64 | return redirect(url_for('main.index')) 65 | 66 | 67 | @auth.route('/confirm/') 68 | @login_required 69 | def confirm(token): 70 | if current_user.confirmed: 71 | return redirect(url_for('main.index')) 72 | if current_user.confirm(token): 73 | flash(constant.VERIFI_SUCCESS) 74 | else: 75 | flash(constant.LINK_FAIL) 76 | return redirect(url_for('main.index')) 77 | 78 | 79 | @auth.route('/unconfirmed') 80 | def unconfirmed(): 81 | if current_user.is_anonymous or current_user.confirmed: 82 | return redirect(url_for('main.index')) 83 | return render_template('auth/unconfirmed.html', user=current_user) 84 | 85 | 86 | @auth.route('/confirm') 87 | @login_required 88 | def resend_confirmation(): 89 | """进行验证邮箱的重新发送""" 90 | token = current_user.generate_confirmation_token() 91 | send_email(current_user.email, 'Confirm Your Account', 92 | 'auth/email/confirm', user=current_user, token=token) 93 | flash(constant.SEND_EMIAL) 94 | return redirect(url_for('main.index')) 95 | 96 | 97 | @auth.route('/settings', methods=['GET', 'POST']) 98 | @login_required 99 | def settings(): 100 | return redirect(url_for("auth.profile")) 101 | 102 | 103 | @auth.route('/settings/profile', methods=['GET', 'POST']) 104 | @login_required 105 | def profile(): 106 | """账户邮箱设置""" 107 | form = ChangeEmailForm() 108 | if form.validate_on_submit(): 109 | if current_user.verify_password(form.password.data): 110 | new_email = form.email.data 111 | token = current_user.generate_email_change_token(new_email) 112 | send_email(new_email, 'Confirm your email address', 113 | 'auth/email/change_email', 114 | user=current_user, token=token) 115 | flash(constant.SEND_EMIAL) 116 | return redirect(url_for('auth.profile')) 117 | else: 118 | flash(constant.WRONG_PWD) 119 | return render_template("email_settings.html", user=current_user, form=form, base64=base64) 120 | 121 | 122 | @auth.route('/change-email/') 123 | @login_required 124 | def change_email(token): 125 | if current_user.change_email(token): 126 | flash(constant.EMAIL_UPDATE) 127 | else: 128 | flash(constant.UPDATE_FAIL) 129 | return redirect(url_for('auth.profile')) 130 | 131 | 132 | @auth.route('/settings/password', methods=['GET', 'POST']) 133 | @login_required 134 | def password(): 135 | """用户密码设置""" 136 | form = ChangepasswordForm() 137 | if form.validate_on_submit(): 138 | if current_user.verify_password(form.oldpassword.data): 139 | if current_user.change_password(form.password.data): 140 | flash(constant.UPDATE_SUCC) 141 | logout_user() 142 | return redirect(url_for('auth.login_register')) 143 | else: 144 | flash(constant.UPDATE_FAIL) 145 | else: 146 | flash(constant.WRONG_PWD) 147 | 148 | return render_template("password_settings.html", user=current_user, form=form, base64=base64) 149 | 150 | 151 | @auth.route('/admin', methods=['GET', 'POST']) 152 | @login_required 153 | @admin_required 154 | def admin_index(): 155 | """管理员界面""" 156 | return render_template('auth/admin_index.html') 157 | 158 | 159 | @auth.route('/add/category', methods=['GET', 'POST']) 160 | @login_required 161 | @admin_required 162 | def add_category(): 163 | """添加话题类别""" 164 | form = InsertCategory() 165 | if form.validate_on_submit(): 166 | if not TopicCategory.query.filter_by(category_name=form.category_name.data).first(): 167 | is_success = TopicCategory.insert_category(form.category_name.data, form.category_desc.data) 168 | if not is_success: 169 | flash(constant.FAIL) 170 | else: 171 | flash(constant.ALREADY_EXIST) 172 | return redirect(url_for('auth.manage_category')) 173 | 174 | return render_template('auth/add_category.html', form=form) 175 | 176 | 177 | @auth.route('/manage/category', methods=['GET']) 178 | @login_required 179 | @admin_required 180 | def manage_category(): 181 | page = request.args.get('page', 1, type=int) 182 | pagination = base_pagination(TopicCategory.query, page, 'ADMIN_MANAGE') 183 | 184 | return render_template("auth/manage_category.html", endpoint='auth.manage_category', 185 | pagination=pagination, items=pagination.items) 186 | 187 | 188 | @auth.route('/delete/category', methods=['GET']) 189 | @login_required 190 | @admin_required 191 | def delete_category(): 192 | cate_id = request.args.get("cate_id", None) 193 | if not cate_id: 194 | return redirect(url_for('auth.manage_category')) 195 | is_success = TopicCategory.delete_category(cate_id) 196 | if not is_success: 197 | flash(constant.FAIL) 198 | else: 199 | flash(constant.UPDATE_SUCC) 200 | return redirect(url_for('auth.manage_category')) 201 | 202 | 203 | @auth.route('/add/topic', methods=['GET', 'POST']) 204 | @login_required 205 | @admin_required 206 | def add_topic(): 207 | form = InsertTopic() 208 | if request.method == 'POST': 209 | if not Topic.query.filter_by(topic_name=form.topic_name.data).first(): 210 | is_success = Topic.insert_topic(form.topic_name.data, form.topic_desc.data, 211 | request.files['file'].read(), form.topic_cate.data) 212 | if not is_success: 213 | flash(constant.FAIL) 214 | else: 215 | flash(constant.ALREADY_EXIST) 216 | return redirect(url_for('auth.manage_topic')) 217 | 218 | return render_template('auth/add_topic.html', form=form) 219 | 220 | 221 | @auth.route('/manage/topic', methods=['GET']) 222 | @login_required 223 | @admin_required 224 | def manage_topic(): 225 | page = request.args.get('page', 1, type=int) 226 | pagination = base_pagination(Topic.query, page, 'ADMIN_MANAGE') 227 | 228 | return render_template("auth/manage_topic.html", endpoint='auth.manage_topic', 229 | pagination=pagination, items=pagination.items) 230 | 231 | 232 | @auth.route('/delete/topic', methods=['GET']) 233 | @login_required 234 | @admin_required 235 | def delete_topic(): 236 | topic_id = request.args.get("topic_id", None) 237 | if not topic_id: 238 | return redirect(url_for('auth.manage_topic')) 239 | is_success = Topic.delete_topic(topic_id) 240 | if not is_success: 241 | flash(constant.FAIL) 242 | else: 243 | flash(constant.UPDATE_SUCC) 244 | return redirect(url_for('auth.manage_topic')) 245 | 246 | 247 | @auth.route('/manage/users', methods=['GET']) 248 | @login_required 249 | @admin_required 250 | def manage_users(): 251 | users = User.query.filter(User.username != current_user.username).all() 252 | return render_template("auth/manage_users.html", users=users) 253 | 254 | 255 | @auth.route('/setting_users/', methods=['GET', 'POST']) 256 | @login_required 257 | @admin_required 258 | def setting_users(id): 259 | user = User.query.get_or_404(id) 260 | form = EditProfileAdminForm(user=user) 261 | if form.validate_on_submit(): 262 | user.email = form.email.data 263 | user.username = form.username.data 264 | user.confirmed = form.confirmed.data 265 | user.role = Role.query.get(form.role.data) 266 | user.name = form.name.data 267 | db.session.add(user) 268 | db.session.commit() 269 | flash(constant.PROFILE_UPDATE) 270 | return redirect(url_for('auth.setting_users', id=id)) 271 | form.email.data = user.email 272 | form.username.data = user.username 273 | form.confirmed.data = user.confirmed 274 | form.role.data = user.role_id 275 | form.name.data = user.name 276 | return render_template('auth/admin_edit_profile.html', form=form) 277 | -------------------------------------------------------------------------------- /app/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/helpers/__init__.py -------------------------------------------------------------------------------- /app/helpers/email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from threading import Thread 4 | from flask import current_app, render_template 5 | from flask_mail import Message 6 | from app import mail 7 | 8 | 9 | def send_async_email(app, msg): 10 | with app.app_context(): 11 | mail.send(msg) 12 | 13 | 14 | def send_email(to, subject, template, **kwargs): 15 | app = current_app._get_current_object() 16 | msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject, sender = app.config['FLASKY_MAIL_SENDER'], recipients = [ 17 | to]) 18 | msg.body = render_template(template + '.txt', **None) 19 | msg.html = render_template(template + '.html', **None) 20 | thr = Thread(target=send_async_email, args=[app, msg]) 21 | thr.start() 22 | return thr 23 | 24 | -------------------------------------------------------------------------------- /app/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/lib/__init__.py -------------------------------------------------------------------------------- /app/lib/constant.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """配置flash抛出的提示信息""" 3 | PROFILE_UPDATE = u"个人资料已更新" 4 | INVALID_USER = u"未找到此人" 5 | INVALID_TOPIC = u"未找到此话题" 6 | CANNOT_CON_MYSELF = u"不能关注自己" 7 | ALREADY_CON = u'已经关注' 8 | NO_CON = u'未关注此人' 9 | AVATAR_MODI_FAIL = u"头像修改失败" 10 | WRONG = u"错误的用户名或密码" 11 | WRONG_PWD = u"密码错误" 12 | SEND_EMIAL = u"验证邮件已发送到你的邮箱,请先登录" 13 | VERIFI_SUCCESS = u"账户验证成功" 14 | LINK_FAIL = u"验证链接已过期或无效." 15 | EMAIL_UPDATE = u"邮箱账户已修改" 16 | UPDATE_FAIL = u"未修改成功" 17 | UPDATE_SUCC = u"更新成功" 18 | QUESTION_ERROR = u"问题或问题描述过长" 19 | COMMENT_ERROR = u"评论过长" 20 | FAIL = u"操作失败" 21 | NOFOUND = u'未找到' 22 | ALREADY_EXIST = u'已经存在' 23 | NOT_VALID_CHOICE = u'话题不是有效的选择' -------------------------------------------------------------------------------- /app/lib/mail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/lib/mail/__init__.py -------------------------------------------------------------------------------- /app/lib/mail/email.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from threading import Thread 3 | from flask import current_app, render_template 4 | from flask_mail import Message 5 | 6 | from app import mail 7 | 8 | 9 | def send_async_email(app, msg): 10 | with app.app_context(): 11 | mail.send(msg) 12 | 13 | 14 | def send_email(to, subject, template, **kwargs): 15 | app = current_app._get_current_object() 16 | msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject, 17 | sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) 18 | msg.body = render_template(template + '.txt', **kwargs) 19 | msg.html = render_template(template + '.html', **kwargs) 20 | thr = Thread(target=send_async_email, args=[app, msg]) 21 | thr.start() 22 | return thr 23 | -------------------------------------------------------------------------------- /app/lib/pagination.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """分页代码""" 3 | from flask import current_app 4 | 5 | 6 | def base_pagination(paging_object, page, _per_page): 7 | """ 8 | :param paging_object: 要分页的对象 9 | :param _per_page: 每页显示的条数 10 | :param page: 要渲染的页数 11 | :return: 12 | """ 13 | 14 | return paging_object.paginate(page, per_page=current_app.config[_per_page], 15 | error_out=False) 16 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint("main", __name__) 4 | 5 | from . import views, errors 6 | -------------------------------------------------------------------------------- /app/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(404) 6 | def page_not_found(e): 7 | return render_template('404.html'), 404 8 | 9 | 10 | @main.app_errorhandler(500) 11 | def internal_server_error(e): 12 | return render_template('500.html'), 500 13 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from flask_wtf import Form 3 | from wtforms import StringField, TextAreaField, SubmitField, RadioField 4 | from wtforms.validators import Length, Required 5 | 6 | 7 | class EditProfileForm(Form): 8 | name = StringField(u'姓名', validators=[Required(), Length(0, 64)]) 9 | sex = RadioField(u'性别', choices=[('man', u'男'), ('woman', u'女')], default='man') 10 | location = StringField(u'居住地', validators=[Length(0, 64)]) 11 | short_intr = StringField(u'一句话介绍', validators=[Length(0, 30)]) 12 | industry = StringField(u'所在行业', validators=[Length(0, 64)]) 13 | school = StringField(u'学校', validators=[Length(0, 64)]) 14 | discipline = StringField(u'专业方向', validators=[Length(0, 64)]) 15 | introduction = TextAreaField(u'简介', validators=[Length(0, 100)]) 16 | submit = SubmitField(u'确定') -------------------------------------------------------------------------------- /app/main/views.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import base64 3 | import os 4 | 5 | from flask import current_app, jsonify 6 | from flask import redirect, flash 7 | from flask import render_template 8 | from flask import request 9 | from flask import url_for 10 | from flask_login import login_required, current_user 11 | 12 | from app import db 13 | from app.main.forms import EditProfileForm 14 | from app.models.models import User, TopicCategory, Topic, Question, Answer, Comments, Dynamic 15 | from . import main 16 | from app.lib import constant 17 | from app.lib.pagination import base_pagination 18 | 19 | 20 | @main.route("/") 21 | @login_required 22 | def index(): 23 | topic_all = Topic.query.filter_by().all() 24 | return render_template("zheye.html", user=current_user, base64=base64, 25 | topic_all=topic_all, index_show=current_user.current_user_index()) 26 | 27 | 28 | @main.route('/people/') 29 | def people(username): 30 | """个人资料界面""" 31 | user = User.query.filter_by(username=username).first_or_404() 32 | dynamics = Dynamic.search_dynamic(user.id) 33 | return render_template('user.html', user=user, base64=base64, dynamics=dynamics) 34 | 35 | 36 | @main.route('/edit-profile', methods=['POST', 'GET']) 37 | @login_required 38 | def edit_profile(): 39 | """编辑个人资料""" 40 | form = EditProfileForm() 41 | if form.validate_on_submit(): 42 | current_user.name = form.name.data 43 | current_user.location = form.location.data 44 | current_user.sex = form.sex.data 45 | current_user.short_intr = form.short_intr.data 46 | current_user.school = form.school.data 47 | current_user.industry = form.industry.data 48 | current_user.discipline = form.discipline.data 49 | current_user.introduction = form.introduction.data 50 | db.session.add(current_user) 51 | db.session.commit() 52 | 53 | flash(constant.PROFILE_UPDATE) 54 | return redirect(url_for('main.people', username=current_user.username)) 55 | 56 | form.name.data = current_user.name 57 | form.sex.data = current_user.sex or "man" 58 | form.short_intr.data = current_user.short_intr 59 | form.industry.data = current_user.industry 60 | form.school.data = current_user.school 61 | form.discipline.data = current_user.discipline 62 | form.introduction.data = current_user.introduction 63 | form.location.data = current_user.location 64 | 65 | return render_template('edit_profile.html', form=form, user=current_user, base64=base64) 66 | 67 | 68 | @main.route('/follow/') 69 | @login_required 70 | def follow(username): 71 | """关注某人""" 72 | user = User.query.filter_by(username=username).first() 73 | if user is None: 74 | return jsonify(error=constant.INVALID_USER) 75 | if user == current_user: 76 | return jsonify(constant.CANNOT_CON_MYSELF) 77 | if current_user.is_following(user): 78 | return jsonify(error=constant.ALREADY_CON) 79 | try: 80 | current_user.follow(user) 81 | except Exception as e: 82 | return jsonify(constant.FAIL) 83 | 84 | current_user.notify_follower(user.id, "follow_user") 85 | return jsonify(error="") 86 | 87 | 88 | @main.route('/unfollow/') 89 | @login_required 90 | def unfollow(username): 91 | """取消关注""" 92 | user = User.query.filter_by(username=username).first() 93 | if user is None: 94 | return jsonify(error=constant.INVALID_USER) 95 | if not current_user.is_following(user): 96 | return jsonify(error=constant.NO_CON) 97 | try: 98 | current_user.unfollow(user) 99 | except Exception as e: 100 | return jsonify(constant.FAIL) 101 | 102 | return jsonify(error="") 103 | 104 | 105 | @main.route('/people//followers') 106 | def followers(username): 107 | """显示username的关注者""" 108 | user = User.query.filter_by(username=username).first_or_404() 109 | page = request.args.get('page', 1, type=int) 110 | 111 | # 获取分页对象 112 | pagination = base_pagination(user.followers, page, 'FLASKY_FOLLOWERS_PER_PAGE') 113 | follows = [{'user': item.follower} 114 | for item in pagination.items] 115 | who = u'我' if user == current_user else u'他' 116 | return render_template('user_followers.html', user=user, who=who, 117 | endpoint='.followers', pagination=pagination, 118 | follows=follows, base64=base64) 119 | 120 | 121 | @main.route('/people//following') 122 | def following(username): 123 | """分页显示username关注了谁""" 124 | user = User.query.filter_by(username=username).first_or_404() 125 | page = request.args.get('page', 1, type=int) 126 | pagination = base_pagination(user.followed, page, 'FLASKY_FOLLOWERS_PER_PAGE') 127 | follows = [{'user': item.followed} 128 | for item in pagination.items] 129 | who = u'我' if user == current_user else u'他' 130 | return render_template('user_following.html', user=user, who=who, 131 | endpoint='.following', pagination=pagination, 132 | follows=follows, base64=base64) 133 | 134 | 135 | @main.route('/people//asks') 136 | def asks(username): 137 | """分页显示username提了哪些问题""" 138 | user = User.query.filter_by(username=username).first_or_404() 139 | page = request.args.get('page', 1, type=int) 140 | pagination = base_pagination(user.questions, page, 'FLASKY_FOLLOWERS_PER_PAGE') 141 | 142 | return render_template('user_asks.html', user=user, base64=base64, 143 | endpoint='.asks', pagination=pagination, items=pagination.items 144 | ) 145 | 146 | 147 | @main.route('/people//answers') 148 | def answers(username): 149 | """分页显示username回答了哪些问题""" 150 | user = User.query.filter_by(username=username).first_or_404() 151 | page = request.args.get('page', 1, type=int) 152 | pagination = base_pagination(user.answers, page, 'FLASKY_FOLLOWERS_PER_PAGE') 153 | 154 | return render_template('user_answers.html', user=user, base64=base64, 155 | endpoint='.answers', pagination=pagination, items=pagination.items) 156 | 157 | 158 | @main.route('/people//activities') 159 | def activities(username): 160 | """个人动态界面""" 161 | user = User.query.filter_by(username=username).first_or_404() 162 | dynamics = Dynamic.search_dynamic(user.id) 163 | return render_template('user.html', user=user, base64=base64, dynamics=dynamics) 164 | 165 | 166 | @main.route('/people/images', methods=['POST']) 167 | @login_required 168 | def images(): 169 | try: 170 | # 读取图片内容 171 | file = request.files['file'].read() 172 | if current_user.change_avatar(file): 173 | return redirect(url_for("main.people", username=current_user.username)) 174 | except: 175 | pass 176 | # 头像修改失败,提示 177 | flash(constant.AVATAR_MODI_FAIL) 178 | return redirect(url_for("main.index")) 179 | 180 | 181 | @main.route('/topics') 182 | @login_required 183 | def topics(): 184 | """话题广场""" 185 | topic_cate = TopicCategory.query.all() # 获取所有的话题类别 186 | cate_id = request.args.get("cate") # 获取选中类别的id 187 | cate_selete = None 188 | if topic_cate: 189 | if cate_id: 190 | for cate in topic_cate: 191 | if cate.id == int(cate_id): 192 | cate_selete = cate 193 | break 194 | if not cate_selete: 195 | cate_selete = topic_cate[0] 196 | 197 | # return render_template("topics.html", base64=base64, user=current_user, 198 | # topic_cate=topic_cate, topics=topic_cate[0].topics) 199 | return render_template("topics.html", base64=base64, user=current_user, 200 | topic_cate=topic_cate, cate_selete=cate_selete) 201 | 202 | 203 | @main.route('/topic') 204 | @login_required 205 | def topic(): 206 | """话题动态, 用户关注的话题""" 207 | topics = current_user.follow_topics.filter_by().all() # 获取当前用户关注的话题 208 | topic_id = request.args.get("topic") # 获取选择的话题的id 209 | topic_selete = None # 选择的话题默认为None 210 | 211 | if topics: 212 | if topic_id: 213 | for topic in topics: 214 | if topic.topic.id == int(topic_id): 215 | topic_selete = topic.topic 216 | break 217 | if not topic_selete: 218 | topic_selete = topics[0].topic 219 | if topic_id: 220 | flash(constant.NOFOUND) 221 | return render_template("topic.html", base64=base64, user=current_user, 222 | topics=topics, topic_selete=topic_selete) 223 | 224 | 225 | # @main.route('/topics_search', methods=['POST']) 226 | # @login_required 227 | # def topics_search(): 228 | # """查询选中话题类型下的所有话题""" 229 | # cate = request.form.get("topic_cate", None) 230 | # topic_cate = TopicCategory.query.filter_by( 231 | # category_name=cate).first() 232 | # if topic_cate: 233 | # # 返回的json数据包含四个参数: 234 | # # ```topic_name:话题名称``` 235 | # # ```topic_desc:话题描述``` 236 | # # ```id:话题索引``` 237 | # # ```follow or unfollow```:是否被当前用户关注 238 | # return jsonify(topics=[[topic.topic_name, topic.topic_desc if topic.topic_desc else "", 239 | # str(topic.id), "follow" if current_user.is_following_topic(topic) else "unfollow"] 240 | # for topic in topic_cate.topics]) 241 | # 242 | # return "error" 243 | 244 | 245 | @main.route('/topic_all') 246 | @login_required 247 | def topic_all(): 248 | """返回所有的话题,初始化问题中的话题选择框""" 249 | topics = Topic.query.filter_by().all() 250 | return jsonify(topics=[[topic.id, topic.topic_name] for topic in topics]) 251 | 252 | 253 | @main.route('/follow_topic/') 254 | @login_required 255 | def follow_topic(topic_id): 256 | """关注某个话题""" 257 | topic = Topic.query.filter_by(id=topic_id).first() 258 | if topic is None or current_user.is_following_topic(topic): 259 | return jsonify(error=constant.FAIL) 260 | 261 | # 关注话题 262 | try: 263 | current_user.follow_topic(topic) 264 | except Exception as e: 265 | return jsonify(error=constant.FAIL) 266 | 267 | current_user.add_dynamic(current_user.id, topic.id, 268 | "topic") # 增加关注话题动态记录 269 | current_user.notify_follower(topic.id, "follow_topic") 270 | return jsonify(error="") 271 | 272 | 273 | @main.route('/unfollow_topic/') 274 | @login_required 275 | def unfollow_topic(topic_id): 276 | """取消关注某个话题""" 277 | topic = Topic.query.filter_by(id=topic_id).first() 278 | if topic is None or not current_user.is_following_topic(topic): 279 | return jsonify(error=constant.FAIL) 280 | 281 | # 取消关注 282 | try: 283 | current_user.unfollow_topic(topic) 284 | return jsonify(error="") 285 | except Exception as e: 286 | return jsonify(error=constant.FAIL) 287 | 288 | 289 | @main.route('/follow_question/') 290 | @login_required 291 | def follow_question(question_id): 292 | """关注某个问题""" 293 | question = Question.query.filter_by(id=question_id).first() 294 | if question is None or current_user.is_following_question(question): 295 | return jsonify(error=constant.FAIL) 296 | 297 | # 关注问题 298 | try: 299 | current_user.follow_question(question) 300 | except Exception as e: 301 | return jsonify(error=constant.FAIL) 302 | 303 | current_user.add_dynamic(current_user.id, question.id, 304 | "question") # 增加关注问题动态记录 305 | current_user.notify_follower(question.id, "follow_ques") 306 | return jsonify(error="") 307 | 308 | 309 | @main.route('/unfollow_question/') 310 | @login_required 311 | def unfollow_question(question_id): 312 | """取消关注某个问题""" 313 | question = Question.query.filter_by(id=question_id).first() 314 | if question is None or not current_user.is_following_question(question): 315 | return jsonify(error=constant.FAIL) 316 | 317 | # 取消关注 318 | try: 319 | current_user.unfollow_question(question) 320 | return jsonify(error="") 321 | except Exception as e: 322 | return jsonify(error=constant.FAIL) 323 | 324 | 325 | @main.route('/submit_question', methods=['POST']) 326 | @login_required 327 | def submit_question(): 328 | question = request.form.get("question") 329 | question_desc = request.form.get("question_desc") 330 | topic = request.form.get("topic") 331 | 332 | if topic == None or topic == "": 333 | return jsonify(error=constant.NOT_VALID_CHOICE) 334 | if len(question) > 60 or len(question_desc) > 500: 335 | return jsonify(error=constant.QUESTION_ERROR) 336 | 337 | # 添加问题 338 | result = Question.add_question(question, question_desc, topic, current_user.id) 339 | if not result: # 操作失败 340 | return jsonify(error=constant.FAIL) 341 | 342 | current_user.follow_question(result) # 提问者默认关注提出的问题 343 | current_user.notify_follower(result.id, "ask") 344 | return jsonify(result=result.id, error="") 345 | 346 | 347 | @main.route('/submit_comment', methods=['POST']) 348 | @login_required 349 | def submit_comment(): 350 | answer_id = request.form.get("answer_id") 351 | comment_body = request.form.get("comment_body") 352 | if not answer_id or len(comment_body) > 200: 353 | return jsonify(error=constant.COMMENT_ERROR) 354 | 355 | # 添加评论 356 | result = Comments.add_comment(answer_id, comment_body, current_user.id) 357 | if not result: 358 | return jsonify(error=constant.FAIL) 359 | return jsonify(error="", username=current_user.username, comment=comment_body) 360 | 361 | 362 | @main.route('/topic/') 363 | @login_required 364 | def topic_detail(id): 365 | """话题详细页面""" 366 | topic = Topic.query.get_or_404(id) 367 | 368 | return render_template("topic_detail.html", topic=topic, count=topic.follow_topics.count(), 369 | base64=base64, questions_excellans=topic.questions_excellans()) 370 | 371 | 372 | @main.route('/question') 373 | @login_required 374 | def answer_question(): 375 | """回答问题页面,默认显示关注话题下的问题""" 376 | topics_all = current_user.follow_topics.filter_by().all() 377 | return render_template("answer_questions.html", base64=base64, topics=topics_all) 378 | 379 | 380 | @main.route('/question/following') 381 | @login_required 382 | def question_follow_all(): 383 | questions = current_user.follow_questions.filter_by().all() 384 | return render_template("question_follow_all.html", questions=questions, base64=base64) 385 | 386 | 387 | @main.route('/question/') 388 | @login_required 389 | def question_detail(id): 390 | question = Question.query.get_or_404(id) 391 | 392 | question.ping() # 增加问题的浏览次数 393 | return render_template("question_detail.html", question=question, base64=base64) 394 | 395 | 396 | @main.route('/answer_submit', methods=['POST']) 397 | def answer_submit(): 398 | answer_body = request.form.get("write_answer") 399 | question_id = request.form.get("question_id") 400 | if not answer_body or not question_id: 401 | flash(constant.FAIL) 402 | return redirect(url_for('main.question_detail', id=question_id)) 403 | 404 | flag = Answer.answer_question(current_user.id, question_id, answer_body) 405 | if not flag: 406 | flash(constant.FAIL) 407 | else: 408 | current_user.notify_follower(flag.id, "answer") 409 | return redirect(url_for('main.question_detail', id=question_id)) 410 | 411 | 412 | @main.route('/delete/answer/') 413 | def delete_answer(id): 414 | answer = Answer.query.filter_by(id=id).first() 415 | if answer is None or answer.users != current_user: 416 | return jsonify(error=constant.FAIL) 417 | db.session.delete(answer) 418 | try: 419 | db.session.commit() 420 | return jsonify(error="") 421 | except: 422 | return jsonify(error=constant.FAIL) 423 | 424 | 425 | @main.route('/topic//followers') 426 | def topic_followers(id): 427 | """显示某个话题的所有关注者""" 428 | topic = Topic.query.get_or_404(id) 429 | return render_template('alluser_follow_topic.html', base64=base64, topic=topic) 430 | 431 | 432 | @main.route('/question//followers') 433 | def question_followers(id): 434 | """显示某个话题的所有关注者""" 435 | question = Question.query.get_or_404(id) 436 | return render_template('alluser_follow_question.html', base64=base64, question=question) 437 | 438 | 439 | @main.route('/explore') 440 | @login_required 441 | def explore(): 442 | """发现""" 443 | recommend_quwstions = Question.recommend() 444 | return render_template("explore.html", base64=base64, 445 | questions_excellans=recommend_quwstions) 446 | -------------------------------------------------------------------------------- /app/models/OperateModel.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from app import db 3 | 4 | 5 | class OperateModel(object): 6 | """ 7 | 定义数据提交到数据库以及提交异常的操作, 8 | """ 9 | def db_commit(self): 10 | try: 11 | db.session.commit() 12 | return True 13 | except Exception as e: 14 | print e 15 | db.session.rollback() # 回滚 16 | return False 17 | 18 | def db_delete(self, orm_object): 19 | db.session.delete(orm_object) 20 | return self.db_commit() 21 | 22 | def db_add(self, orm_object): 23 | db.session.add(orm_object) 24 | return self.db_commit() 25 | 26 | 27 | operate_model = OperateModel() 28 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/models/__init__.py -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/default.jpg -------------------------------------------------------------------------------- /app/static/images/e82bab09c_s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/e82bab09c_s.jpg -------------------------------------------------------------------------------- /app/static/images/logo.6837e927.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/logo.6837e927.png -------------------------------------------------------------------------------- /app/static/images/logo_black_trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/logo_black_trans.png -------------------------------------------------------------------------------- /app/static/images/new_logo.ede2316d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/new_logo.ede2316d.png -------------------------------------------------------------------------------- /app/static/images/new_logo@2x.9187366b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/new_logo@2x.9187366b.png -------------------------------------------------------------------------------- /app/static/images/sprites-1.9.2.4c54885a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/sprites-1.9.2.4c54885a.png -------------------------------------------------------------------------------- /app/static/images/sprites.auto.2bb79a7e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/sprites.auto.2bb79a7e.png -------------------------------------------------------------------------------- /app/static/images/user_images.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathbugua/zheye/d93b3e8c2c176daa9f381f71f9c1afafdb8f34c8/app/static/images/user_images.jpg -------------------------------------------------------------------------------- /app/static/js/demo.js: -------------------------------------------------------------------------------- 1 | define(['jquery'], function() { 2 | (function () { 3 | var doc = $(document); 4 | var win = $(window); 5 | // 多次使用, 缓存起来 6 | doc.on('click', '.unfold', function () { 7 | var unfold = $(this); 8 | if (unfold.text() !== '收起') { 9 | unfold.text('收起').siblings('.part-content').hide().siblings('.all-content').show(); 10 | var panel = unfold.parent(); 11 | var panelScroll = panel.offset().top + panel.height(); 12 | var scrollHeight = doc.scrollTop() + win.height(); 13 | var right = win.width() / 2 - 350 + 20 > 20 ? win.width() / 2 - 350 + 20 : 20; 14 | if (scrollHeight - panelScroll < 50) { 15 | unfold.addClass('fold-fix').css('right', right); 16 | } 17 | // scroll 事件性能优化 18 | // 鼠标滚动时 scroll 事件触发的间隔大约为 10~20 ms, 相对于其他的鼠标、键盘事件,它被触发的频率很高,间隔很近。 19 | // 如果 scroll 事件涉及大量的位置计算、元素重绘等工作,且这些工作无法在下个 scroll 事件触发前完成,就会导致浏览器掉帧 20 | // 1. 因此需要减少绑定给 scroll 中具体想要执行的业务逻辑的执行次数 21 | // 2. 并将对象初始化、不变的高度值等缓存在 scroll 事件外部 22 | // 存在 bug : 当以很快的速度滚动时,有可能执行不到 scroll 绑定的事件 23 | var cb = { 24 | onscroll: function() { 25 | var panelScroll = panel.offset().top + panel.height(); 26 | var scrollHeight = doc.scrollTop() + win.height(); 27 | var right = win.width() / 2 - 350 + 20 > 20 ? win.width() / 2 - 350 + 20 : 20; 28 | if (scrollHeight - panelScroll < 50 && 29 | panel.offset().top - scrollHeight < -90 && unfold.text() !== '查看全部') { 30 | unfold.addClass('fold-fix').css('right', right); 31 | } else { 32 | changeStyle(unfold); 33 | } 34 | win.off("scroll", cb.onscroll); 35 | setTimeout(function() { 36 | win.on("scroll", cb.onscroll); 37 | }, 50); 38 | } 39 | }; 40 | win.on("scroll", cb.onscroll); 41 | 42 | // win.on('scroll', function () { 43 | // var panelScroll = panel.offset().top + panel.height(); 44 | // var scrollHeight = doc.scrollTop() + win.height(); 45 | // if (scrollHeight - panelScroll < 50 && 46 | // panel.offset().top - scrollHeight < -90 && unfold.text() !== '展开') { 47 | // unfold.addClass('fold-fix'); 48 | // unfold.css('right', right); 49 | // } else { 50 | // changeStyle(unfold); 51 | // } 52 | // }) 53 | 54 | } else { 55 | var fold = $(this); 56 | changeStyle(fold); 57 | fold.text('查看全部').siblings('.part-content').show() 58 | .siblings('.all-content').hide(); 59 | } 60 | }); 61 | 62 | function changeStyle(i) { 63 | i.removeClass('fold-fix').css('right', '20px'); 64 | } 65 | })() 66 | }); 67 | -------------------------------------------------------------------------------- /app/static/js/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'jquery': '01-jquery-1.11.3.min' 4 | } 5 | }); 6 | 7 | require(['demo']); 8 | -------------------------------------------------------------------------------- /app/static/js/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.2.0 Copyright jQuery Foundation and other contributors. 3 | Released under MIT license, http://github.com/requirejs/requirejs/LICENSE 4 | */ 5 | var requirejs,require,define; 6 | (function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!== 18 | ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c); 19 | this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})|| 20 | ""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0=== 21 | a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a, 22 | b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id); 23 | b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b= 24 | a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&& 25 | (p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs", 26 | "Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){}, 35 | 0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&& 36 | (d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this); 37 | -------------------------------------------------------------------------------- /app/static/style/errors.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | color: #222; 4 | font: 16px/1.7 'Helvetica Neue', Helvetica, Arial, Sans-serif; 5 | background: #eff2f5; 6 | } 7 | 8 | img { 9 | border: none; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | color: #105cb6; 15 | } 16 | 17 | a:hover { 18 | text-decoration: underline; 19 | } 20 | 21 | .error { 22 | margin: 169px auto 0; 23 | width: 404px; 24 | } 25 | 26 | .error-wide { 27 | width: 500px; 28 | } 29 | 30 | @media (max-width: 500px) { 31 | .error { 32 | width: 98%; 33 | } 34 | } 35 | 36 | .error .header { 37 | overflow: hidden; 38 | font-size: 1.8em; 39 | line-height: 1.2; 40 | margin: 0 0 .33em .33em; 41 | } 42 | 43 | .error .header img { 44 | vertical-align: text-bottom; 45 | } 46 | 47 | .error .header .mute { 48 | color: #999; 49 | font-size: .5em; 50 | } 51 | 52 | .error hr { 53 | margin: 1.3em 0; 54 | } 55 | 56 | .error p { 57 | margin: 0 0 1.7em; 58 | color: #999; 59 | } 60 | 61 | .error p:last-child { 62 | margin-bottom: 0; 63 | } 64 | 65 | .error strong { 66 | font-size: 1.1em; 67 | color: #000; 68 | } 69 | 70 | .error .content { 71 | padding: 2em 1.25em; 72 | border: 1px solid #babbbc; 73 | border-radius: 5px; 74 | background: #f7f7f7; 75 | text-align: center; 76 | } 77 | 78 | .error .content .single { 79 | margin: 3em 0; 80 | font-size: 1.1em; 81 | color: #666; 82 | } -------------------------------------------------------------------------------- /app/static/style/mycss.css: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | width: auto; 4 | } 5 | 6 | .top-nav-dropdowntop-nav-dropdown-other{ 7 | display: inline; 8 | } 9 | 10 | 11 | /*user_pages*/ 12 | .App-main { 13 | display: block; 14 | } 15 | 16 | .ProfileHeader { 17 | position: relative; 18 | width: 1000px; 19 | padding: 0 16px; 20 | margin: 10px auto; 21 | } 22 | 23 | .Card:last-child { 24 | margin-bottom: 0; 25 | } 26 | 27 | .Card { 28 | margin-bottom: 10px; 29 | background: #fff; 30 | border: 1px solid #e7eaf1; 31 | border-radius: 2px; 32 | box-shadow: 0 1px 3px rgba(0,37,55,.05); 33 | box-sizing: border-box; 34 | } 35 | 36 | .ProfileHeader-userCover { 37 | width: 100%; 38 | } 39 | 40 | .UserCoverEditor { 41 | position: relative; 42 | } 43 | 44 | .UserCover { 45 | position: relative; 46 | height: 240px; 47 | overflow: hidden; 48 | background: #f7f8fa; 49 | border-top-right-radius: 1px; 50 | border-top-left-radius: 1px; 51 | -webkit-transition: height .3s; 52 | transition: height .3s; 53 | } 54 | 55 | .UserCover-image, .UserCover-image img { 56 | width: 100%; 57 | height: 100%; 58 | -o-object-fit: cover; 59 | object-fit: cover; 60 | } 61 | 62 | .UserCover-image { 63 | -webkit-transition: -webkit-transform 6s ease-out; 64 | transition: -webkit-transform 6s ease-out; 65 | transition: transform 6s ease-out; 66 | transition: transform 6s ease-out,-webkit-transform 6s ease-out; 67 | } 68 | 69 | .ProfileHeader-wrapper { 70 | position: relative; 71 | width: 100%; 72 | background: #fff; 73 | box-sizing: border-box; 74 | } 75 | 76 | .ProfileHeader-main { 77 | position: relative; 78 | margin: 0 20px 24px; 79 | } 80 | 81 | .ProfileHeader-content { 82 | padding-top: 16px; 83 | padding-left: 32px; 84 | border-left: 164px solid transparent; 85 | } 86 | 87 | .ProfileHeader-contentHead { 88 | position: relative; 89 | padding-right: 106px; 90 | margin-bottom: 16px; 91 | } 92 | 93 | .ProfileHeader-title { 94 | -webkit-box-flex: 1; 95 | -ms-flex: 1; 96 | flex: 1; 97 | overflow: hidden; 98 | text-overflow: ellipsis; 99 | white-space: nowrap; 100 | } 101 | 102 | h1, h2, h3 { 103 | margin: 0; 104 | font: inherit; 105 | } 106 | 107 | h1 { 108 | display: block; 109 | font-size: 2em; 110 | -webkit-margin-before: 0.67em; 111 | -webkit-margin-after: 0.67em; 112 | -webkit-margin-start: 0px; 113 | -webkit-margin-end: 0px; 114 | font-weight: bold; 115 | } 116 | 117 | .ProfileHeader-contentBody { 118 | position: relative; 119 | width: 524px; 120 | overflow: hidden; 121 | -webkit-transition: height .3s; 122 | transition: height .3s; 123 | } 124 | 125 | .ProfileHeader-detail { 126 | width: 100%; 127 | font-size: 14px; 128 | line-height: 1.8; 129 | color: #262626; 130 | } 131 | 132 | .ProfileHeader-detailItem { 133 | display: -webkit-box; 134 | display: -ms-flexbox; 135 | display: flex; 136 | margin-bottom: 18px; 137 | } 138 | 139 | .ProfileHeader-detailLabel { 140 | width: 60px; 141 | margin-right: 37px; 142 | font-weight: 500; 143 | } 144 | 145 | .ProfileHeader-detailValue { 146 | -webkit-box-flex: 1; 147 | -ms-flex: 1; 148 | flex: 1; 149 | overflow: hidden; 150 | } 151 | 152 | .ProfileHeader-contentFooter { 153 | position: relative; 154 | padding-top: 8px; 155 | } 156 | 157 | .ProfileHeader-buttons { 158 | position: absolute; 159 | right: 0; 160 | bottom: 0; 161 | } 162 | 163 | .Button--blue { 164 | color: #0f88eb; 165 | border: 1px solid #0f88eb; 166 | } 167 | 168 | .Button { 169 | display: inline-block; 170 | /*padding: 0 16px;*/ 171 | font-size: 14px; 172 | line-height: 32px; 173 | color: #8590a6; 174 | text-align: center; 175 | cursor: pointer; 176 | border: 1px solid #ccd8e1; 177 | border-radius: 3px; 178 | background: none; 179 | } 180 | 181 | .top-nav-profile-login { 182 | float: right; 183 | position: relative; 184 | min-width: 120px; 185 | margin-left: 15px; 186 | z-index: 10; 187 | } 188 | 189 | .top-nav-profile-login a{ 190 | color: #f4f4f4; 191 | } 192 | 193 | .Profile-main { 194 | display: -webkit-box; 195 | display: -ms-flexbox; 196 | display: flex; 197 | width: 1000px; 198 | min-height: 100vh; 199 | padding: 0 16px; 200 | margin: 10px auto; 201 | -webkit-box-pack: justify; 202 | -ms-flex-pack: justify; 203 | justify-content: space-between; 204 | -webkit-box-align: start; 205 | -ms-flex-align: start; 206 | align-items: flex-start; 207 | } 208 | 209 | .Profile-mainColumn { 210 | width: 694px; 211 | } 212 | 213 | .Profile-sideColumn { 214 | width: 296px; 215 | color: #555; 216 | } 217 | 218 | .FollowshipCard { 219 | font-size: 14px; 220 | } 221 | 222 | .Card { 223 | margin-bottom: 10px; 224 | background: #fff; 225 | border: 1px solid #e7eaf1; 226 | border-radius: 2px; 227 | box-shadow: 0 1px 3px rgba(0,37,55,.05); 228 | box-sizing: border-box; 229 | } 230 | 231 | .NumberBoard { 232 | display: -webkit-box; 233 | display: -ms-flexbox; 234 | display: flex; 235 | -webkit-box-align: center; 236 | -ms-flex-align: center; 237 | align-items: center; 238 | text-align: center; 239 | } 240 | 241 | .NumberBoard { 242 | display: -webkit-box; 243 | display: -ms-flexbox; 244 | display: flex; 245 | -webkit-box-align: center; 246 | -ms-flex-align: center; 247 | align-items: center; 248 | text-align: center; 249 | } 250 | 251 | .FollowshipCard { 252 | font-size: 14px; 253 | } 254 | 255 | .Profile-sideColumn { 256 | width: 296px; 257 | color: #555; 258 | } 259 | 260 | .FollowshipCard-counts .NumberBoard-item { 261 | padding: 16px 0; 262 | } 263 | 264 | .NumberBoard-item { 265 | -webkit-box-flex: 1; 266 | -ms-flex: 1; 267 | flex: 1; 268 | } 269 | 270 | .Button--link, .Button--plain { 271 | height: auto; 272 | padding: 0; 273 | line-height: inherit; 274 | background-color: transparent; 275 | border: none; 276 | border-radius: 0; 277 | } 278 | 279 | .NumberBoard-name { 280 | font-size: 14px; 281 | line-height: 20px; 282 | color: #8590a6; 283 | } 284 | 285 | .NumberBoard-value { 286 | margin-top: 4px; 287 | font-size: 18px; 288 | font-weight: 500; 289 | line-height: 24px; 290 | color: #262626; 291 | } 292 | 293 | .ProfileHeader-contentFooter { 294 | position: relative; 295 | padding-top: 8px; 296 | } 297 | 298 | .ProfileHeader-expandButton { 299 | line-height: 1; 300 | } 301 | 302 | .ProfileHeader-buttons { 303 | position: absolute; 304 | right: 0; 305 | bottom: 0; 306 | } 307 | 308 | .Button--primary.Button--blue, .Button--primary.Button--blue:disabled { 309 | background: #0f88eb; 310 | } 311 | .Button--primary.Button--blue { 312 | color: #fff; 313 | } 314 | .FollowButton { 315 | width: 96px; 316 | } 317 | 318 | .Button--primary.Button--grey, .Button--primary.Button--grey:disabled { 319 | background: #c3ccd9; 320 | border: 1px solid #c3ccd9; 321 | } 322 | .Button--primary.Button--grey { 323 | color: #fff; 324 | } 325 | 326 | /*个人资料动态、关注等的配置*/ 327 | .Card:last-child { 328 | margin-bottom: 0; 329 | } 330 | .Card { 331 | margin-bottom: 10px; 332 | background: #fff; 333 | border: 1px solid #e7eaf1; 334 | border-radius: 2px; 335 | box-shadow: 0 1px 3px rgba(0,37,55,.05); 336 | box-sizing: border-box; 337 | } 338 | 339 | .ProfileMain-tabs { 340 | -webkit-box-flex: 1; 341 | -ms-flex: 1; 342 | flex: 1; 343 | } 344 | 345 | .Tabs { 346 | border-bottom: 1px solid #f0f2f7; 347 | } 348 | 349 | ol, ul { 350 | padding: 0; 351 | margin: 0; 352 | } 353 | 354 | ul, menu, dir { 355 | display: block; 356 | list-style-type: disc; 357 | -webkit-margin-before: 1em; 358 | -webkit-margin-after: 1em; 359 | -webkit-margin-start: 0px; 360 | -webkit-margin-end: 0px; 361 | -webkit-padding-start: 40px; 362 | } 363 | 364 | .Tabs-item { 365 | display: inline-block; 366 | padding: 0 20px; 367 | } 368 | li { 369 | list-style-type: none; 370 | } 371 | 372 | li { 373 | display: list-item; 374 | text-align: -webkit-match-parent; 375 | } 376 | 377 | body:not(.Body--isAppleDevice) .ProfileMain-tabs .is-active { 378 | font-weight: 700; 379 | } 380 | .Tabs-link.is-active { 381 | font-weight: 500; 382 | color: #333; 383 | } 384 | 385 | .Tabs-link { 386 | position: relative; 387 | display: inline-block; 388 | padding: 14px 0; 389 | font-size: 16px; 390 | line-height: 22px; 391 | color: #2e2e2e; 392 | text-align: center; 393 | } 394 | 395 | a { 396 | color: inherit; 397 | text-decoration: none; 398 | } 399 | 400 | 401 | /*关注者与被关注者样式*/ 402 | .Card:last-child { 403 | margin-bottom: 0; 404 | } 405 | .Card { 406 | margin-bottom: 10px; 407 | background: #fff; 408 | border: 1px solid #e7eaf1; 409 | border-radius: 2px; 410 | box-shadow: 0 1px 3px rgba(0,37,55,.05); 411 | box-sizing: border-box; 412 | } 413 | .List-item { 414 | position: relative; 415 | padding: 16px 20px; 416 | font-size: 14px; 417 | } 418 | 419 | .ContentItem-main { 420 | display: -webkit-box; 421 | display: -ms-flexbox; 422 | /*display: flex;*/ 423 | } 424 | .ContentItem-image { 425 | float: left; 426 | margin-right: 20px; 427 | } 428 | avatar .Popover { 429 | display: block; 430 | } 431 | 432 | .Popover { 433 | position: relative; 434 | display: inline-block; 435 | } 436 | 437 | .UserItem-avatar .Avatar, .UserItem-avatar .Popover { 438 | display: block; 439 | } 440 | .Avatar--large { 441 | border-radius: 4px; 442 | } 443 | .Avatar { 444 | background: #fff; 445 | border-radius: 2px; 446 | } 447 | 448 | .UserAvatar .Avatar{ 449 | width: 160px; 450 | height: 160px; 451 | } 452 | img { 453 | width: 60px; 454 | height: 60px; 455 | } 456 | .ContentItem-head { 457 | -webkit-box-flex: 1; 458 | -ms-flex: 1; 459 | flex: 1; 460 | overflow: hidden; 461 | } 462 | .ContentItem-title { 463 | font-size: 18px; 464 | font-weight: 700; 465 | line-height: 1.6; 466 | color: #1e1e1e; 467 | margin-top: -5px; 468 | margin-bottom: -5px; 469 | } 470 | 471 | .UserItem-name, .UserItem-name .UserLink-badge, .UserItem-title { 472 | display: -webkit-box; 473 | display: -ms-flexbox; 474 | display: flex; 475 | } 476 | 477 | .UserItem-title { 478 | -webkit-box-align: center; 479 | -ms-flex-align: center; 480 | align-items: center; 481 | } 482 | 483 | .List-item:not(:last-child):after { 484 | position: absolute; 485 | bottom: 0; 486 | display: block; 487 | width: calc(100% - 32px); 488 | border-bottom: 1px solid #f0f2f7; 489 | content: ""; 490 | } 491 | 492 | .Popover { 493 | position: relative; 494 | display: inline-block; 495 | } 496 | 497 | .ContentItem-status { 498 | margin-top: 5px; 499 | color: #8590a6; 500 | font-size: 14px; 501 | } 502 | 503 | Inherited from div.ContentItem-meta 504 | .ContentItem-meta { 505 | font-size: 15px; 506 | color: #555; 507 | } 508 | 509 | .ContentItem-statusItem:not(:first-child):before { 510 | margin: 0 5px; 511 | content: "\B7"; 512 | } 513 | 514 | 515 | .pagination>li { 516 | display: inline; 517 | } 518 | li { 519 | display: list-item; 520 | text-align: -webkit-match-parent; 521 | } 522 | 523 | li { 524 | list-style-type: none; 525 | } 526 | 527 | .pagination>li:first-child>a, .pagination>li:first-child>span { 528 | margin-left: 0; 529 | border-top-left-radius: 4px; 530 | border-bottom-left-radius: 4px; 531 | } 532 | .pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover, .pagination>.disabled>span, .pagination>.disabled>span:focus, .pagination>.disabled>span:hover { 533 | color: #777; 534 | cursor: not-allowed; 535 | background-color: #fff; 536 | border-color: #ddd; 537 | } 538 | .pagination>li>a, .pagination>li>span { 539 | position: relative; 540 | float: left; 541 | padding: 6px 12px; 542 | margin-left: -1px; 543 | line-height: 1.42857143; 544 | color: #337ab7; 545 | text-decoration: none; 546 | background-color: #fff; 547 | border: 1px solid #ddd; 548 | } 549 | .pagination>.active>a, .pagination>.active>a:focus, .pagination>.active>a:hover, .pagination>.active>span, .pagination>.active>span:focus, .pagination>.active>span:hover { 550 | z-index: 3; 551 | color: #fff; 552 | cursor: default; 553 | background-color: #337ab7; 554 | border-color: #337ab7; 555 | } 556 | .pagination>li>a, .pagination>li>span { 557 | position: relative; 558 | float: left; 559 | padding: 6px 12px; 560 | margin-left: -1px; 561 | line-height: 1.42857143; 562 | color: #337ab7; 563 | text-decoration: none; 564 | background-color: #fff; 565 | border: 1px solid #ddd; 566 | } 567 | 568 | 569 | /*修改头像*/ 570 | .ProfileHeader-avatar { 571 | position: absolute; 572 | top: -25px; 573 | left: 0; 574 | z-index: 4; 575 | } 576 | .UserAvatarEditor { 577 | cursor: pointer; 578 | } 579 | .UserAvatar, .UserAvatar-inner { 580 | vertical-align: top; 581 | } 582 | .UserAvatar { 583 | display: inline-block; 584 | overflow: hidden; 585 | border: 4px solid #fff; 586 | border-radius: 8px; 587 | } 588 | .UserAvatar, .UserAvatar-inner { 589 | vertical-align: top; 590 | } 591 | .Avatar--large { 592 | border-radius: 4px; 593 | } 594 | 595 | .Avatar { 596 | background: #fff; 597 | border-radius: 2px; 598 | } 599 | img { 600 | width: 160px; 601 | height: 160px; 602 | } 603 | .Mask-hidden { 604 | pointer-events: none; 605 | opacity: 0; 606 | } 607 | 608 | .Mask { 609 | position: absolute; 610 | top: 0; 611 | right: 0; 612 | bottom: 0; 613 | left: 0; 614 | z-index: 1; 615 | -webkit-transition: opacity .2s ease-in; 616 | transition: opacity .2s ease-in; 617 | } 618 | .UserAvatarEditor-maskInner { 619 | z-index: 4; 620 | border: 4px solid #fff; 621 | border-radius: 8px; 622 | } 623 | .Mask-mask--black { 624 | background: #000; 625 | } 626 | 627 | .Mask-mask { 628 | position: absolute; 629 | z-index: -1; 630 | width: 100%; 631 | height: 100%; 632 | opacity: .4; 633 | box-sizing: border-box; 634 | } 635 | 636 | .Mask-content { 637 | position: absolute; 638 | top: 50%; 639 | left: 50%; 640 | z-index: 5; 641 | color: #fff; 642 | text-align: center; 643 | -webkit-transform: translate(-50%,-50%); 644 | transform: translate(-50%,-50%); 645 | } 646 | .UserAvatarEditor-cameraIcon { 647 | margin-bottom: 14px; 648 | fill: #fff; 649 | } 650 | .Icon { 651 | vertical-align: text-bottom; 652 | fill: #9fadc7; 653 | } 654 | 655 | html|* > svg { 656 | transform-origin: 50% 50% 0px; 657 | } 658 | 659 | svg:not(:root), symbol, image, marker, pattern, foreignObject { 660 | overflow: hidden; 661 | } 662 | 663 | * { 664 | transform-origin: 0px 0px 0px; 665 | } 666 | input[type="file" ] { 667 | align-items: baseline; 668 | color: inherit; 669 | text-align: start; 670 | } 671 | 672 | input[type="hidden" ], input[type="image" ], input[type="file" ] { 673 | -webkit-appearance: initial; 674 | background-color: initial; 675 | padding: initial; 676 | border: initial; 677 | } 678 | 679 | input { 680 | /*-webkit-appearance: textfield;*/ 681 | background-color: white; 682 | border-image-source: initial; 683 | border-image-slice: initial; 684 | border-image-width: initial; 685 | border-image-outset: initial; 686 | border-image-repeat: initial; 687 | -webkit-rtl-ordering: logical; 688 | -webkit-user-select: text; 689 | cursor: auto; 690 | padding: 1px; 691 | border-width: 2px; 692 | border-style: inset; 693 | border-color: initial; 694 | } 695 | 696 | input, textarea, keygen, select, button { 697 | text-rendering: auto; 698 | color: initial; 699 | letter-spacing: normal; 700 | word-spacing: normal; 701 | text-transform: none; 702 | text-indent: 0px; 703 | text-shadow: none; 704 | display: inline-block; 705 | text-align: start; 706 | margin: 0em 0em 0em 0em; 707 | font: 13.3333px Arial; 708 | } 709 | 710 | input, textarea, keygen, select, button, meter, progress { 711 | -webkit-writing-mode: horizontal-tb; 712 | } 713 | 714 | .zu-top-nav-userinfo .Avatar { 715 | position: absolute; 716 | top: 9px; 717 | left: 10px; 718 | border: 1px solid rgba(0,0,0,.1); 719 | box-shadow: 0 1px 0 rgba(255,255,255,.1); 720 | background-color: transparent; 721 | } 722 | 723 | .UserLink-link .Avatar{ 724 | width: 60px; 725 | height: 60px; 726 | } 727 | .topic-pages .topic-avatar .zm-entry-head-avatar-link, .topic-feed-page .topic-avatar .zm-entry-head-avatar-link { 728 | display: block; 729 | position: relative; 730 | width: 50px; 731 | height: 50px; 732 | } 733 | .topic-pages .topic-avatar .zm-entry-head-avatar-link img.zm-avatar-editor-preview, .topic-feed-page .topic-avatar .zm-entry-head-avatar-link img.zm-avatar-editor-preview { 734 | width: 50px; 735 | height: 50px; 736 | border-radius: 4px; 737 | } 738 | 739 | .topic-info .topic-name, .topic-info .topic-name { 740 | margin: 0 0 8px 65px; 741 | } 742 | 743 | .zm-topic-list-container .feed-main, .zm-topic-list-container .feed-main { 744 | margin-left: 0; 745 | } 746 | .feed-switcher, .feed-switcher { 747 | text-align: right; 748 | padding: 10px 0; 749 | color: #999; 750 | font-size: 12px; 751 | } 752 | 753 | .topic-info .zm-topic-topbar-nav { 754 | margin-left: 65px; 755 | } 756 | .zm-topic-topbar { 757 | margin-top: 4px; 758 | } 759 | -------------------------------------------------------------------------------- /app/static/style/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | .wrap { 6 | margin: 0 auto; 7 | } 8 | .wrap li { 9 | list-style: none; 10 | margin: 20px 0; 11 | padding: 20px 80px 30px 20px; 12 | color: #666; 13 | line-height: 2; 14 | border: 1px solid #81baeb; 15 | box-shadow: 5px 6px 5px #ccc; 16 | position: relative; 17 | } 18 | .content { 19 | text-indent: 2em; 20 | } 21 | .content p { 22 | margin-bottom: 15px; 23 | } 24 | .sign { 25 | font-size: 12px; 26 | padding: 1px 5px; 27 | position: absolute; 28 | bottom: 10px; 29 | right: 20px; 30 | text-align: center; 31 | -webkit-border-radius: 4px; 32 | -moz-border-radius: 4px; 33 | border-radius: 4px; 34 | color: #0c5897; 35 | background-color: #eff6fa; 36 | cursor: pointer; 37 | } 38 | .fold-fix { 39 | color: #fff; 40 | background-color: #81baeb; 41 | position: fixed; 42 | } 43 | h2 { 44 | font-size: 26px; 45 | line-height: 2.5; 46 | } 47 | 48 | .topic-title .Avatar { 49 | width: 40px; 50 | height: 40px; 51 | margin-right: 16px; 52 | } 53 | 54 | .topic-title .topic-title-name { 55 | color: #555; 56 | font-weight: 700; 57 | font-size: 14px; 58 | max-width: 40%; 59 | word-wrap: normal; 60 | white-space: nowrap; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | } 64 | .topic-title { 65 | padding-top: 15px; 66 | line-height: 40px; 67 | margin-bottom: 16px; 68 | } 69 | -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 者也 9 | 10 | 11 | 12 | 13 |
14 |
15 |

16 | 19 | - 404 20 |

21 |
22 |

23 | 你似乎来到了没有知识存在的荒原... 24 |

25 |

来源链接是否正确?用户、话题或问题是否存在?

26 |
27 |

28 | 返回首页 29 |

30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | 服务器出错 -------------------------------------------------------------------------------- /app/templates/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro pagination_widget(pagination, endpoint) %} 2 |
    3 | 4 | 5 | « 6 | 7 | 8 | 9 | {% for p in pagination.iter_pages() %} 10 | {% if p %} 11 | {% if p == pagination.page %} 12 |
  • 13 | {{ p }} 14 |
  • 15 | {% else %} 16 |
  • 17 | {{ p }} 18 |
  • 19 | {% endif %} 20 | {% else %} 21 |
  • 22 | {% endif %} 23 | {% endfor %} 24 | 25 | 26 | » 27 | 28 | 29 |
30 | {% endmacro %} 31 | -------------------------------------------------------------------------------- /app/templates/alluser_follow_question.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}关注[{{question.question_name}}]问题的人-者也{% endblock %} 3 | {% block head %} 4 | {{super()}} 5 | 45 | {% endblock %} 46 | {% block body %} 47 | 48 |
49 | 52 |
53 | 54 | 55 |
56 |
57 | 58 |
59 | {% for topic_follow in question.follow_questions %} 60 |
61 |
62 | {% if topic_follow.users == current_user %} 63 | 64 | {% else %} 65 | {% if not current_user.is_following(topic_follow.users) %} 66 | 关注 68 | {% else %} 69 | 取消关注 71 | {% endif %} 72 | {% endif %} 73 |
74 | 75 | 79 | {% else %} 80 | src="{{url_for('static', filename='images/default.jpg')}}"> 81 | {% endif %} 82 | 83 |
84 | 85 |
86 | {{topic_follow.users.name}} 87 |
88 |
89 | {{topic_follow.users.short_intr or ""}} 90 |
91 | 95 | 96 |
97 |
98 | {% endfor %} 99 |
100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /app/templates/alluser_follow_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}关注[{{topic.topic_name}}]话题的人-者也{% endblock %} 3 | {% block head %} 4 | {{super()}} 5 | 45 | {% endblock %} 46 | {% block body %} 47 |
48 |
49 | 51 | 知乎 53 |
54 |
55 |
56 |
57 |

{{topic.topic_name}}

58 |
59 |
60 |
61 | {{topic.topic_name}}  »  {{topic.follow_topics.count()}}人关注该话题 63 |
64 | 65 |
66 |
67 | 68 |
69 | {% for topic_follow in topic.follow_topics.all() %} 70 |
71 |
72 | {% if topic_follow.users == current_user %} 73 | 74 | {% else %} 75 | {% if not current_user.is_following(topic_follow.users) %} 76 | 关注 78 | {% else %} 79 | 取消关注 81 | {% endif %} 82 | {% endif %} 83 |
84 | 85 | 89 | {% else %} 90 | src="{{url_for('static', filename='images/default.jpg')}}"> 91 | {% endif %} 92 | 93 |
94 | 95 |
96 | {{topic_follow.users.name}} 97 |
98 |
99 | {{topic_follow.users.short_intr or ""}} 100 |
101 | 105 | 106 |
107 |
108 | {% endfor %} 109 |
110 | {% endblock %} 111 | 112 | {% block page_sidebar %} 113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 | 121 | {% if not current_user.is_following_topic(topic) %} 122 | 关注话题 124 | 125 | {% else %} 126 | 取消关注 129 | {% endif %} 130 |
131 | 132 | {{topic.follow_topics.count()}} 133 | 人关注了该话题 134 | 135 |
136 |
137 |
138 | 139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 |

描述 147 |

148 |
150 | 151 |
{{topic.topic_name}}
{{topic.topic_desc 152 | or ""}} 153 |
154 |
155 |
156 |
157 |
158 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/answer_questions.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}回答问题-者也{% endblock %} 3 | {% block body %} 4 |
5 |

推荐以下问题

6 | 7 |
8 |
9 |
10 | {% for topic in topics %} 11 | {% for question in topic.topic.question_topic %} 12 |
13 | 15 |

16 | {{question.question.question_name}}? 17 |

18 |
19 | {{question.question.answers.count()}} 个回答 20 | 21 | {{question.question.follow_questions.count()}} 人关注 22 |
23 |
24 | {% endfor %} 25 | {% endfor %} 26 |
27 |
28 | {% endblock %} 29 | 30 | {% block page_sidebar %} 31 |
32 |
33 |
34 |
35 | 43 |
44 |
45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/add_category.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}添加话题类别{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 | {{ wtf.quick_form(form) }} 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/add_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | {% block title %}添加话题{% endblock %} 5 | 6 | {% block page_content %} 7 | 10 |
11 |
12 |
14 |
15 | {{ form.topic_name(class="form-control", required="True") }} 16 |
17 |
18 | {{ form.topic_desc(class="form-control") }} 19 |
20 |
21 | {{ form.topic_cate(class="form-control") }} 22 |
23 | 24 | 25 | 26 | {{ form.submit(class="btn btn-default") }} 27 | 28 | 29 |
30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/admin_base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %}者也后台管理{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block navbar %} 13 | 42 | {% endblock %} 43 | 44 | {% block content %} 45 |
46 | {% for message in get_flashed_messages() %} 47 |
48 | 49 | {{ message }} 50 |
51 | {% endfor %} 52 | 53 | {% block page_content %}{% endblock %} 54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /app/templates/auth/admin_edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | 4 | 5 | {% block page_content %} 6 | 9 |
10 | {{ wtf.quick_form(form) }} 11 |
12 | {% endblock %} 13 | s -------------------------------------------------------------------------------- /app/templates/auth/admin_index.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | -------------------------------------------------------------------------------- /app/templates/auth/email/change_email.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

To confirm your new email address click here.

3 |

Alternatively, you can paste the following link in your browser's address bar:

4 |

{{ url_for('auth.change_email', token=token, _external=True) }}

5 |

Sincerely,

6 | -------------------------------------------------------------------------------- /app/templates/auth/email/change_email.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | To confirm your new email address click on the following link: 4 | 5 | {{ url_for('auth.change_email', token=token, _external=True) }} 6 | 7 | Sincerely, 8 | 9 | -------------------------------------------------------------------------------- /app/templates/auth/email/confirm.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

To confirm your account please click here.

3 |

Alternatively, you can paste the following link in your browser's address bar:

4 |

{{ url_for('auth.confirm', token=token, _external=True) }}

5 |

Sincerely,

6 | -------------------------------------------------------------------------------- /app/templates/auth/email/confirm.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | 4 | To confirm your account please click on the following link: 5 | 6 | {{ url_for('auth.confirm', token=token, _external=True) }} 7 | 8 | Sincerely, 9 | 10 | 11 | Note: replies to this email address are not monitored. 12 | -------------------------------------------------------------------------------- /app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 者也 - 与世界分享你的知识、经验和见解 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% import "bootstrap/wtf.html" as wtf %} 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |

者也

32 | 33 |

与世界分享你的知识、经验和见解

34 |
35 |
36 | {% for message in get_flashed_messages() %} 37 |
38 | 39 | {{ message }} 40 |
41 | {% endfor %} 42 | 43 |
44 |
45 | 46 | 54 | 55 |
56 | 57 |
58 | {{ wtf.quick_form(form1) }} 59 | 60 | 61 |
62 |
63 | 64 | {{ wtf.quick_form(form2) }} 65 | 66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/templates/auth/manage_category.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% import "_macros.html" as macros %} 3 | {% block title %}管理话题类别{% endblock %} 4 | {% block head %} 5 | {{super()}} 6 | 17 | {% endblock %} 18 | {% block page_content %} 19 | 20 | 21 | {% for cate in items %} 22 | 23 | 26 | 27 | 28 | 29 | {% endfor %} 30 |
类别描述操作
24 | {{cate.category_name}} 25 | {{cate.category_desc}}
31 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /app/templates/auth/manage_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% import "_macros.html" as macros %} 3 | {% block title %}管理话题类别{% endblock %} 4 | {% block head %} 5 | {{super()}} 6 | 17 | {% endblock %} 18 | {% block page_content %} 19 | 20 | 21 | {% for topic in items %} 22 | 23 | 26 | 27 | 28 | 29 | {% endfor %} 30 |
话题描述操作
24 | {{topic.topic_name}} 25 | {{topic.topic_desc}}
31 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /app/templates/auth/manage_users.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/admin_base.html" %} 2 | {% block title %}管理用户{% endblock %} 3 | {% block page_content %} 4 | 5 | 6 | {% for user in users %} 7 | 8 | 11 | 12 | 13 | 14 | {% endfor %} 15 |
用户名邮箱操作
9 | {{user.username}} 10 | {{user.email}}
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /app/templates/auth/unconfirmed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}邮箱验证{% endblock %} 4 | 5 | {% block page_content %} 6 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/templates/base_user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{user.name}}-者也{% endblock %} 4 | {% block head %} 5 | 6 | {{super()}} 7 | 92 | 93 | {% endblock %} 94 | {% block page_content %} 95 |
96 |
97 |
98 |
99 |
100 | 用户封面 102 |
103 |
104 | 105 |
106 |
107 | 108 | {% if user != current_user %} 109 |
110 | {% else %} 111 |
113 | {% endif %} 114 |
115 | 118 | {% else %} 119 | src="{{url_for('static', filename='images/default.jpg')}}"> 120 | {% endif %} 121 |
122 |
123 |
124 | 125 |
126 |
127 | 135 |
136 | 修改我的头像 137 |
138 |
139 |
140 | {% if user == current_user %} 141 |
142 | 144 |
145 | {% endif %} 146 |
147 | 148 | 149 |
150 |
151 |

152 | {{user.name}} 153 | {{user.short_intr or ""}} 154 |

155 |
156 |
157 | 158 |
159 |
160 | 居住地 161 |
162 | 163 | 现居{{user.location or "未设置"}} 164 | 165 | 166 |
167 |
168 |
169 | 170 | 所在行业 171 | 172 |
{{user.industry or "未设置"}}
173 |
174 |
175 | 176 | 教育经历 177 | 178 |
179 |
180 | 182 | {{user.school or ""}} 183 | · 184 | {{user.discipline or ""}} 185 |
186 |
187 |
188 | 189 |
190 | 个人简介 191 |
192 | {{user.introduction or ""}} 193 |
194 |
195 | 196 |
197 |
198 |
199 | 231 | 232 | {% if user == current_user %} 233 | 241 | {% endif %} 242 |
243 |
244 |
245 |
246 |
247 |
248 | 249 |
250 |
251 | 252 | {% block person_data %} 253 | {% endblock %} 254 |
255 | 272 |
273 |
274 | 275 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | {{ super()}} 4 | 5 | {% endblock %} 6 | {% import "bootstrap/wtf.html" as wtf %} 7 | {% block title %}{{user.username}}{% endblock %} 8 | {% block page_content %} 9 |
10 |
11 |
12 |
13 |
14 | 用户封面 16 |
17 |
18 |
19 | {{ wtf.quick_form(form) }} 20 |
21 |
22 |
23 | 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/email_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}邮箱设置{% endblock %} 3 | {% block head %} 4 | {{super()}} 5 | 6 | {% endblock %} 7 | {% block body %} 8 | {% import "bootstrap/wtf.html" as wtf %} 9 |
10 |
11 | 19 |
20 | {{wtf.quick_form(form)}} 21 |
22 | 23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/explore.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block navi_color %} 3 |
  • 4 | 首页 6 |
  • 7 | 8 | 9 |
  • 10 | 话题 11 |
  • 12 | 13 |
  • 14 | 发现 15 |
  • 16 | 17 |
  • 18 | 消息 20 |
  • 21 | {% endblock %} 22 | {% block head %} 23 | {{super()}} 24 | 25 | 26 | 28 | 29 | {% endblock %} 30 | 31 | {% block body %} 32 |
    33 |
    34 |
    35 | 36 | 编辑推荐 37 |
    38 |
    39 |
    40 |
    41 |
    42 | 43 |
    44 | 201 |
    202 |
    203 |
    204 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/password_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}密码设置{% endblock %} 3 | {% block head %} 4 | {{super()}} 5 | 6 | 7 | {% endblock %} 8 | {% block body %} 9 | {% import "bootstrap/wtf.html" as wtf %} 10 |
    11 |
    12 | 21 |
    22 | {{ wtf.quick_form(form) }} 23 |
    24 | 25 |
    26 |
    27 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/question_follow_all.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}我关注的问题-者也{% endblock %} 3 | {% block head %} 4 | {{super()}} 5 | {% endblock %} 6 | {% block body %} 7 |
    8 |
    我关注的问题
    9 |
    10 |
    11 | 42 |
    43 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/topic.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}话题动态-者也{% endblock %} 3 | {% block navi_color %} 4 |
  • 5 | 首页 7 |
  • 8 | 9 | 10 |
  • 11 | 话题 12 |
  • 13 | 14 |
  • 15 | 发现 16 |
  • 17 | 18 |
  • 19 | 消息 21 |
  • 22 | {% endblock %} 23 | {% block head %} 24 | {{super()}} 25 | 26 | 27 | 29 | {% endblock %} 30 | 31 | {% block page_content %} 32 | 39 | 50 | 51 | {% if topic_selete %} 52 | 68 | 69 | 70 |
    71 | 225 |
    226 | {% endif %} 227 | 228 | {% endblock %} 229 | 230 | {% block page_sidebar %} 231 | 241 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/topic_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | {{super()}} 4 | 5 | 6 | 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
    13 |
    14 |
    15 | 16 | 23 |
    24 |
    25 | 26 |
    27 |
    28 |

    {{topic.topic_name}}

    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 | 36 |
    37 | 178 |
    179 |
    180 |
    181 | {% endblock %} 182 | 183 | {% block page_sidebar %} 184 |
    185 |
    186 |
    187 |
    188 | 189 |
    190 |
    191 | 192 | {% if not current_user.is_following_topic(topic) %} 193 | 关注话题 195 | 196 | {% else %} 197 | 取消关注 199 | {% endif %} 200 |
    201 | 202 | {{count}} 人关注了该话题 203 | 204 |
    205 |
    206 |
    207 | 208 | 209 |
    210 |
    211 |
    212 | 213 |
    214 |
    215 |

    描述 216 |

    217 |
    219 | 220 |
    {{topic.topic_name}}
    {{topic.topic_desc 221 | or ""}} 222 |
    223 |
    224 |
    225 |
    226 |
    227 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/topics.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}话题广场-者也{% endblock %} 3 | 4 | {% block page_content %} 5 | 52 |
    53 | 57 | 58 | 71 | 72 | {% if cate_selete %} 73 |
    74 |
    75 | {% for topic in cate_selete.topics %} 76 |
    77 |
    78 | 79 | 87 | {{topic.topic_name}} 88 | 89 |

    {{topic.topic_desc or ""}}

    90 | {% if current_user.is_following_topic(topic) %} 91 | 93 | 94 | {% else %} 95 | 97 | {% endif %} 98 |
    99 |
    100 | {% endfor %} 101 |
    102 |
    103 | {% endif %} 104 |
    105 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base_user.html" %} 2 | {% block head %} 3 | {{super()}} 4 | 5 | {% endblock %} 6 | {% block person_data %} 7 |
    8 |
    9 | 29 |
    30 |
    31 |
    32 |

    我的动态

    33 |
    34 |
    35 |
    36 | {% for dynamic in dynamics %} 37 | {% if dynamic[0] == "question" %} 38 |
    39 |
    40 |
    41 | 关注了问题{{dynamic[2]}} 42 |
    43 |
    44 | 48 |
    49 | {% else %} 50 |
    51 |
    52 |
    53 | 关注了话题{{dynamic[2]}} 54 |
    55 |
    56 |
    57 | 83 |
    84 |
    85 | {% endif %} 86 | {% endfor %} 87 |
    88 |
    89 |
    90 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_answers.html: -------------------------------------------------------------------------------- 1 | {% extends "base_user.html" %} 2 | {% block head %} 3 | {{super()}} 4 | 5 | {% endblock %} 6 | {% import "_macros.html" as macros %} 7 | {% block person_data %} 8 |
    9 |
    10 | 30 |
    31 | 32 |
    33 |
    34 | {% for item in items %} 35 |
    36 |
    37 |

    38 | 39 | {{item.question.question_name}}? 40 | 41 |

    42 |
    43 |
    44 |
    45 | 46 |
    47 | 56 | 57 | 58 |
    59 |
    60 | 67 |
    68 |
    {{item.users.short_intr or ""}}
    69 |
    70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 |

    {{item.answer_body}}

    77 |
    78 | 80 |
    81 |
    82 | 83 | 100 | 101 | {% if current_user == user %} 102 |
    103 | 113 |
    114 | {% endif %} 115 |
    116 |
    117 |
    118 | 119 | 183 |
    184 |
    185 | {% endfor %} 186 |
    187 |
    188 | 191 |
    192 | {% endblock %}} -------------------------------------------------------------------------------- /app/templates/user_asks.html: -------------------------------------------------------------------------------- 1 | {% extends "base_user.html" %} 2 | {% import "_macros.html" as macros %} 3 | {% block head %} 4 | {{super()}} 5 | 6 | {% endblock %} 7 | {% block person_data %} 8 |
    9 |
    10 | 28 |
    29 | 30 |
    31 |
    32 | {% for item in items %} 33 |
    34 |
    35 |

    36 | 41 |

    42 |
    43 | {{item.question_time}} 44 | {{item.answers.count()}} 个回答 45 | {{item.follow_questions.count()}} 个关注 46 |
    47 |
    48 | {% endfor %} 49 |
    50 |
    51 | 52 | 55 |
    56 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_follow_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_user.html" %} 2 | {% import "_macros.html" as macros %} 3 | {% block head %} 4 | {{super()}} 5 | 6 | 7 | {% endblock %} 8 | {% block person_data %} 9 |
    10 |
    11 | 29 | 30 | 31 |
    32 | 33 |
    34 | {% block follow %}{% endblock %} 35 |
    36 | {% for follow in follows %} 37 |
    38 |
    40 |
    41 | 59 |
    60 |

    61 |
    62 | 63 |
    64 |
    69 |
    70 |
    71 |

    72 |
    73 |
    74 |
    {{follow.user.short_intr or ""}}
    75 |
    76 | 0 回答 77 | 0 文章{{follow.user.followers.count()}} 关注者 79 |
    80 |
    81 |
    82 |
    83 | {% if current_user != follow.user %} 84 | {% if current_user.is_anonymous or not current_user.is_following(follow.user) %} 85 | {% if user == current_user %} 86 | 102 | {% else %} 103 | {% if user == current_user %} 104 | 107 | {% else %} 108 | 111 | {% endif %} 112 | {% endif %} 113 | {% endif %} 114 | 115 | 116 |
    117 |
    118 |
    119 | {% endfor %} 120 |
    121 |
    122 | 123 | 126 |
    127 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_followers.html: -------------------------------------------------------------------------------- 1 | {% extends "user_follow_base.html" %} 2 | {% block follow %} 3 |
    4 |
    5 | 10 |
    11 |
    12 |
    13 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_following.html: -------------------------------------------------------------------------------- 1 | {% extends "user_follow_base.html" %} 2 | {% block follow %} 3 |
    4 |
    5 | 9 |
    10 |
    11 |
    12 | {% endblock %} -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | develop = . 3 | index = https://pypi.tuna.tsinghua.edu.cn/simple 4 | newest = false 5 | update-versions-file = versions.cfg 6 | extends = versions.cfg 7 | relative-paths = true 8 | show-picked-versions = true 9 | versions = versions 10 | parts = app 11 | init_db 12 | cleanpyc 13 | 14 | [app] 15 | recipe = zc.recipe.egg 16 | interpreter = python 17 | eggs = zheye 18 | setuptools 19 | 20 | [init_db] 21 | recipe = plone.recipe.command 22 | command = ./bin/init_db 23 | 24 | [cleanpyc] 25 | recipe = plone.recipe.command 26 | command = find ${buildout:directory} -iname '*.pyc' -delete 27 | update-command = ${:command} -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config: 8 | """ 9 | 项目的配置文件类,配置可以都多种选择,```Config```为基类, 10 | 配置公共部分 11 | """ 12 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True # 开启自动commit 13 | SECRET_KEY = os.environ.get("SECRET_KEY") or "hard to guess string" 14 | SQLALCHEMY_TRACK_MODIFICATIONS = False 15 | MAIL_SERVER = 'smtp.163.com' 16 | MAIL_PORT = 25 17 | MAIL_USE_TLS = True 18 | MAIL_USERNAME = 'cl20141205@163.com' 19 | MAIL_PASSWORD = '' 20 | FLASKY_MAIL_SUBJECT_PREFIX = '[zheye]' 21 | FLASKY_MAIL_SENDER = 'Zheye Admin ' 22 | FLASKY_ADMIN = 'cl20141205@163.com' 23 | FLASKY_FOLLOWERS_PER_PAGE = 2 24 | ADMIN_MANAGE = 10 25 | VIEW_MAX = 5 # 问题浏览次数常量 26 | UPLOAD_FOLDER = os.path.join(basedir) 27 | 28 | @staticmethod 29 | def init_app(app): 30 | pass 31 | 32 | 33 | class DevelopmentConfig(Config): 34 | DEBUG = True 35 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 36 | 'sqlite:///' + os.path.join(basedir, 'data.sqlite') 37 | 38 | 39 | class TestingConfig(Config): 40 | pass 41 | 42 | 43 | config = { 44 | "development": DevelopmentConfig, 45 | "testing": TestingConfig, 46 | 47 | "default": DevelopmentConfig 48 | } 49 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from app import create_app 3 | from tornado.wsgi import WSGIContainer 4 | from tornado.httpserver import HTTPServer 5 | from tornado.options import define, options 6 | from tornado.ioloop import IOLoop 7 | from app import db 8 | from app.models.models import Role 9 | 10 | app = create_app("default") 11 | define("port", default=5000, type=int) 12 | define("cmd", default="runserver") 13 | 14 | 15 | def runserver(): 16 | http_server = HTTPServer(WSGIContainer(app)) 17 | http_server.listen(options.port) 18 | print "Server runing on http://0.0.0.0:%d" % options.port 19 | IOLoop.instance().start() 20 | 21 | 22 | def create_db(): 23 | with app.app_context(): 24 | db.create_all() 25 | Role.insert_roles() # 创建角色 26 | 27 | 28 | if __name__ == '__main__': 29 | options.parse_command_line() 30 | if options.cmd == "runserver": 31 | runserver() 32 | elif options.cmd == "create_db": 33 | create_db() 34 | 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0 2 | Flask-Bootstrap==3.0.3.1 3 | Flask-Login==0.3.1 4 | Flask-Mail==0.9.0 5 | Flask-Migrate==1.1.0 6 | Flask-Moment==0.2.1 7 | Flask-SQLAlchemy==1.0 8 | Flask-Script==0.6.6 9 | Flask-WTF==0.9.4 10 | Jinja2==2.7.1 11 | Mako==0.9.1 12 | Markdown==2.3.1 13 | Flask-PageDown==0.1.4 14 | MarkupSafe==0.18 15 | SQLAlchemy==0.9.9 16 | WTForms==1.0.5 17 | Werkzeug==0.15.3 18 | alembic==0.6.2 19 | blinker==1.3 20 | itsdangerous==0.23 21 | six==1.10.0 22 | bleach==3.1.4 23 | click==6.7 24 | html5lib==1.0b3 25 | python-editor==1.0.3 26 | Flask-HTTPAuth==2.7.0 27 | tornado==4.4.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # flake8: noqa 3 | from setuptools import find_packages, setup 4 | 5 | entry_points = """ 6 | [console_scripts] 7 | run_web=manage:runserver 8 | init_db=manage:create_db 9 | """ 10 | 11 | setup( 12 | name='zheye', 13 | version='0.0.1', 14 | license='PRIVATE', 15 | author='', 16 | author_email='', 17 | url='https://github.com/mathbugua/zheye', 18 | description=u'zheye', 19 | packages=find_packages(exclude=['tests']), 20 | zip_safe=False, 21 | install_requires=[ 22 | 'Flask', 23 | 'Flask-Bootstrap', 24 | 'Flask-Login', 25 | 'Flask-Mail', 26 | 'Flask-Migrate', 27 | 'Flask-Moment', 28 | 'Flask-SQLAlchemy', 29 | 'Flask-Script', 30 | 'Flask-WTF', 31 | 'Jinja2', 32 | 'Mako', 33 | 'Markdown', 34 | 'Flask-PageDown', 35 | 'MarkupSafe', 36 | 'SQLAlchemy', 37 | 'WTForms', 38 | 'Werkzeug', 39 | 'alembic', 40 | 'blinker', 41 | 'itsdangerous', 42 | 'six', 43 | 'bleach', 44 | 'click', 45 | 'html5lib', 46 | 'python-editor', 47 | 'Flask-HTTPAuth', 48 | 'tornado', 49 | ], 50 | entry_points=entry_points, 51 | ) 52 | -------------------------------------------------------------------------------- /versions.cfg: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | zc.recipe.egg = 2.0.3 4 | Flask = 0.10.1 5 | Flask-Bootstrap = 3.0.3.1 6 | Flask-Login = 0.3.1 7 | Flask-Mail = 0.9.0 8 | Flask-Migrate = 1.1.0 9 | Flask-Moment = 0.2.1 10 | Flask-SQLAlchemy = 1.0 11 | Flask-Script = 0.6.6 12 | Flask-WTF = 0.9.4 13 | Jinja2 = 2.7.1 14 | Mako = 0.9.1 15 | Markdown = 2.3.1 16 | Flask-PageDown = 0.1.4 17 | MarkupSafe = 0.18 18 | SQLAlchemy = 0.9.9 19 | WTForms = 1.0.5 20 | Werkzeug = 0.10.4 21 | alembic = 0.6.2 22 | blinker = 1.3 23 | itsdangerous = 0.23 24 | six = 1.10.0 25 | bleach = 1.4.0 26 | click = 6.7 27 | html5lib = 1.0b3 28 | python-editor = 1.0.3 29 | Flask-HTTPAuth = 2.7.0 30 | tornado = 4.4.1 31 | # Added by buildout at 2018-11-23 15:53:25.866619 32 | 33 | # Required by: 34 | # tornado==4.4.1 35 | certifi = 2018.10.15 36 | 37 | # Added by buildout at 2018-11-23 15:59:10.651691 38 | plone.recipe.command = 1.1 39 | --------------------------------------------------------------------------------