Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
maftwonfv | 0202f7e601 | 2 weeks ago |
maftwonfv | 02afba5ac4 | 2 weeks ago |
maftwonfv | f41f9dc0ff | 2 weeks ago |
maftwonfv | b7e32b7e62 | 2 weeks ago |
maftwonfv | 6df8ce6348 | 2 weeks ago |
maftwonfv | 3faff17bb1 | 2 weeks ago |
maftwonfv | 6c592afa9d | 2 weeks ago |
maftwonfv | e01c18b426 | 2 weeks ago |
maftwonfv | 7b0aa6a65d | 2 weeks ago |
maftwonfv | 31b3ac701a | 2 weeks ago |
maftwonfv | 861c6adec3 | 2 weeks ago |
maftwonfv | a4e4d1491d | 2 weeks ago |
maftwonfv | 3487cba7c4 | 2 weeks ago |
maftwonfv | 6fdb33c99d | 2 weeks ago |
maftwonfv | bd84a1f1d6 | 2 weeks ago |
p4s7bv5if | ad327e090a | 2 months ago |
After Width: | Height: | Size: 420 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -0,0 +1 @@
|
||||
run.py
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/app/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/flaskProject.iml" filepath="$PROJECT_DIR$/.idea/flaskProject.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,47 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from config import Config
|
||||
import os
|
||||
|
||||
# 初始化扩展
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
|
||||
def create_app(config_class=Config):
|
||||
# 创建Flask应用实例
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# 确保上传目录存在
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
# 初始化扩展
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# 配置登录管理器
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = '请先登录'
|
||||
login_manager.login_message_category = 'info'
|
||||
|
||||
# 注册蓝图
|
||||
from app.routes import auth, main, triage, records, settings, chat
|
||||
app.register_blueprint(auth.bp)
|
||||
app.register_blueprint(main.bp)
|
||||
app.register_blueprint(triage.bp)
|
||||
app.register_blueprint(records.bp)
|
||||
app.register_blueprint(settings.bp)
|
||||
app.register_blueprint(chat.bp)
|
||||
|
||||
# 用户加载函数
|
||||
from app.models.models import User
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
||||
# 创建数据库表
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
return app
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
from . import main
|
||||
from . import auth
|
||||
from . import records
|
||||
from . import triage
|
||||
|
||||
# 确保所有蓝图都被正确导入
|
||||
__all__ = ['main', 'auth', 'records', 'triage']
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,106 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from app.models.models import User, Department
|
||||
from app import db
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
remember = request.form.get('remember', False)
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.check_password(password):
|
||||
login_user(user, remember=remember)
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page or url_for('main.index'))
|
||||
flash('用户名或密码错误')
|
||||
|
||||
return render_template('auth/login.html')
|
||||
|
||||
|
||||
@bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
role = request.form.get('role', 'patient')
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('用户名已存在')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('邮箱已被注册')
|
||||
return redirect(url_for('auth.register'))
|
||||
|
||||
user = User(username=username, email=email, role=role)
|
||||
user.set_password(password)
|
||||
|
||||
if role == 'doctor':
|
||||
department_id = request.form.get('department')
|
||||
title = request.form.get('title')
|
||||
user.department_id = department_id
|
||||
user.title = title
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
flash('注册成功,请登录')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
departments = Department.query.all()
|
||||
return render_template('auth/register.html', departments=departments)
|
||||
|
||||
|
||||
@bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
@bp.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def profile():
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email')
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
|
||||
if current_password and new_password:
|
||||
if current_user.check_password(current_password):
|
||||
current_user.set_password(new_password)
|
||||
flash('密码修改成功')
|
||||
else:
|
||||
flash('当前密码错误')
|
||||
|
||||
if email != current_user.email:
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('邮箱已被使用')
|
||||
else:
|
||||
current_user.email = email
|
||||
flash('邮箱修改成功')
|
||||
|
||||
if current_user.is_doctor():
|
||||
department_id = request.form.get('department')
|
||||
title = request.form.get('title')
|
||||
current_user.department_id = department_id
|
||||
current_user.title = title
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for('auth.profile'))
|
||||
|
||||
departments = Department.query.all()
|
||||
return render_template('auth/profile.html', departments=departments)
|
@ -0,0 +1,96 @@
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from app.models.models import Message, User
|
||||
from app import db
|
||||
|
||||
# 将 chat_bp 改为 bp
|
||||
bp = Blueprint('chat', __name__)
|
||||
|
||||
|
||||
@bp.route('/chat')
|
||||
@login_required
|
||||
def chat_list():
|
||||
"""聊天列表"""
|
||||
if current_user.is_doctor():
|
||||
# 医生查看与其对话的患者列表
|
||||
chats = db.session.query(User).join(
|
||||
Message,
|
||||
(Message.sender_id == User.id) | (Message.receiver_id == User.id)
|
||||
).filter(
|
||||
(Message.sender_id == current_user.id) |
|
||||
(Message.receiver_id == current_user.id)
|
||||
).distinct().all()
|
||||
else:
|
||||
# 患者查看与其对话的医生列表
|
||||
chats = User.query.filter_by(role='doctor').all()
|
||||
|
||||
return render_template('chat/list.html', chats=chats)
|
||||
|
||||
|
||||
@bp.route('/chat/<int:user_id>')
|
||||
@login_required
|
||||
def chat_room(user_id):
|
||||
"""聊天室"""
|
||||
other_user = User.query.get_or_404(user_id)
|
||||
|
||||
# 获取聊天记录
|
||||
messages = Message.query.filter(
|
||||
((Message.sender_id == current_user.id) & (Message.receiver_id == user_id)) |
|
||||
((Message.sender_id == user_id) & (Message.receiver_id == current_user.id))
|
||||
).order_by(Message.created_at.asc()).all()
|
||||
|
||||
# 标记消息为已读
|
||||
unread_messages = Message.query.filter_by(
|
||||
receiver_id=current_user.id,
|
||||
sender_id=user_id,
|
||||
read=False
|
||||
).all()
|
||||
|
||||
for msg in unread_messages:
|
||||
msg.read = True
|
||||
db.session.commit()
|
||||
|
||||
return render_template('chat/room.html', other_user=other_user, messages=messages)
|
||||
|
||||
|
||||
@bp.route('/chat/send', methods=['POST'])
|
||||
@login_required
|
||||
def send_message():
|
||||
"""发送消息"""
|
||||
try:
|
||||
receiver_id = request.form.get('receiver_id')
|
||||
content = request.form.get('content')
|
||||
|
||||
if not all([receiver_id, content]):
|
||||
return jsonify({'success': False, 'message': '参数不完整'})
|
||||
|
||||
message = Message(
|
||||
sender_id=current_user.id,
|
||||
receiver_id=receiver_id,
|
||||
content=content
|
||||
)
|
||||
db.session.add(message)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': {
|
||||
'id': message.id,
|
||||
'content': message.content,
|
||||
'created_at': message.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
|
||||
@bp.route('/chat/unread_count')
|
||||
@login_required
|
||||
def get_unread_count():
|
||||
"""获取未读消息数"""
|
||||
count = Message.query.filter_by(
|
||||
receiver_id=current_user.id,
|
||||
read=False
|
||||
).count()
|
||||
return jsonify({'count': count})
|
@ -0,0 +1,71 @@
|
||||
from flask import Blueprint, render_template, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
from app.models.models import ChatHistory
|
||||
from app import db
|
||||
from app.ai_service import ask
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return render_template('main/index.html')
|
||||
|
||||
|
||||
@bp.route('/ai_consult', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def ai_consult():
|
||||
if request.method == 'POST':
|
||||
question = request.form.get('question')
|
||||
|
||||
# 保存用户问题
|
||||
user_message = ChatHistory(
|
||||
user_id=current_user.id,
|
||||
message_type='user',
|
||||
message=question
|
||||
)
|
||||
db.session.add(user_message)
|
||||
|
||||
# 获取AI回答
|
||||
try:
|
||||
ai_response = ask(question)
|
||||
|
||||
# 保存AI回答
|
||||
ai_message = ChatHistory(
|
||||
user_id=current_user.id,
|
||||
message_type='ai',
|
||||
message=ai_response
|
||||
)
|
||||
db.session.add(ai_message)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'answer': ai_response
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
# 获取历史聊天记录
|
||||
chat_history = ChatHistory.query.filter_by(
|
||||
user_id=current_user.id
|
||||
).order_by(ChatHistory.created_at.desc()).limit(50).all()
|
||||
|
||||
return render_template('main/ai_consult.html', chat_history=chat_history)
|
||||
|
||||
|
||||
@bp.route('/chat_history')
|
||||
@login_required
|
||||
def chat_history():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
pagination = ChatHistory.query.filter_by(
|
||||
user_id=current_user.id
|
||||
).order_by(ChatHistory.created_at.desc()).paginate(
|
||||
page=page, per_page=20, error_out=False
|
||||
)
|
||||
|
||||
return render_template('main/chat_history.html', pagination=pagination)
|
@ -0,0 +1,111 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from app.models.models import MedicalRecord, Department, User
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
|
||||
bp = Blueprint('records', __name__)
|
||||
|
||||
|
||||
@bp.route('/medical_records')
|
||||
@login_required
|
||||
def medical_records():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
if current_user.is_doctor():
|
||||
# 医生查看其创建的病历
|
||||
pagination = MedicalRecord.query.filter_by(
|
||||
doctor_id=current_user.id
|
||||
).order_by(MedicalRecord.created_at.desc()).paginate(
|
||||
page=page, per_page=10, error_out=False
|
||||
)
|
||||
else:
|
||||
# 患者查看自己的病历
|
||||
pagination = MedicalRecord.query.filter_by(
|
||||
patient_id=current_user.id
|
||||
).order_by(MedicalRecord.created_at.desc()).paginate(
|
||||
page=page, per_page=10, error_out=False
|
||||
)
|
||||
|
||||
return render_template('records/list.html', pagination=pagination)
|
||||
|
||||
|
||||
@bp.route('/medical_records/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_record():
|
||||
if not current_user.is_doctor():
|
||||
flash('只有医生可以创建病历', 'warning')
|
||||
return redirect(url_for('records.medical_records'))
|
||||
|
||||
# 获取所有患者列表(仅普通用户,非医生)
|
||||
patients = User.query.filter_by(role='patient').all()
|
||||
|
||||
# 获取所有科室列表
|
||||
departments = Department.query.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
record = MedicalRecord(
|
||||
patient_id=request.form.get('patient_id'),
|
||||
doctor_id=current_user.id,
|
||||
department_id=request.form.get('department_id', current_user.department_id),
|
||||
chief_complaint=request.form.get('chief_complaint'),
|
||||
present_illness=request.form.get('present_illness'),
|
||||
past_history=request.form.get('past_history'),
|
||||
diagnosis=request.form.get('diagnosis'),
|
||||
treatment=request.form.get('treatment'),
|
||||
prescription=request.form.get('prescription'),
|
||||
notes=request.form.get('notes'),
|
||||
status='completed'
|
||||
)
|
||||
|
||||
db.session.add(record)
|
||||
db.session.commit()
|
||||
flash('病历创建成功', 'success')
|
||||
return redirect(url_for('records.medical_records'))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'病历创建失败:{str(e)}', 'danger')
|
||||
|
||||
return render_template('records/new.html',
|
||||
patients=patients,
|
||||
departments=departments)
|
||||
|
||||
|
||||
@bp.route('/medical_records/<int:id>')
|
||||
@login_required
|
||||
def view_record(id):
|
||||
record = MedicalRecord.query.get_or_404(id)
|
||||
|
||||
# 检查访问权限
|
||||
if not (current_user.is_doctor() or record.patient_id == current_user.id):
|
||||
flash('您没有权限查看此病历')
|
||||
return redirect(url_for('records.medical_records'))
|
||||
|
||||
return render_template('records/view.html', record=record)
|
||||
|
||||
|
||||
@bp.route('/medical_records/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_record(id):
|
||||
if not current_user.is_doctor():
|
||||
flash('只有医生可以编辑病历')
|
||||
return redirect(url_for('records.medical_records'))
|
||||
|
||||
record = MedicalRecord.query.get_or_404(id)
|
||||
|
||||
if request.method == 'POST':
|
||||
record.chief_complaint = request.form.get('chief_complaint')
|
||||
record.present_illness = request.form.get('present_illness')
|
||||
record.past_history = request.form.get('past_history')
|
||||
record.diagnosis = request.form.get('diagnosis')
|
||||
record.treatment = request.form.get('treatment')
|
||||
record.prescription = request.form.get('prescription')
|
||||
record.notes = request.form.get('notes')
|
||||
record.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
flash('病历更新成功')
|
||||
return redirect(url_for('records.view_record', id=id))
|
||||
|
||||
return render_template('records/edit.html', record=record)
|
@ -0,0 +1,57 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
from app import db
|
||||
from app.models.models import UserProfile
|
||||
|
||||
# 将 settings_bp 改为 bp
|
||||
bp = Blueprint('settings', __name__)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
"""检查文件类型是否允许"""
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@bp.route('/settings', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def settings():
|
||||
"""用户设置页面"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# 处理头像上传
|
||||
if 'avatar' in request.files:
|
||||
file = request.files['avatar']
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
# 生成唯一文件名
|
||||
unique_filename = f"{current_user.id}_{filename}"
|
||||
file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename))
|
||||
|
||||
# 更新或创建用户资料
|
||||
profile = UserProfile.query.filter_by(user_id=current_user.id).first()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.session.add(profile)
|
||||
profile.avatar = unique_filename
|
||||
|
||||
# 更新昵称
|
||||
nickname = request.form.get('nickname')
|
||||
if nickname:
|
||||
profile = UserProfile.query.filter_by(user_id=current_user.id).first()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.session.add(profile)
|
||||
profile.nickname = nickname
|
||||
|
||||
db.session.commit()
|
||||
flash('设置已更新', 'success')
|
||||
return redirect(url_for('settings.settings'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'更新失败:{str(e)}', 'danger')
|
||||
|
||||
return render_template('settings/settings.html')
|
@ -0,0 +1,116 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, flash
|
||||
from flask_login import login_required, current_user
|
||||
from app.models.models import TriageRecord, SymptomDepartment, Department
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
|
||||
bp = Blueprint('triage', __name__)
|
||||
|
||||
|
||||
@bp.route('/triage', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def triage():
|
||||
if request.method == 'POST':
|
||||
symptoms = request.form.get('symptoms')
|
||||
|
||||
# 分析症状,获取推荐科室和严重程度
|
||||
max_severity = 1
|
||||
recommended_dept = None
|
||||
|
||||
# 将症状拆分为列表
|
||||
symptom_list = [s.strip() for s in symptoms.split(',')]
|
||||
|
||||
# 查找匹配的症状规则
|
||||
for symptom in symptom_list:
|
||||
symptom_rule = SymptomDepartment.query.filter_by(
|
||||
symptom=symptom
|
||||
).first()
|
||||
|
||||
if symptom_rule and symptom_rule.severity_level > max_severity:
|
||||
max_severity = symptom_rule.severity_level
|
||||
recommended_dept = symptom_rule.department
|
||||
|
||||
# 如果没有找到匹配的科室,使用默认科室
|
||||
if not recommended_dept:
|
||||
recommended_dept = Department.query.filter_by(
|
||||
name='普通内科'
|
||||
).first()
|
||||
|
||||
# 创建分诊记录
|
||||
triage_record = TriageRecord(
|
||||
patient_id=current_user.id,
|
||||
department_id=recommended_dept.id,
|
||||
symptoms=symptoms,
|
||||
severity=f'级别{max_severity}',
|
||||
triage_result=f'建议前往{recommended_dept.name}就诊',
|
||||
status='processed',
|
||||
processed_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
db.session.add(triage_record)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'department': recommended_dept.name,
|
||||
'severity': f'级别{max_severity}',
|
||||
'description': recommended_dept.description
|
||||
})
|
||||
|
||||
return render_template('triage/triage.html')
|
||||
|
||||
|
||||
@bp.route('/triage/history')
|
||||
@login_required
|
||||
def triage_history():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
pagination = TriageRecord.query.filter_by(
|
||||
patient_id=current_user.id
|
||||
).order_by(TriageRecord.created_at.desc()).paginate(
|
||||
page=page, per_page=10, error_out=False
|
||||
)
|
||||
|
||||
return render_template('triage/history.html', pagination=pagination)
|
||||
|
||||
|
||||
@bp.route('/triage/manage', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_triage():
|
||||
if not current_user.is_doctor():
|
||||
flash('只有医生可以访问此页面')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
action = request.form.get('action')
|
||||
|
||||
if action == 'add_rule':
|
||||
symptom = request.form.get('symptom')
|
||||
department_id = request.form.get('department_id')
|
||||
severity = request.form.get('severity')
|
||||
description = request.form.get('description')
|
||||
|
||||
rule = SymptomDepartment(
|
||||
symptom=symptom,
|
||||
department_id=department_id,
|
||||
severity_level=severity,
|
||||
description=description
|
||||
)
|
||||
|
||||
db.session.add(rule)
|
||||
db.session.commit()
|
||||
flash('规则添加成功')
|
||||
|
||||
elif action == 'delete_rule':
|
||||
rule_id = request.form.get('rule_id')
|
||||
rule = SymptomDepartment.query.get_or_404(rule_id)
|
||||
db.session.delete(rule)
|
||||
db.session.commit()
|
||||
flash('规则删除成功')
|
||||
|
||||
# 获取所有规则和科室
|
||||
rules = SymptomDepartment.query.all()
|
||||
departments = Department.query.all()
|
||||
|
||||
return render_template('triage/manage.html',
|
||||
rules=rules,
|
||||
departments=departments)
|
@ -0,0 +1,139 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #343a40;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f4f6f9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 导航栏样式 */
|
||||
.navbar {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
border: none;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,.075);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-top: none;
|
||||
background-color: var(--light-color);
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 聊天界面样式 */
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background-color: #e3f2fd;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
background-color: #f8f9fa;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
// 通用工具函数
|
||||
const utils = {
|
||||
// 显示提示消息
|
||||
showAlert: function(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('main.container');
|
||||
container.insertBefore(alertDiv, container.firstChild);
|
||||
|
||||
// 5秒后自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime: function(date) {
|
||||
return new Date(date).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
// 防抖函数
|
||||
debounce: function(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 表单验证
|
||||
const formValidation = {
|
||||
validateRequired: function(form) {
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
let valid = true;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
valid = false;
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
// 添加提示信息
|
||||
let feedback = field.nextElementSibling;
|
||||
if (!feedback || !feedback.classList.contains('invalid-feedback')) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
field.parentNode.appendChild(feedback);
|
||||
}
|
||||
feedback.textContent = '此字段不能为空';
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
return valid;
|
||||
},
|
||||
|
||||
validateEmail: function(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
},
|
||||
|
||||
validatePassword: function(password) {
|
||||
return password.length >= 6;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化所有工具提示
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// 初始化所有弹出框
|
||||
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
popoverTriggerList.map(function(popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
|
||||
// 处理表单验证
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!formValidation.validateRequired(form)) {
|
||||
e.preventDefault();
|
||||
utils.showAlert('请填写所有必填字段', 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 聊天相关功能
|
||||
const chat = {
|
||||
scrollToBottom: function(container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
},
|
||||
|
||||
addMessage: function(message, type, container) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `chat-message ${type}-message fade-in`;
|
||||
|
||||
const now = utils.formatDateTime(new Date());
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-header">
|
||||
<small class="text-muted">${now}</small>
|
||||
</div>
|
||||
<div class="message-content">${message}</div>
|
||||
`;
|
||||
|
||||
container.appendChild(messageDiv);
|
||||
this.scrollToBottom(container);
|
||||
}
|
||||
};
|
After Width: | Height: | Size: 178 KiB |
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}登录 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center mb-4">用户登录</h4>
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">用户名</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">密码</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">记住我</label>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">登录</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">
|
||||
还没有账号?立即注册
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,112 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}注册 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center mb-4">用户注册</h4>
|
||||
<form method="POST" action="{{ url_for('auth.register') }}" id="registerForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">注册类型</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="role" id="rolePatient"
|
||||
value="patient" checked>
|
||||
<label class="form-check-label" for="rolePatient">患者</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="role" id="roleDoctor"
|
||||
value="doctor">
|
||||
<label class="form-check-label" for="roleDoctor">医生</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">用户名</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">邮箱</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">密码</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">确认密码</label>
|
||||
<input type="password" class="form-control" id="confirm_password"
|
||||
name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<!-- 医生特有字段 -->
|
||||
<div id="doctorFields" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="department" class="form-label">所属科室</label>
|
||||
<select class="form-select" id="department" name="department">
|
||||
{% for dept in departments %}
|
||||
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">职称</label>
|
||||
<select class="form-select" id="title" name="title">
|
||||
<option value="主任医师">主任医师</option>
|
||||
<option value="副主任医师">副主任医师</option>
|
||||
<option value="主治医师">主治医师</option>
|
||||
<option value="住院医师">住院医师</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">注册</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">
|
||||
已有账号?立即登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const roleInputs = document.querySelectorAll('input[name="role"]');
|
||||
const doctorFields = document.getElementById('doctorFields');
|
||||
|
||||
function toggleDoctorFields() {
|
||||
const isDoctor = document.getElementById('roleDoctor').checked;
|
||||
doctorFields.style.display = isDoctor ? 'block' : 'none';
|
||||
}
|
||||
|
||||
roleInputs.forEach(input => {
|
||||
input.addEventListener('change', toggleDoctorFields);
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
const form = document.getElementById('registerForm');
|
||||
form.addEventListener('submit', function(event) {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirm_password').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
event.preventDefault();
|
||||
alert('两次输入的密码不一致!');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}重置密码 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">重置密码</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">邮箱地址</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
required placeholder="请输入注册时使用的邮箱">
|
||||
<div class="form-text">
|
||||
我们将向您的邮箱发送重置密码的链接。
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
发送重置链接
|
||||
</button>
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">
|
||||
返回登录
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}医疗问答系统{% endblock %}</title>
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
||||
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" height="30">
|
||||
医疗问答系统
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.role == 'patient' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('triage.triage') }}">智能分诊</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.ai_consult') }}">AI问诊</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('records.medical_records') }}">我的病历</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('chat.chat_list') }}">
|
||||
找医生咨询
|
||||
<span class="badge bg-danger" id="unreadCount"></span>
|
||||
</a>
|
||||
</li>
|
||||
{% elif current_user.role == 'doctor' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('triage.manage_triage') }}">分诊管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('records.medical_records') }}">病历管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('chat.chat_list') }}">
|
||||
患者咨询
|
||||
<span class="badge bg-danger" id="unreadCount"></span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
|
||||
data-bs-toggle="dropdown">
|
||||
<img src="{{ url_for('static', filename='uploads/' + current_user.profile.avatar) if current_user.profile and current_user.profile.avatar else url_for('static', filename='images/default-avatar.png') }}"
|
||||
class="rounded-circle me-2"
|
||||
style="width: 30px; height: 30px; object-fit: cover;">
|
||||
{{ current_user.profile.nickname if current_user.profile and current_user.profile.nickname else current_user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{{ url_for('settings.settings') }}">
|
||||
<i class="bi bi-gear"></i> 系统设置
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('chat.chat_list') }}">
|
||||
<i class="bi bi-chat"></i> 我的消息
|
||||
<span class="badge bg-danger float-end" id="dropdownUnreadCount"></span>
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
||||
<i class="bi bi-box-arrow-right"></i> 退出登录
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.register') }}">注册</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="container mt-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer mt-5 py-3 bg-light">
|
||||
<div class="container text-center">
|
||||
<span class="text-muted">© 2024 医疗问答系统. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.7.2/font/bootstrap-icons.css"></script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% if current_user.is_authenticated %}
|
||||
<script>
|
||||
// 获取未读消息数
|
||||
async function updateUnreadCount() {
|
||||
try {
|
||||
const response = await fetch('{{ url_for("chat.get_unread_count") }}');
|
||||
const data = await response.json();
|
||||
const count = data.count;
|
||||
|
||||
// 更新导航栏未读消息数
|
||||
const unreadCount = document.getElementById('unreadCount');
|
||||
const dropdownUnreadCount = document.getElementById('dropdownUnreadCount');
|
||||
|
||||
if (count > 0) {
|
||||
unreadCount.textContent = count;
|
||||
dropdownUnreadCount.textContent = count;
|
||||
unreadCount.style.display = 'inline';
|
||||
dropdownUnreadCount.style.display = 'inline';
|
||||
} else {
|
||||
unreadCount.style.display = 'none';
|
||||
dropdownUnreadCount.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取未读消息数失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 定期更新未读消息数
|
||||
updateUnreadCount();
|
||||
setInterval(updateUnreadCount, 60000); // 每分钟更新一次
|
||||
</script>
|
||||
{% endif %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}消息列表 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">消息列表</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group">
|
||||
{% for user in chats %}
|
||||
<a href="{{ url_for('chat.chat_room', user_id=user.id) }}"
|
||||
class="list-group-item list-group-item-action">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ url_for('static', filename='uploads/' + user.profile.avatar) if user.profile and user.profile.avatar else url_for('static', filename='images/default-avatar.png') }}"
|
||||
class="rounded-circle me-3"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
<div>
|
||||
<h6 class="mb-0">{{ user.profile.nickname if user.profile else user.username }}</h6>
|
||||
<small class="text-muted">
|
||||
{{ '医生' if user.is_doctor() else '患者' }} -
|
||||
{{ user.department.name if user.is_doctor() and user.department else '' }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}与 {{ other_user.profile.nickname if other_user.profile else other_user.username }} 的对话 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ url_for('static', filename='uploads/' + other_user.profile.avatar) if other_user.profile and other_user.profile.avatar else url_for('static', filename='images/default-avatar.png') }}"
|
||||
class="rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; object-fit: cover;">
|
||||
<h5 class="card-title mb-0">
|
||||
{{ other_user.profile.nickname if other_user.profile else other_user.username }}
|
||||
<small class="text-muted">
|
||||
{{ '医生' if other_user.is_doctor() else '患者' }}
|
||||
{% if other_user.is_doctor() and other_user.department %}
|
||||
- {{ other_user.department.name }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body chat-container" style="height: 400px; overflow-y: auto;" id="messageContainer">
|
||||
{% for message in messages %}
|
||||
<div class="d-flex mb-3 {{ 'justify-content-end' if message.sender_id == current_user.id else 'justify-content-start' }}">
|
||||
<div class="message {{ 'bg-primary text-white' if message.sender_id == current_user.id else 'bg-light' }}"
|
||||
style="max-width: 70%; padding: 10px; border-radius: 10px;">
|
||||
{{ message.content }}
|
||||
<div class="small {{ 'text-white-50' if message.sender_id == current_user.id else 'text-muted' }}">
|
||||
{{ message.created_at.strftime('%H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<form id="messageForm" class="d-flex">
|
||||
<input type="hidden" name="receiver_id" value="{{ other_user.id }}">
|
||||
<input type="text" class="form-control me-2" name="content" placeholder="输入消息...">
|
||||
<button type="submit" class="btn btn-primary">发送</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('messageForm');
|
||||
const messageContainer = document.getElementById('messageContainer');
|
||||
|
||||
// 滚动到最新消息
|
||||
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
try {
|
||||
const response = await fetch('{{ url_for("chat.send_message") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// 添加新消息到界面
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'd-flex mb-3 justify-content-end';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message bg-primary text-white" style="max-width: 70%; padding: 10px; border-radius: 10px;">
|
||||
${data.message.content}
|
||||
<div class="small text-white-50">
|
||||
${data.message.created_at}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messageContainer.appendChild(messageDiv);
|
||||
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||
form.reset();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,143 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}AI问诊 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">AI智能问诊</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 聊天记录区域 -->
|
||||
<div class="chat-container mb-3" id="chatContainer">
|
||||
{% for message in chat_history %}
|
||||
<div class="chat-message {% if message.message_type == 'user' %}user-message{% else %}ai-message{% endif %}">
|
||||
<div class="message-header">
|
||||
<small class="text-muted">
|
||||
{{ message.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{{ message.message }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<form id="consultForm" class="mt-3">
|
||||
<div class="input-group">
|
||||
<textarea class="form-control" id="questionInput" rows="2"
|
||||
placeholder="请描述您的症状或健康问题..."></textarea>
|
||||
<button class="btn btn-primary" type="submit">发送</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background-color: #e3f2fd;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
background-color: #f5f5f5;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('consultForm');
|
||||
const input = document.getElementById('questionInput');
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 添加消息到聊天区域
|
||||
function addMessage(message, type) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `chat-message ${type}-message`;
|
||||
|
||||
const now = new Date().toLocaleString();
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-header">
|
||||
<small class="text-muted">${now}</small>
|
||||
</div>
|
||||
<div class="message-content">${message}</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(messageDiv);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const question = input.value.trim();
|
||||
if (!question) return;
|
||||
|
||||
// 添加用户消息
|
||||
addMessage(question, 'user');
|
||||
input.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/ai_consult', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `question=${encodeURIComponent(question)}`
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
addMessage(data.answer, 'ai');
|
||||
} else {
|
||||
addMessage('抱歉,系统出现错误,请稍后重试。', 'ai');
|
||||
}
|
||||
} catch (error) {
|
||||
addMessage('网络错误,请检查您的网络连接。', 'ai');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始滚动到底部
|
||||
scrollToBottom();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}病历记录 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
{% if current_user.is_doctor() %}
|
||||
病历管理
|
||||
{% else %}
|
||||
我的病历
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if current_user.is_doctor() %}
|
||||
<a href="{{ url_for('records.new_record') }}" class="btn btn-primary btn-sm">
|
||||
新建病历
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>编号</th>
|
||||
{% if current_user.is_doctor() %}
|
||||
<th>患者</th>
|
||||
{% else %}
|
||||
<th>主治医生</th>
|
||||
{% endif %}
|
||||
<th>科室</th>
|
||||
<th>诊断</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in pagination.items %}
|
||||
<tr>
|
||||
<td>{{ record.id }}</td>
|
||||
{% if current_user.is_doctor() %}
|
||||
<td>{{ record.patient.username }}</td>
|
||||
{% else %}
|
||||
<td>{{ record.doctor.username if record.doctor else '待分配' }}</td>
|
||||
{% endif %}
|
||||
<td>{{ record.department.name if record.department else '-' }}</td>
|
||||
<td>{{ record.diagnosis[:20] + '...' if record.diagnosis else '-' }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ {
|
||||
'pending': 'warning',
|
||||
'in_progress': 'info',
|
||||
'completed': 'success'
|
||||
}[record.status] }}">
|
||||
{{ {
|
||||
'pending': '待处理',
|
||||
'in_progress': '处理中',
|
||||
'completed': '已完成'
|
||||
}[record.status] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ record.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('records.view_record', id=record.id) }}"
|
||||
class="btn btn-info btn-sm">查看</a>
|
||||
{% if current_user.is_doctor() and record.status != 'completed' %}
|
||||
<a href="{{ url_for('records.edit_record', id=record.id) }}"
|
||||
class="btn btn-warning btn-sm">编辑</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for page in pagination.iter_pages() %}
|
||||
{% if page %}
|
||||
<li class="page-item {{ 'active' if page == pagination.page else '' }}">
|
||||
<a class="page-link" href="{{ url_for('records.medical_records', page=page) }}">
|
||||
{{ page }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">...</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}新建病历 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">新建病历</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" id="newRecordForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="patient_id" class="form-label">患者</label>
|
||||
<select class="form-select" id="patient_id" name="patient_id" required>
|
||||
{% for patient in patients %}
|
||||
<option value="{{ patient.id }}">{{ patient.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="department_id" class="form-label">就诊科室</label>
|
||||
<select class="form-select" id="department_id" name="department_id" required>
|
||||
{% for dept in departments %}
|
||||
<option value="{{ dept.id }}"
|
||||
{{ 'selected' if dept.id == current_user.department_id else '' }}>
|
||||
{{ dept.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="chief_complaint" class="form-label">主诉</label>
|
||||
<textarea class="form-control" id="chief_complaint" name="chief_complaint"
|
||||
rows="3" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="present_illness" class="form-label">现病史</label>
|
||||
<textarea class="form-control" id="present_illness" name="present_illness"
|
||||
rows="4" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="past_history" class="form-label">既往史</label>
|
||||
<textarea class="form-control" id="past_history" name="past_history"
|
||||
rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="diagnosis" class="form-label">诊断</label>
|
||||
<textarea class="form-control" id="diagnosis" name="diagnosis"
|
||||
rows="3" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="treatment" class="form-label">治疗方案</label>
|
||||
<textarea class="form-control" id="treatment" name="treatment"
|
||||
rows="4" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="prescription" class="form-label">处方</label>
|
||||
<textarea class="form-control" id="prescription" name="prescription"
|
||||
rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">备注</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary">保存病历</button>
|
||||
<a href="{{ url_for('records.medical_records') }}" class="btn btn-secondary">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('newRecordForm');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
let valid = true;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
valid = false;
|
||||
field.classList.add('is-invalid');
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
e.preventDefault();
|
||||
alert('请填写所有必填字段');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,125 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}查看病历 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">病历详情</h5>
|
||||
<div>
|
||||
{% if current_user.is_doctor() and record.status != 'completed' %}
|
||||
<a href="{{ url_for('records.edit_record', id=record.id) }}"
|
||||
class="btn btn-warning btn-sm">编辑病历</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('records.medical_records') }}"
|
||||
class="btn btn-secondary btn-sm">返回列表</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<strong>病历编号:</strong>
|
||||
<span>{{ record.id }}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>患者姓名:</strong>
|
||||
<span>{{ record.patient.username }}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>主治医生:</strong>
|
||||
<span>{{ record.doctor.username if record.doctor else '待分配' }}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>就诊科室:</strong>
|
||||
<span>{{ record.department.name if record.department else '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">主诉</h6>
|
||||
<div class="section-content">{{ record.chief_complaint or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">现病史</h6>
|
||||
<div class="section-content">{{ record.present_illness or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">既往史</h6>
|
||||
<div class="section-content">{{ record.past_history or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">诊断</h6>
|
||||
<div class="section-content">{{ record.diagnosis or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">治疗方案</h6>
|
||||
<div class="section-content">{{ record.treatment or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">处方</h6>
|
||||
<div class="section-content">{{ record.prescription or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="medical-record-section">
|
||||
<h6 class="section-title">备注</h6>
|
||||
<div class="section-content">{{ record.notes or '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<strong>状态:</strong>
|
||||
<span class="badge bg-{{ {
|
||||
'pending': 'warning',
|
||||
'in_progress': 'info',
|
||||
'completed': 'success'
|
||||
}[record.status] }}">
|
||||
{{ {
|
||||
'pending': '待处理',
|
||||
'in_progress': '处理中',
|
||||
'completed': '已完成'
|
||||
}[record.status] }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>创建时间:</strong>
|
||||
<span>{{ record.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>最后更新:</strong>
|
||||
<span>{{ record.updated_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.medical-record-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
white-space: pre-wrap;
|
||||
color: #212529;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}系统设置 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">个人设置</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="avatar" class="form-label">头像</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ url_for('static', filename='uploads/' + current_user.profile.avatar) if current_user.profile and current_user.profile.avatar else url_for('static', filename='images/default-avatar.png') }}"
|
||||
class="rounded-circle me-3"
|
||||
style="width: 100px; height: 100px; object-fit: cover;">
|
||||
<input type="file" class="form-control" id="avatar" name="avatar" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nickname" class="form-label">昵称</label>
|
||||
<input type="text" class="form-control" id="nickname" name="nickname"
|
||||
value="{{ current_user.profile.nickname if current_user.profile else '' }}"
|
||||
placeholder="请输入您的昵称">
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary">保存设置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}分诊规则管理 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">添加分诊规则</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" id="addRuleForm">
|
||||
<input type="hidden" name="action" value="add_rule">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="symptom" class="form-label">症状</label>
|
||||
<input type="text" class="form-control" id="symptom" name="symptom" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="department_id" class="form-label">科室</label>
|
||||
<select class="form-select" id="department_id" name="department_id" required>
|
||||
{% for dept in departments %}
|
||||
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="severity" class="form-label">严重程度</label>
|
||||
<select class="form-select" id="severity" name="severity" required>
|
||||
<option value="1">1级 - 轻微</option>
|
||||
<option value="2">2级 - 较轻</option>
|
||||
<option value="3">3级 - 中等</option>
|
||||
<option value="4">4级 - 严重</option>
|
||||
<option value="5">5级 - 危急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"> </label>
|
||||
<button type="submit" class="btn btn-primary d-block w-100">添加规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">现有分诊规则</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>症状</th>
|
||||
<th>科室</th>
|
||||
<th>严重程度</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rule in rules %}
|
||||
<tr>
|
||||
<td>{{ rule.symptom }}</td>
|
||||
<td>{{ rule.department.name }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ ['success', 'info', 'warning', 'danger', 'dark'][rule.severity_level-1] }}">
|
||||
{{ rule.severity_level }}级
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ rule.updated_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete_rule">
|
||||
<input type="hidden" name="rule_id" value="{{ rule.id }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('确定要删除这条规则吗?')">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}智能分诊 - {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">智能分诊</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="triageForm">
|
||||
<div class="mb-3">
|
||||
<label for="symptoms" class="form-label">请描述您的症状</label>
|
||||
<textarea class="form-control" id="symptoms" name="symptoms" rows="4"
|
||||
placeholder="请详细描述您的症状,多个症状请用逗号分隔..." required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">常见症状快速选择:</label>
|
||||
<div class="common-symptoms">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="头痛">头痛</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="发烧">发烧</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="咳嗽">咳嗽</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="腹痛">腹痛</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="恶心">恶心</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm m-1" data-symptom="头晕">头晕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">开始分诊</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 分诊结果 -->
|
||||
<div id="triageResult" class="mt-4" style="display: none;">
|
||||
<h5 class="border-bottom pb-2">分诊结果</h5>
|
||||
<div class="result-content">
|
||||
<div class="mb-3">
|
||||
<strong>建议科室:</strong>
|
||||
<span id="recommendedDepartment"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>严重程度:</strong>
|
||||
<span id="severityLevel"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>科室说明:</strong>
|
||||
<p id="departmentDescription" class="text-muted"></p>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
提示:此结果仅供参考,具体诊疗请以医生的专业判断为准。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.common-symptoms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.severity-low {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.severity-medium {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.severity-high {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('triageForm');
|
||||
const symptomsInput = document.getElementById('symptoms');
|
||||
const resultDiv = document.getElementById('triageResult');
|
||||
|
||||
// 处理快速选择按钮
|
||||
document.querySelectorAll('[data-symptom]').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const symptom = this.dataset.symptom;
|
||||
const currentSymptoms = symptomsInput.value.split(',').map(s => s.trim()).filter(s => s);
|
||||
|
||||
if (!currentSymptoms.includes(symptom)) {
|
||||
currentSymptoms.push(symptom);
|
||||
symptomsInput.value = currentSymptoms.join(', ');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理表单提交
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const response = await fetch('/triage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `symptoms=${encodeURIComponent(symptomsInput.value)}`
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('recommendedDepartment').textContent = data.department;
|
||||
document.getElementById('severityLevel').textContent = data.severity;
|
||||
document.getElementById('departmentDescription').textContent = data.description;
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.scrollIntoView({ behavior: 'smooth' });
|
||||
} else {
|
||||
alert('分诊失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('网络错误,请检查您的网络连接');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,19 @@
|
||||
import os
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Config:
|
||||
# 密钥配置
|
||||
SECRET_KEY = 'dev' # 在生产环境中应该使用复杂的随机字符串
|
||||
|
||||
# 数据库配置
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_FOLDER = os.path.join(basedir, 'app/static/uploads')
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 最大16MB
|
||||
|
||||
# 智谱AI配置
|
||||
ZHIPUAI_API_KEY = "7dac79af6b3a474893d3c5aac13c087c.QJBPxMxLIaFClkLx" # 替换为您的智谱API密钥
|
@ -0,0 +1,36 @@
|
||||
|
||||
Flask==2.3.3
|
||||
Werkzeug==2.3.7
|
||||
Flask-Login==0.6.2
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
|
||||
|
||||
SQLAlchemy==2.0.21
|
||||
alembic==1.12.0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
python-dotenv==1.0.0
|
||||
email-validator==2.0.0.post2
|
||||
pytz==2023.3.post1
|
||||
requests==2.31.0
|
||||
urllib3==2.0.4
|
||||
|
||||
|
||||
cryptography==41.0.3
|
||||
PyJWT==2.8.0
|
||||
|
||||
|
||||
python-dateutil==2.8.2
|
||||
|
||||
|
||||
pytest==7.4.2
|
||||
pytest-cov==4.1.0
|
||||
black==23.7.0
|
||||
flake8==6.1.0
|
||||
|
||||
|
||||
gunicorn==21.2.0
|
||||
gevent==23.7.0
|
@ -0,0 +1,6 @@
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
@ -0,0 +1,158 @@
|
||||
from app import create_app, db
|
||||
from app.models.models import User, Department, SymptomDepartment, UserProfile
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
||||
def init_departments():
|
||||
"""初始化科室数据"""
|
||||
departments = [
|
||||
{'name': '普通内科', 'description': '处理常见内科疾病,如感冒、发烧等'},
|
||||
{'name': '心内科', 'description': '专治心脏相关疾病'},
|
||||
{'name': '神经内科', 'description': '治疗神经系统疾病'},
|
||||
{'name': '消化内科', 'description': '治疗消化系统疾病'},
|
||||
{'name': '呼吸内科', 'description': '治疗呼吸系统疾病'},
|
||||
{'name': '内分泌科', 'description': '治疗内分泌系统疾病'},
|
||||
{'name': '普通外科', 'description': '处理常见外科疾病'},
|
||||
{'name': '骨科', 'description': '治疗骨骼相关疾病'},
|
||||
{'name': '妇产科', 'description': '治疗妇科疾病和孕产相关'},
|
||||
{'name': '儿科', 'description': '专门治疗儿童疾病'},
|
||||
{'name': '皮肤科', 'description': '治疗皮肤相关疾病'},
|
||||
{'name': '眼科', 'description': '治疗眼部相关疾病'},
|
||||
{'name': '耳鼻喉科', 'description': '治疗耳鼻喉相关疾病'},
|
||||
{'name': '口腔科', 'description': '治疗口腔相关疾病'},
|
||||
{'name': '精神科', 'description': '治疗心理和精神相关疾病'}
|
||||
]
|
||||
|
||||
for dept in departments:
|
||||
if not Department.query.filter_by(name=dept['name']).first():
|
||||
department = Department(name=dept['name'], description=dept['description'])
|
||||
db.session.add(department)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def init_symptom_rules():
|
||||
"""初始化症状-科室关联规则"""
|
||||
rules = [
|
||||
{'symptom': '发烧', 'department': '普通内科', 'severity': 2},
|
||||
{'symptom': '咳嗽', 'department': '呼吸内科', 'severity': 2},
|
||||
{'symptom': '胸痛', 'department': '心内科', 'severity': 4},
|
||||
{'symptom': '头痛', 'department': '神经内科', 'severity': 3},
|
||||
{'symptom': '腹痛', 'department': '消化内科', 'severity': 3},
|
||||
{'symptom': '骨折', 'department': '骨科', 'severity': 4},
|
||||
{'symptom': '皮疹', 'department': '皮肤科', 'severity': 2},
|
||||
{'symptom': '视力模糊', 'department': '眼科', 'severity': 3},
|
||||
{'symptom': '耳鸣', 'department': '耳鼻喉科', 'severity': 2},
|
||||
{'symptom': '牙痛', 'department': '口腔科', 'severity': 2},
|
||||
{'symptom': '焦虑', 'department': '精神科', 'severity': 3},
|
||||
{'symptom': '高血压', 'department': '心内科', 'severity': 3},
|
||||
{'symptom': '糖尿病', 'department': '内分泌科', 'severity': 3},
|
||||
{'symptom': '哮喘', 'department': '呼吸内科', 'severity': 3},
|
||||
{'symptom': '胃痛', 'department': '消化内科', 'severity': 2}
|
||||
]
|
||||
|
||||
for rule in rules:
|
||||
dept = Department.query.filter_by(name=rule['department']).first()
|
||||
if dept and not SymptomDepartment.query.filter_by(symptom=rule['symptom']).first():
|
||||
symptom_dept = SymptomDepartment(
|
||||
symptom=rule['symptom'],
|
||||
department_id=dept.id,
|
||||
severity_level=rule['severity']
|
||||
)
|
||||
db.session.add(symptom_dept)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def create_test_users():
|
||||
"""创建测试用户"""
|
||||
# 创建管理员用户
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
if not admin:
|
||||
admin = User(
|
||||
username='admin',
|
||||
email='admin@example.com',
|
||||
role='doctor',
|
||||
department_id=1, # 默认分配到普通内科
|
||||
title='主任医师'
|
||||
)
|
||||
admin.set_password('admin123')
|
||||
db.session.add(admin)
|
||||
db.session.commit() # 先提交以获取用户ID
|
||||
|
||||
# 创建管理员的个人资料
|
||||
admin_profile = UserProfile(
|
||||
user_id=admin.id, # 现在可以安全地使用admin.id
|
||||
nickname='系统管理员',
|
||||
avatar='admin_default.png'
|
||||
)
|
||||
db.session.add(admin_profile)
|
||||
db.session.commit()
|
||||
|
||||
# 创建测试医生
|
||||
test_doctor = User.query.filter_by(username='doctor').first()
|
||||
if not test_doctor:
|
||||
test_doctor = User(
|
||||
username='doctor',
|
||||
email='doctor@example.com',
|
||||
role='doctor',
|
||||
department_id=1,
|
||||
title='主治医师'
|
||||
)
|
||||
test_doctor.set_password('doctor123')
|
||||
db.session.add(test_doctor)
|
||||
db.session.commit() # 先提交以获取用户ID
|
||||
|
||||
# 创建医生的个人资料
|
||||
doctor_profile = UserProfile(
|
||||
user_id=test_doctor.id, # 现在可以安全地使用test_doctor.id
|
||||
nickname='测试医生',
|
||||
avatar='doctor_default.png'
|
||||
)
|
||||
db.session.add(doctor_profile)
|
||||
db.session.commit()
|
||||
|
||||
# 创建测试患者
|
||||
test_patient = User.query.filter_by(username='patient').first()
|
||||
if not test_patient:
|
||||
test_patient = User(
|
||||
username='patient',
|
||||
email='patient@example.com',
|
||||
role='patient'
|
||||
)
|
||||
test_patient.set_password('patient123')
|
||||
db.session.add(test_patient)
|
||||
db.session.commit() # 先提交以获取用户ID
|
||||
|
||||
# 创建患者的个人资料
|
||||
patient_profile = UserProfile(
|
||||
user_id=test_patient.id, # 现在可以安全地使用test_patient.id
|
||||
nickname='测试患者',
|
||||
avatar='patient_default.png'
|
||||
)
|
||||
db.session.add(patient_profile)
|
||||
db.session.commit()
|
||||
|
||||
print('测试用户创建完成')
|
||||
|
||||
def init_db():
|
||||
"""初始化数据库"""
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# 创建所有表
|
||||
db.create_all()
|
||||
|
||||
# 初始化基础数据
|
||||
init_departments()
|
||||
init_symptom_rules()
|
||||
create_test_users()
|
||||
|
||||
print('数据库初始化完成!')
|
||||
print('测试账号:')
|
||||
print('管理员 - username: admin, password: admin123')
|
||||
print('医生 - username: doctor, password: doctor123')
|
||||
print('患者 - username: patient, password: patient123')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
@ -0,0 +1,25 @@
|
||||
from zhipuai import ZhipuAI
|
||||
|
||||
def extract_content(response_message):
|
||||
# 提取 content 字段的值
|
||||
content = response_message.content # 获取 content 属性
|
||||
return content
|
||||
|
||||
|
||||
|
||||
def ask(conte):
|
||||
client = ZhipuAI(api_key="7dac79af6b3a474893d3c5aac13c087c.QJBPxMxLIaFClkLx") # 请填写您自己的APIKey
|
||||
response = client.chat.completions.create(
|
||||
model="glm-4-plus", # 请填写您要调用的模型名称
|
||||
messages=[
|
||||
{"role": "user", "content": conte},
|
||||
],
|
||||
)
|
||||
r=extract_content(response.choices[0].message)
|
||||
# print(r)
|
||||
return r.encode('utf-8', 'ignore').decode('utf-8')
|
||||
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
Loading…
Reference in new issue