Compare commits

...

10 Commits

Author SHA1 Message Date
48bb1b3c12 新增“数据编辑” 2025-10-14 19:14:43 +08:00
8a752b2b92 新增“数据编辑” 2025-10-14 18:00:40 +08:00
a678adf646 新增“数据编辑” 2025-10-14 16:00:45 +08:00
08994d732d Merge remote-tracking branch 'origin/main-v2'
# Conflicts:
#	ESConnect.py
#	app.py
2025-10-14 15:46:11 +08:00
9c011dfc8c 新增“数据编辑” 2025-10-14 15:37:22 +08:00
aa6b1dec3f 新增“数据编辑” 2025-10-14 15:35:32 +08:00
0926ab2535 处理冲突 2025-10-14 15:17:51 +08:00
e887494796 2025-10-14 14:51:08 +08:00
7e44ccdb31 Merge branch 'main' of ssh://gitea.spdis.top/Viajero/Achievement_Inputing 2025-10-14 14:48:22 +08:00
5575370621 用户系统 2025-10-14 14:46:38 +08:00
10 changed files with 3721 additions and 410 deletions

View File

@@ -1,6 +1,6 @@
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
import os # import os
import json # import json
import hashlib import hashlib
import requests import requests
import json import json
@@ -15,14 +15,17 @@ AUTH = None # 如需认证则改为("用户名","密码")
es = Elasticsearch(["http://localhost:9200"]) es = Elasticsearch(["http://localhost:9200"])
# 定义索引名称和类型名称 # 定义索引名称和类型名称
index_name = "wordsearch2666" data_index_name = "wordsearch266666"
users_index_name = "users"
def create_index_with_mapping(): def create_index_with_mapping():
"""修正后的索引映射配置""" """修正后的索引映射配置"""
# 修正映射结构移除keyword字段的非法参数 # 新增一个用户mapping
mapping = { data_mapping = {
"mappings": { "mappings": {
"properties": { "properties": {
"writer_id":{"type": "text"},
"data": { "data": {
"type": "text", # 存储转换后的字符串,支持分词搜索 "type": "text", # 存储转换后的字符串,支持分词搜索
"analyzer": "ik_max_word", "analyzer": "ik_max_word",
@@ -33,13 +36,32 @@ def create_index_with_mapping():
} }
} }
# 检查索引是否存在,不存在则创建 users_mapping = {
if not es.indices.exists(index=index_name): "mappings": {
es.indices.create(index=index_name, body=mapping) "properties": {
print(f"创建索引 {index_name} 并设置映射") "user_id":{"type":"long"}, #由系统分配的用户唯一id
else: "username":{"type":"keyword"}, #可修改的用户名
print(f"索引 {index_name} 已存在") "password":{"type":"keyword"}, #密码
"premission":{"type":"integer"},#权限组分配比方说0就是管理员1是普通用户以此类推
}
}
}
# 检查数据索引是否存在,不存在则创建
if not es.indices.exists(index=data_index_name):
es.indices.create(index=data_index_name, body=data_mapping)
print(f"创建索引 {data_index_name} 并设置映射")
else:
print(f"索引 {data_index_name} 已存在")
# 检查用户索引是否存在,不存在则创建
if not es.indices.exists(index=users_index_name):
es.indices.create(index=users_index_name, body=users_mapping)
print(f"创建索引 {users_index_name} 并设置映射")
admin={"user_id":0000000000,"username": "admin", "password": "admin", "premission": 0}
write_user_data(admin)
else:
print(f"索引 {users_index_name} 已存在")
def update_document(es, index_name, doc_id=None, updated_doc=None): def update_document(es, index_name, doc_id=None, updated_doc=None):
"""更新指定ID的文档""" """更新指定ID的文档"""
es.update(index=index_name, id=doc_id, body={"doc": updated_doc}) es.update(index=index_name, id=doc_id, body={"doc": updated_doc})
@@ -88,7 +110,7 @@ def search_data(query):
list: 包含搜索结果的列表,每个元素是一个文档的源数据 list: 包含搜索结果的列表,每个元素是一个文档的源数据
""" """
# 执行多字段匹配搜索 # 执行多字段匹配搜索
result = es.search(index=index_name, body={"query": {"multi_match": {"query": query, "fields": ["*"]}}}) result = es.search(index=data_index_name, body={"query": {"multi_match": {"query": query, "fields": ["*"]}}})
# 返回搜索结果的源数据部分 # 返回搜索结果的源数据部分
return [hit["_source"] for hit in result['hits']['hits']] return [hit["_source"] for hit in result['hits']['hits']]
@@ -100,7 +122,7 @@ def search_all():
list: 包含所有文档的列表每个元素包含文档ID和源数据 list: 包含所有文档的列表每个元素包含文档ID和源数据
""" """
# 执行匹配所有文档的查询 # 执行匹配所有文档的查询
result = es.search(index=index_name, body={"query": {"match_all": {}}}) result = es.search(index=data_index_name, body={"query": {"match_all": {}}})
# 返回包含文档ID和源数据的列表 # 返回包含文档ID和源数据的列表
return [{ return [{
"_id": hit["_id"], "_id": hit["_id"],
@@ -119,7 +141,7 @@ def delete_by_id(doc_id):
""" """
try: try:
# 执行删除操作 # 执行删除操作
es.delete(index=index_name, id=doc_id) es.delete(index=data_index_name, id=doc_id)
return True return True
except Exception as e: except Exception as e:
print("删除失败:", str(e)) print("删除失败:", str(e))
@@ -138,7 +160,7 @@ def update_by_id(doc_id, updated_data):
""" """
try: try:
# 执行更新操作 # 执行更新操作
es.update(index=index_name, id=doc_id, body={"doc": updated_data}) es.update(index=data_index_name, id=doc_id, body={"doc": updated_data})
print(f"文档 {doc_id} 更新成功") print(f"文档 {doc_id} 更新成功")
return True return True
except Exception as e: except Exception as e:
@@ -157,7 +179,7 @@ def get_by_id(doc_id):
""" """
try: try:
# 执行获取操作 # 执行获取操作
result = es.get(index=index_name, id=doc_id) result = es.get(index=data_index_name, id=doc_id)
if result['found']: if result['found']:
return { return {
"_id": result['_id'], "_id": result['_id'],
@@ -171,9 +193,9 @@ def get_by_id(doc_id):
def search_by_any_field(keyword): def search_by_any_field(keyword):
"""全字段模糊搜索(支持拼写错误)""" """全字段模糊搜索(支持拼写错误)"""
try: try:
# update_mapping() # update_data_mapping()
response = requests.post( response = requests.post(
f"{ES_URL}/{index_name}/_search", f"{ES_URL}/{data_index_name}/_search",
auth=AUTH, auth=AUTH,
json={ json={
"query": { "query": {
@@ -209,7 +231,7 @@ def batch_write_data(data):
"""批量写入获奖数据""" """批量写入获奖数据"""
try: try:
response = requests.post( response = requests.post(
f"{ES_URL}/{index_name}/_doc", f"{ES_URL}/{data_index_name}/_doc",
json=data, json=data,
auth=AUTH, auth=AUTH,
headers={"Content-Type": "application/json"} headers={"Content-Type": "application/json"}
@@ -221,3 +243,538 @@ def batch_write_data(data):
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
print(f"文档写入失败: {e.response.text}, 数据: {data}") print(f"文档写入失败: {e.response.text}, 数据: {data}")
return False return False
def write_user_data(data):
"""写入用户数据"""
try:
response = requests.post(
f"{ES_URL}/{users_index_name}/_doc",
json=data,
auth=AUTH,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
doc_id = response.json()["_id"]
print(f"文档写入成功ID: {doc_id}, 内容: {data}")
return True
except requests.exceptions.HTTPError as e:
print(f"文档写入失败: {e.response.text}, 数据: {data}")
return False
def verify_user(username, password):
"""
验证用户登录信息
参数:
username (str): 用户名
password (str): 密码
返回:
dict or None: 验证成功返回用户信息失败返回None
"""
try:
# 搜索用户名匹配的用户
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"username": username
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if results:
user_data = results[0]["_source"]
# 验证密码
if user_data.get("password") == password:
print(f"用户 {username} 登录成功")
return user_data
else:
print(f"用户 {username} 密码错误")
return None
else:
print(f"用户 {username} 不存在")
return None
except requests.exceptions.HTTPError as e:
print(f"用户验证失败: {e.response.text}")
return None
def get_user_by_username(username):
"""
根据用户名查询用户信息
参数:
username (str): 用户名
返回:
dict or None: 查询成功返回用户信息失败返回None
"""
try:
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"username": username
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if results:
return results[0]["_source"]
else:
return None
except requests.exceptions.HTTPError as e:
print(f"用户查询失败: {e.response.text}")
return None
def create_user(username, password, permission=1):
"""
创建新用户
参数:
username (str): 用户名
password (str): 密码
permission (int): 权限级别默认为1普通用户
返回:
bool: 创建成功返回True失败返回False
"""
# 检查用户名是否已存在
if get_user_by_username(username):
print(f"用户名 {username} 已存在")
return False
# 生成新的用户ID
import time
user_id = int(time.time() * 1000) # 使用时间戳作为用户ID
user_data = {
"user_id": user_id,
"username": username,
"password": password,
"premission": permission
}
return write_user_data(user_data)
def get_all_users():
"""
获取所有用户信息
返回:
list: 包含所有用户信息的列表
"""
try:
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"match_all": {}
},
"size": 1000 # 限制返回数量,可根据需要调整
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
users = []
for hit in results:
user_data = hit["_source"]
user_data["_id"] = hit["_id"] # 添加文档ID用于后续操作
users.append(user_data)
return users
except requests.exceptions.HTTPError as e:
print(f"获取用户列表失败: {e.response.text}")
return []
def update_user_password(username, new_password):
"""
更新用户密码
参数:
username (str): 用户名
new_password (str): 新密码
返回:
bool: 更新成功返回True失败返回False
"""
try:
# 先查找用户
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"username": username
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if not results:
print(f"用户 {username} 不存在")
return False
# 获取用户文档ID
doc_id = results[0]["_id"]
user_data = results[0]["_source"]
# 更新密码
user_data["password"] = new_password
# 更新文档
update_response = requests.post(
f"{ES_URL}/{users_index_name}/_doc/{doc_id}",
auth=AUTH,
json=user_data,
headers={"Content-Type": "application/json"}
)
update_response.raise_for_status()
print(f"用户 {username} 密码更新成功")
return True
except requests.exceptions.HTTPError as e:
print(f"更新用户密码失败: {e.response.text}")
return False
def delete_user(username):
"""
删除用户
参数:
username (str): 要删除的用户名
返回:
bool: 删除成功返回True失败返回False
"""
try:
# 防止删除管理员账户
if username == "admin":
print("不能删除管理员账户")
return False
# 先查找用户
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"username": username
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if not results:
print(f"用户 {username} 不存在")
return False
# 获取用户文档ID
doc_id = results[0]["_id"]
# 删除用户
delete_response = requests.delete(
f"{ES_URL}/{users_index_name}/_doc/{doc_id}",
auth=AUTH
)
delete_response.raise_for_status()
print(f"用户 {username} 删除成功")
return True
except requests.exceptions.HTTPError as e:
print(f"删除用户失败: {e.response.text}")
return False
def update_user_permission(username, new_permission):
"""
更新用户权限
参数:
username (str): 用户名
new_permission (int): 新权限级别
返回:
bool: 更新成功返回True失败返回False
"""
try:
# 防止修改管理员权限
if username == "admin":
print("不能修改管理员权限")
return False
# 先查找用户
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"username": username
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if not results:
print(f"用户 {username} 不存在")
return False
# 获取用户文档ID
doc_id = results[0]["_id"]
user_data = results[0]["_source"]
# 更新权限
user_data["premission"] = new_permission
# 更新文档
update_response = requests.post(
f"{ES_URL}/{users_index_name}/_doc/{doc_id}",
auth=AUTH,
json=user_data,
headers={"Content-Type": "application/json"}
)
update_response.raise_for_status()
print(f"用户 {username} 权限更新成功")
return True
except requests.exceptions.HTTPError as e:
print(f"更新用户权限失败: {e.response.text}")
return False
def search_data_by_user(user_id, keyword=None):
"""
根据用户ID查询该用户的数据支持关键词搜索
参数:
user_id (str): 用户ID
keyword (str, optional): 搜索关键词
返回:
list: 包含文档ID和源数据的列表
"""
try:
if keyword:
# 带关键词的搜索
query = {
"bool": {
"must": [
{"term": {"user_id": user_id}},
{
"multi_match": {
"query": keyword,
"fields": ["*"],
"fuzziness": "AUTO"
}
}
]
}
}
else:
# 只按用户ID搜索
query = {
"term": {"user_id": user_id}
}
response = requests.post(
f"{ES_URL}/{data_index_name}/_search",
auth=AUTH,
json={
"query": query,
"size": 1000 # 限制返回数量
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
# 返回包含文档ID和源数据的列表
return [{
"_id": hit["_id"],
**hit["_source"]
} for hit in results]
except requests.exceptions.HTTPError as e:
print(f"查询用户数据失败: {e.response.text}")
return []
def update_data_by_id(doc_id, updated_data, user_id):
"""
根据文档ID更新数据仅允许数据所有者修改
参数:
doc_id (str): 文档ID
updated_data (dict): 更新的数据
user_id (str): 当前用户ID
返回:
bool: 更新成功返回True失败返回False
"""
try:
# 先查询文档,验证所有权
response = requests.get(
f"{ES_URL}/{data_index_name}/_doc/{doc_id}",
auth=AUTH
)
response.raise_for_status()
doc = response.json()
# 检查文档是否存在
if not doc.get("found"):
print(f"文档 {doc_id} 不存在")
return False
# 检查用户权限(只能修改自己的数据)
if doc["_source"].get("user_id") != user_id:
print(f"用户 {user_id} 无权修改文档 {doc_id}")
return False
# 保持用户ID不变
updated_data["user_id"] = user_id
# 更新文档
update_response = requests.post(
f"{ES_URL}/{data_index_name}/_doc/{doc_id}",
auth=AUTH,
json=updated_data,
headers={"Content-Type": "application/json"}
)
update_response.raise_for_status()
print(f"文档 {doc_id} 更新成功")
return True
except requests.exceptions.HTTPError as e:
print(f"更新文档失败: {e.response.text}")
return False
def delete_data_by_id(doc_id, user_id):
"""
根据文档ID删除数据仅允许数据所有者或管理员删除
参数:
doc_id (str): 文档ID
user_id (str): 当前用户ID
返回:
bool: 删除成功返回True失败返回False
"""
try:
# 先查询文档,验证所有权
response = requests.get(
f"{ES_URL}/{data_index_name}/_doc/{doc_id}",
auth=AUTH
)
response.raise_for_status()
doc = response.json()
# 检查文档是否存在
if not doc.get("found"):
print(f"文档 {doc_id} 不存在")
return False
# 检查用户权限(只能删除自己的数据,管理员可以删除所有数据)
doc_user_id = doc["_source"].get("user_id")
if doc_user_id != user_id:
# 检查是否为管理员
user_info = get_user_by_username(user_id) # 这里需要用户名,稍后会修改
if not user_info or user_info.get("premission") != 0:
print(f"用户 {user_id} 无权删除文档 {doc_id}")
return False
# 删除文档
delete_response = requests.delete(
f"{ES_URL}/{data_index_name}/_doc/{doc_id}",
auth=AUTH
)
delete_response.raise_for_status()
print(f"文档 {doc_id} 删除成功")
return True
except requests.exceptions.HTTPError as e:
print(f"删除文档失败: {e.response.text}")
return False
def update_user_own_password(user_id, old_password, new_password):
"""
用户修改自己的密码
参数:
user_id (str): 用户ID
old_password (str): 旧密码
new_password (str): 新密码
返回:
bool: 修改成功返回True失败返回False
"""
try:
# 先查找用户
response = requests.post(
f"{ES_URL}/{users_index_name}/_search",
auth=AUTH,
json={
"query": {
"term": {
"user_id": user_id
}
}
}
)
response.raise_for_status()
results = response.json()["hits"]["hits"]
if not results:
print(f"用户 {user_id} 不存在")
return False
user_data = results[0]["_source"]
doc_id = results[0]["_id"]
# 验证旧密码
if user_data.get("password") != old_password:
print("旧密码错误")
return False
# 更新密码
user_data["password"] = new_password
# 更新文档
update_response = requests.post(
f"{ES_URL}/{users_index_name}/_doc/{doc_id}",
auth=AUTH,
json=user_data,
headers={"Content-Type": "application/json"}
)
update_response.raise_for_status()
print(f"用户 {user_id} 密码修改成功")
return True
except requests.exceptions.HTTPError as e:
print(f"修改密码失败: {e.response.text}")
return False

592
app.py
View File

@@ -1,10 +1,13 @@
import base64 import base64
from flask import Flask, request, render_template, redirect, url_for, jsonify from flask import Flask, request, render_template, redirect, url_for, jsonify, session, flash, send_from_directory
from werkzeug.utils import secure_filename
import os import os
import uuid import uuid
from PIL import Image from PIL import Image
import re import re
import json import json
import requests
from functools import wraps
from ESConnect import * from ESConnect import *
from json_converter import json_to_string, string_to_json from json_converter import json_to_string, string_to_json
from openai import OpenAI from openai import OpenAI
@@ -14,7 +17,45 @@ from openai import OpenAI
app = Flask(__name__) app = Flask(__name__)
# app.config.from_object(config.Config) # app.config.from_object(config.Config)
# 设置会话密钥,用于加密会话数据
app.secret_key = 'your-secret-key-change-this-in-production'
# OCR和信息提取函数使用大模型API处理图片并提取结构化信息 # OCR和信息提取函数使用大模型API处理图片并提取结构化信息
# 权限装饰器
def login_required(f):
"""要求用户登录的装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请先登录', 'error')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""要求管理员权限的装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请先登录', 'error')
return redirect(url_for('login'))
if session.get('permission', 1) != 0:
flash('权限不足,需要管理员权限', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
def user_or_admin_required(f):
"""要求普通用户或管理员权限的装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
flash('请先登录', 'error')
return redirect(url_for('login'))
permission = session.get('permission', 1)
if permission not in [0, 1]:
flash('权限不足', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
def ocr_and_extract_info(image_path): def ocr_and_extract_info(image_path):
""" """
使用大模型API进行OCR识别并提取图片中的结构化信息 使用大模型API进行OCR识别并提取图片中的结构化信息
@@ -138,8 +179,258 @@ def ocr_and_extract_info(image_path):
""" """
# 登录页面路由
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
处理用户登录
GET: 显示登录页面
POST: 处理登录表单提交
"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
flash('请输入用户名和密码', 'error')
return render_template('login.html')
# 验证用户
user_data = verify_user(username, password)
if user_data:
# 登录成功,设置会话
session['user_id'] = user_data['user_id']
session['username'] = user_data['username']
session['permission'] = user_data['premission']
flash(f'欢迎回来,{username}', 'success')
return redirect(url_for('index'))
else:
flash('用户名或密码错误', 'error')
return render_template('login.html')
return render_template('login.html')
# 登出路由
@app.route('/logout')
def logout():
"""
处理用户登出
"""
session.clear()
flash('已成功登出', 'info')
return redirect(url_for('login'))
# 用户管理页面路由
@app.route('/user_management')
@admin_required
def user_management():
"""
显示用户管理页面(仅管理员可访问)
"""
users = get_all_users()
return render_template('user_management.html', users=users)
# 注册新用户路由
@app.route('/register', methods=['GET', 'POST'])
@admin_required
def register():
"""
注册新用户(仅管理员可访问)
GET: 显示注册页面
POST: 处理注册表单提交
"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
permission = int(request.form.get('permission', 1))
# 验证输入
if not username or not password:
flash('请输入用户名和密码', 'error')
return render_template('register.html')
if password != confirm_password:
flash('两次输入的密码不一致', 'error')
return render_template('register.html')
if len(password) < 6:
flash('密码长度至少6位', 'error')
return render_template('register.html')
# 检查用户名是否已存在
existing_user = get_user_by_username(username)
if existing_user:
flash('用户名已存在', 'error')
return render_template('register.html')
# 创建新用户
success = create_user(username, password, permission)
if success:
flash(f'用户 {username} 创建成功', 'success')
return redirect(url_for('user_management'))
else:
flash('创建用户失败', 'error')
return render_template('register.html')
return render_template('register.html')
# 修改用户密码路由
@app.route('/change_password/<username>', methods=['POST'])
@admin_required
def change_password(username):
"""
修改用户密码(仅管理员可访问)
"""
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if not new_password or not confirm_password:
flash('请输入新密码', 'error')
return redirect(url_for('user_management'))
if new_password != confirm_password:
flash('两次输入的密码不一致', 'error')
return redirect(url_for('user_management'))
if len(new_password) < 6:
flash('密码长度至少6位', 'error')
return redirect(url_for('user_management'))
success = update_user_password(username, new_password)
if success:
flash(f'用户 {username} 密码修改成功', 'success')
else:
flash(f'修改用户 {username} 密码失败', 'error')
return redirect(url_for('user_management'))
# 修改用户权限路由
@app.route('/change_permission/<username>', methods=['POST'])
@admin_required
def change_permission(username):
"""
修改用户权限(仅管理员可访问)
"""
new_permission = int(request.form.get('permission', 1))
success = update_user_permission(username, new_permission)
if success:
flash(f'用户 {username} 权限修改成功', 'success')
else:
flash(f'修改用户 {username} 权限失败', 'error')
return redirect(url_for('user_management'))
# 删除用户路由
@app.route('/delete_user/<username>', methods=['POST'])
@admin_required
def delete_user_route(username):
"""
删除用户(仅管理员可访问)
"""
success = delete_user(username)
if success:
flash(f'用户 {username} 删除成功', 'success')
else:
flash(f'删除用户 {username} 失败', 'error')
return redirect(url_for('user_management'))
# 个人设置页面路由
@app.route('/profile')
@login_required
def profile():
"""
显示个人设置页面
"""
return render_template('profile.html')
# 修改个人密码路由
@app.route('/change_own_password', methods=['POST'])
@login_required
def change_own_password():
"""
用户修改自己的密码
"""
old_password = request.form.get('old_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# 验证输入
if not old_password or not new_password or not confirm_password:
flash('请填写所有密码字段', 'error')
return redirect(url_for('profile'))
if new_password != confirm_password:
flash('两次输入的新密码不一致', 'error')
return redirect(url_for('profile'))
if len(new_password) < 6:
flash('新密码长度至少6位', 'error')
return redirect(url_for('profile'))
# 调用修改密码函数
success = update_user_own_password(session['user_id'], old_password, new_password)
if success:
flash('密码修改成功', 'success')
else:
flash('密码修改失败,请检查旧密码是否正确', 'error')
return redirect(url_for('profile'))
# 个人数据页面路由
@app.route('/my_data')
@login_required
def my_data():
"""
显示用户自己的数据
"""
user_id = session['user_id']
keyword = request.args.get('keyword', '')
# 查询用户自己的数据
if keyword:
data = search_data_by_user(user_id, keyword)
else:
data = search_data_by_user(user_id)
# 将data字段从字符串转换回JSON格式以便显示
processed_data = []
for item in data:
if 'data' in item and item['data']:
try:
# 将data字段的字符串转换回JSON
original_data = string_to_json(item['data'])
# 合并原始数据和其他字段
display_item = {
'_id': item['_id'],
'image': item.get('image', ''),
**original_data # 展开原始数据字段
}
processed_data.append(display_item)
except Exception as e:
# 如果转换失败,保持原始格式
processed_data.append(item)
else:
processed_data.append(item)
return render_template('my_data.html', data=processed_data, keyword=keyword)
# 首页路由 # 首页路由
@app.route('/') @app.route('/')
@login_required
def index(): def index():
""" """
渲染首页模板 渲染首页模板
@@ -151,12 +442,13 @@ def index():
# 图片上传路由 # 图片上传路由
@app.route('/upload', methods=['POST']) @app.route('/upload', methods=['POST'])
@user_or_admin_required
def upload_image(): def upload_image():
""" """
处理图片上传请求调用OCR识别存储结果 处理图片上传请求调用OCR识别但不存储结果
返回: 返回:
JSON: 上传成功或失败的响应 JSON: 识别结果,供用户编辑确认
""" """
# 获取上传的文件 # 获取上传的文件
file = request.files.get('file') file = request.files.get('file')
@@ -173,20 +465,13 @@ def upload_image():
print(f"开始处理图片: {image_path}") print(f"开始处理图片: {image_path}")
original_data = ocr_and_extract_info(image_path) # 获取原始JSON数据 original_data = ocr_and_extract_info(image_path) # 获取原始JSON数据
if original_data: if original_data:
# 使用json_converter将JSON数据转换为字符串 print(f"识别成功: {original_data}")
data_string = json_to_string(original_data) # 返回识别结果和图片文件名,供用户编辑确认
print(f"转换后的数据字符串: {data_string}") return jsonify({
"message": "识别成功,请确认数据后点击录入",
# 构造新的数据结构只包含data和image字段 "data": original_data,
processed_data = { "image": filename
"data": data_string, })
"image": filename # 存储图片文件名
}
print(f"准备存储的数据: {processed_data}")
insert_data(processed_data) # 存入ES
print("✓ 数据成功存储到Elasticsearch")
return jsonify({"message": "成功录入", "data": original_data, "processed": processed_data})
else: else:
print("✗ 无法识别图片内容") print("✗ 无法识别图片内容")
return jsonify({"error": "无法识别图片内容"}), 400 return jsonify({"error": "无法识别图片内容"}), 400
@@ -194,8 +479,54 @@ def upload_image():
print(f"✗ 处理过程中发生错误: {str(e)}") print(f"✗ 处理过程中发生错误: {str(e)}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# 确认录入路由
@app.route('/confirm', methods=['POST'])
@user_or_admin_required
def confirm_data():
"""
确认并录入用户编辑后的数据
返回:
JSON: 录入成功或失败的响应
"""
try:
# 获取前端提交的数据
request_data = request.get_json()
if not request_data:
return jsonify({"error": "没有接收到数据"}), 400
# 获取编辑后的数据和图片文件名
edited_data = request_data.get('data', {})
image_filename = request_data.get('image', '')
if not edited_data:
return jsonify({"error": "数据不能为空"}), 400
# 使用json_converter将JSON数据转换为字符串
data_string = json_to_string(edited_data)
print(f"转换后的数据字符串: {data_string}")
# 构造新的数据结构只包含data和image字段并添加用户ID
processed_data = {
"data": data_string,
"image": image_filename, # 存储图片文件名
"user_id": session['user_id'] # 添加用户ID关联
}
print(f"准备存储的数据: {processed_data}")
# 存入ES
insert_data(processed_data)
print("✓ 数据成功存储到Elasticsearch")
return jsonify({"message": "数据录入成功", "data": edited_data})
except Exception as e:
print(f"✗ 录入过程中发生错误: {str(e)}")
return jsonify({"error": str(e)}), 500
# 搜索路由 # 搜索路由
@app.route('/search') @app.route('/search')
@user_or_admin_required
def search(): def search():
""" """
处理搜索请求从Elasticsearch中检索匹配的数据 处理搜索请求从Elasticsearch中检索匹配的数据
@@ -235,6 +566,7 @@ def search():
# 结果页面路由 # 结果页面路由
@app.route('/results') @app.route('/results')
@user_or_admin_required
def results_page(): def results_page():
""" """
渲染搜索结果页面 渲染搜索结果页面
@@ -246,6 +578,7 @@ def results_page():
# 显示所有数据路由 # 显示所有数据路由
@app.route('/all') @app.route('/all')
@admin_required
def show_all(): def show_all():
""" """
获取所有数据并渲染到页面 获取所有数据并渲染到页面
@@ -276,85 +609,24 @@ def show_all():
return render_template('all.html', data=processed_data) return render_template('all.html', data=processed_data)
# 编辑数据页面路由 # 添加图片路由
@app.route('/edit/<doc_id>') @app.route('/image/<filename>')
def edit_entry(doc_id): def serve_image(filename):
""" """
渲染编辑页面 提供图片文件服务
参数: 参数:
doc_id (str): 要编辑的文档ID filename (str): 图片文件名
返回: 返回:
str: 渲染后的编辑页面或错误信息 Response: 图片文件响应
""" """
# 获取要编辑的文档数据 from flask import send_from_directory
document = get_by_id(doc_id) return send_from_directory('image', filename)
if not document:
return "文档不存在", 404
# 保持原始数据格式不进行JSON转换
# 直接传递包含data字段的原始文档
return render_template('edit.html', document=document)
# 更新数据路由
@app.route('/update/<doc_id>', methods=['POST'])
def update_entry(doc_id):
"""
处理数据更新请求
参数:
doc_id (str): 要更新的文档ID
返回:
重定向到所有数据页面或错误信息
"""
# 获取原文档的图片信息
original_doc = get_by_id(doc_id)
if not original_doc:
return "文档不存在", 404
# 从表单中获取所有字段数据
data_parts = []
i = 1
while True:
key_name = request.form.get(f'key_{i}')
field_value = request.form.get(f'field_{i}')
if not key_name or not field_value:
break
# 处理字段值(如果是列表格式,用|##|分隔)
if ',' in field_value:
# 如果是逗号分隔的值,转换为列表格式
items = [item.strip() for item in field_value.split(',') if item.strip()]
if len(items) > 1:
field_value = f"[{'|##|'.join(items)}]"
data_parts.append(f"{key_name}:{field_value}")
i += 1
# 验证是否有数据
if not data_parts:
return "没有可更新的数据", 400
# 构建新的数据字符串
data_value = "|###|".join(data_parts)
# 构造更新数据
updated_data = {
'data': data_value,
'image': original_doc.get('image', '') # 保持原图片
}
# 更新文档
if update_by_id(doc_id, updated_data):
return redirect(url_for('show_all'))
else:
return "更新失败", 500
# 删除数据路由 # 删除数据路由
@app.route('/delete/<doc_id>', methods=['POST']) @app.route('/delete/<doc_id>', methods=['POST'])
@login_required
def delete_entry(doc_id): def delete_entry(doc_id):
""" """
根据文档ID删除数据 根据文档ID删除数据
@@ -365,58 +637,144 @@ def delete_entry(doc_id):
返回: 返回:
重定向到所有数据页面或错误信息 重定向到所有数据页面或错误信息
""" """
user_id = session['user_id']
user_permission = session.get('permission', 1)
# 管理员可以删除所有数据,普通用户只能删除自己的数据
if user_permission == 0: # 管理员
success = delete_by_id(doc_id)
redirect_url = 'show_all'
else: # 普通用户
success = delete_data_by_id(doc_id, user_id)
redirect_url = 'my_data'
if success:
return redirect(url_for(redirect_url))
if delete_by_id(doc_id): if delete_by_id(doc_id):
return redirect(url_for('show_all')) return redirect(url_for('show_all'))
else: else:
return "删除失败", 500 return "删除失败", 500
# 批量删除数据路由 # 批量删除数据路由
@app.route('/batch_delete', methods=['POST']) @app.route('/batch_delete', methods=['POST'])
@admin_required
def batch_delete(): def batch_delete():
""" """
批量删除数据 批量删除选中的数据(仅管理员可访问)
返回: 返回:
重定向到所有数据页面或错误信息 重定向到所有数据页面或错误信息
""" """
doc_ids = request.form.getlist('doc_ids') try:
if not doc_ids: # 获取选中的文档ID列表
return "没有选择要删除的文档", 400 doc_ids = request.form.getlist('doc_ids')
success_count = 0 if not doc_ids:
for doc_id in doc_ids: flash('请选择要删除的记录', 'error')
if delete_by_id(doc_id): return redirect(url_for('show_all'))
success_count += 1
# 批量删除选中的文档
success_count = 0
for doc_id in doc_ids:
if delete_by_id(doc_id):
success_count += 1
if success_count > 0:
flash(f'成功删除 {success_count} 条记录', 'success')
else:
flash('删除失败,请重试', 'error')
if success_count == len(doc_ids):
return redirect(url_for('show_all')) return redirect(url_for('show_all'))
else:
return f"成功删除 {success_count} 条记录,失败 {len(doc_ids) - success_count}", 500 except Exception as e:
print(f"批量删除失败: {str(e)}")
flash('批量删除失败,请重试', 'error')
return redirect(url_for('show_all'))
@app.route('/edit/<doc_id>', methods=['GET', 'POST'])
# 提供图片访问的路由 @login_required
@app.route('/image/<filename>') def edit_entry(doc_id):
def serve_image(filename):
""" """
提供image目录下图片的访问服务 编辑数据条目(用户只能编辑自己的数据)
参数:
filename (str): 图片文件名
返回:
图片文件或404错误
""" """
import os if request.method == 'GET':
from flask import send_from_directory # 获取要编辑的数据
try:
# 先获取文档检查权限
response = requests.get(
f"{ES_URL}/{data_index_name}/_doc/{doc_id}",
auth=AUTH
)
response.raise_for_status()
doc = response.json()
# 确保文件存在 if not doc.get("found"):
image_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'image') flash('数据不存在', 'error')
if not os.path.exists(os.path.join(image_dir, filename)): return redirect(url_for('my_data'))
return "图片不存在", 404
# 发送图片文件 # 检查权限
return send_from_directory(image_dir, filename) user_id = session['user_id']
user_permission = session.get('permission', 1)
doc_user_id = doc["_source"].get("user_id")
# 管理员可以编辑所有数据,普通用户只能编辑自己的数据
if user_permission != 0 and doc_user_id != user_id:
flash('您无权编辑此数据', 'error')
return redirect(url_for('my_data'))
# 解析数据
data_str = doc["_source"].get("data", "{}")
original_data = string_to_json(data_str)
edit_data = {
'_id': doc_id,
'image': doc["_source"].get('image', ''),
**original_data
}
return render_template('edit.html', data=edit_data)
except Exception as e:
flash('获取数据失败', 'error')
return redirect(url_for('my_data'))
else: # POST 请求 - 保存编辑
try:
# 获取编辑后的数据
edited_data = {}
for key, value in request.form.items():
if key != '_id' and key != 'image':
edited_data[key] = value
# 转换为字符串格式
data_string = json_to_string(edited_data)
# 构造更新数据
updated_data = {
"data": data_string,
"image": request.form.get('image', ''),
"user_id": session['user_id']
}
# 更新数据
success = update_data_by_id(doc_id, updated_data, session['user_id'])
if success:
flash('数据更新成功', 'success')
else:
flash('数据更新失败', 'error')
# 根据用户权限重定向
if session.get('permission', 1) == 0:
return redirect(url_for('show_all'))
else:
return redirect(url_for('my_data'))
except Exception as e:
flash('保存数据失败', 'error')
return redirect(url_for('my_data'))
# 主程序入口 # 主程序入口
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -35,6 +35,9 @@
box-shadow: var(--shadow); box-shadow: var(--shadow);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
} }
.header:before { .header:before {
@@ -62,6 +65,51 @@
text-shadow: 0 0 5px rgba(0,0,0,0.2); text-shadow: 0 0 5px rgba(0,0,0,0.2);
} }
.user-info {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 15px;
}
.user-info .username {
font-size: 14px;
opacity: 0.9;
}
.user-info .permission-badge {
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.user-info .permission-badge.admin {
background: var(--accent);
}
.user-info .permission-badge.user {
background: var(--success);
}
.logout-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 6px 12px;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
transition: var(--transition);
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
.sidebar { .sidebar {
width: 240px; width: 240px;
height: calc(100vh - 60px); height: calc(100vh - 60px);
@@ -152,6 +200,15 @@
<body> <body>
<div class="header"> <div class="header">
<h1><span>紫金</span> 稷下薪火·云枢智海师生成果共创系统</h1> <h1><span>紫金</span> 稷下薪火·云枢智海师生成果共创系统</h1>
<div class="user-info">
<span class="username">{{ session.username }}</span>
{% if session.permission == 0 %}
<span class="permission-badge admin">管理员</span>
{% elif session.permission == 1 %}
<span class="permission-badge user">普通用户</span>
{% endif %}
<a href="{{ url_for('logout') }}" class="logout-btn">登出</a>
</div>
</div> </div>
<div class="sidebar"> <div class="sidebar">
@@ -161,12 +218,53 @@
<a href="{{ url_for('results_page') }}" {% if request.endpoint == 'results_page' %}class="active"{% endif %}> <a href="{{ url_for('results_page') }}" {% if request.endpoint == 'results_page' %}class="active"{% endif %}>
<i>📈</i> 查询统计 <i>📈</i> 查询统计
</a> </a>
<a href="{{ url_for('my_data') }}" {% if request.endpoint == 'my_data' or request.endpoint == 'edit_entry' %}class="active"{% endif %}>
<i>📋</i> 我的数据
</a>
{% if session.permission == 0 %}
<a href="{{ url_for('show_all') }}" {% if request.endpoint == 'show_all' %}class="active"{% endif %}> <a href="{{ url_for('show_all') }}" {% if request.endpoint == 'show_all' %}class="active"{% endif %}>
<i>📁</i> 数据操作 <i>📁</i> 数据操作
</a> </a>
{% endif %}
<a href="{{ url_for('profile') }}" {% if request.endpoint == 'profile' %}class="active"{% endif %}>
<i>⚙️</i> 个人设置
</a>
{% if session.permission == 0 %}
<a href="{{ url_for('user_management') }}" {% if request.endpoint == 'user_management' or request.endpoint == 'register' %}class="active"{% endif %}>
<i>👥</i> 用户管理
</a>
{% endif %}
</div> </div>
<div class="content"> <div class="content">
<!-- Flash消息显示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages" style="margin-bottom: 20px;">
{% for category, message in messages %}
<div class="flash-message {{ category }}" style="padding: 10px; border-radius: 5px; margin-bottom: 10px;">{{ message }}</div>
{% endfor %}
</div>
<style>
.flash-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.flash-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.flash-message.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
</style>
{% endif %}
{% endwith %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -1,256 +1,316 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}编辑数据 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %} {% block title %}编辑数据{% endblock %}
{% block content %} {% block content %}
<style> <div class="edit-container">
/* 基础样式重置 */ <div class="edit-header">
* { margin: 0; padding: 0; box-sizing: border-box; } <h1>编辑数据</h1>
<p>修改您的数据信息</p>
</div>
/* 容器样式 */ <form method="POST" class="edit-form" id="editForm">
.container { <!-- 隐藏字段 -->
max-width: 800px; <input type="hidden" name="_id" value="{{ data._id }}">
margin: 0 auto; <input type="hidden" name="image" value="{{ data.image }}">
padding: 20px;
}
/* 标题样式 */ <!-- 图片显示 -->
h2 { {% if data.image %}
color: #2c3e50; <div class="image-preview">
border-bottom: 2px solid #3498db; <h3>关联图片</h3>
padding-bottom: 8px; <img src="{{ url_for('serve_image', filename=data.image) }}" alt="数据图片" onclick="openImageModal('{{ url_for('serve_image', filename=data.image) }}')">
margin-bottom: 20px;
}
/* 表单样式 */
.form-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e1e8ed;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-hint {
font-size: 12px;
color: #7f8c8d;
margin-top: 5px;
}
/* 按钮样式 */
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: linear-gradient(to right, #3498db, #2980b9);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.3);
}
.btn-secondary {
background: linear-gradient(to right, #95a5a6, #7f8c8d);
color: white;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(149, 165, 166, 0.3);
}
.btn-danger {
background: linear-gradient(to right, #e74c3c, #c0392b);
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.3);
}
/* 图片预览样式 */
.image-preview {
margin-top: 10px;
text-align: center;
}
.image-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 错误提示样式 */
.error-message {
color: #e74c3c;
font-size: 12px;
margin-top: 5px;
}
/* 必填字段标记 */
.required {
color: #e74c3c;
}
</style>
<div class="container">
<h2>编辑成果信息</h2>
<div class="form-container">
<form action="{{ url_for('update_entry', doc_id=document._id) }}" method="POST" id="editForm">
{% if document.data %}
{# 从原始数据中解析字段 #}
{% set data_string = document.data %}
{% set pairs = data_string.split('|###|') %}
{% for pair in pairs %}
{% if ':' in pair %}
{% set key_value = pair.split(':', 1) %}
{% set field_key = key_value[0].strip() %}
{% set field_value = key_value[1].strip() %}
{# 处理列表格式 [item1|##|item2] #}
{% if field_value.startswith('[') and field_value.endswith(']') %}
{% set list_content = field_value[1:-1] %}
{% set field_value = list_content.split('|##|')|join(', ') %}
{% endif %}
<div class="form-group">
<label for="field_{{ loop.index }}">{{ field_key }} <span class="required">*</span></label>
<input type="text" id="field_{{ loop.index }}" name="field_{{ loop.index }}" value="{{ field_value }}" required>
<input type="hidden" name="key_{{ loop.index }}" value="{{ field_key }}">
</div>
{% endif %}
{% endfor %}
{% else %}
{# 如果没有data字段显示提示信息 #}
<div class="form-group">
<p style="color: #e74c3c; text-align: center;">该记录没有可编辑的数据</p>
</div>
{% endif %}
{% if document.image %}
<div class="form-group">
<label>原图片预览</label>
<div class="image-preview">
<img src="{{ url_for('serve_image', filename=document.image) }}" alt="原图片" onerror="this.style.display='none'">
</div>
<div class="form-hint">当前关联的图片,编辑时无法修改图片</div>
</div> </div>
{% endif %} {% endif %}
<div class="button-group"> <!-- 数据字段编辑 -->
<button type="submit" class="btn btn-primary">保存修改</button> <div class="fields-container">
<a href="{{ url_for('show_all') }}" class="btn btn-secondary">取消返回</a> <h3>数据字段</h3>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">删除记录</button> {% for key, value in data.items() %}
</div> {% if key not in ['_id', 'image', 'user_id'] %}
</form> <div class="form-group">
<label for="{{ key }}">{{ key }}:</label>
<input type="text" id="{{ key }}" name="{{ key }}" value="{{ value }}" class="form-input">
</div>
{% endif %}
{% endfor %}
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="icon">💾</i>
保存修改
</button>
<a href="{{ url_for('my_data') }}" class="btn btn-secondary">
<i class="icon">↩️</i>
取消
</a>
</div>
</form>
</div>
<!-- 图片预览模态框 -->
<div id="imageModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeImageModal()">&times;</span>
<img id="modalImage" src="" alt="图片预览">
</div> </div>
</div> </div>
<script> <style>
// 表单验证 .edit-container {
document.getElementById('editForm').addEventListener('submit', function(e) { max-width: 800px;
// 检查所有字段是否都有值 margin: 0 auto;
const inputs = document.querySelectorAll('input[type="text"]'); padding: 20px;
let hasEmptyField = false; }
inputs.forEach(input => { .edit-header {
if (!input.value.trim()) { text-align: center;
hasEmptyField = true; margin-bottom: 30px;
input.style.borderColor = '#e74c3c'; padding: 30px;
} else { background: white;
input.style.borderColor = '#e1e8ed'; border-radius: 12px;
} box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}); }
if (hasEmptyField) { .edit-header h1 {
e.preventDefault(); color: #333;
alert('所有字段都必须填写!'); font-size: 28px;
return false; margin-bottom: 10px;
} }
return true; .edit-header p {
}); color: #666;
font-size: 16px;
}
// 删除确认 .edit-form {
function confirmDelete() { background: white;
if (confirm('确定要删除这条记录吗?此操作不可撤销!')) { border-radius: 12px;
// 创建删除表单并提交 padding: 30px;
const form = document.createElement('form'); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
form.method = 'POST'; }
form.action = '{{ url_for("delete_entry", doc_id=document._id) }}';
document.body.appendChild(form); .image-preview {
form.submit(); margin-bottom: 30px;
} text-align: center;
}
.image-preview h3 {
color: #333;
margin-bottom: 15px;
font-size: 18px;
}
.image-preview img {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
cursor: pointer;
transition: transform 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.image-preview img:hover {
transform: scale(1.02);
}
.fields-container {
margin-bottom: 30px;
}
.fields-container h3 {
color: #333;
margin-bottom: 20px;
font-size: 18px;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
font-size: 14px;
text-transform: capitalize;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: #f8f9fa;
}
.form-input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
padding-top: 20px;
border-top: 1px solid #e9ecef;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
position: relative;
margin: 5% auto;
padding: 20px;
width: 90%;
max-width: 800px;
background: white;
border-radius: 12px;
text-align: center;
}
.close {
position: absolute;
top: 15px;
right: 25px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #666;
}
.close:hover {
color: #333;
}
#modalImage {
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
}
@media (max-width: 768px) {
.edit-container {
padding: 10px;
} }
// 自动格式化逗号分隔的值 .edit-form {
document.querySelectorAll('input[type="text"]').forEach(input => { padding: 20px;
input.addEventListener('blur', function(e) { }
const value = e.target.value.trim();
if (value && value.includes(',')) { .form-actions {
// 格式化逗号分隔的值 flex-direction: column;
const formatted = value }
.split(',')
.map(item => item.trim()) .btn {
.filter(item => item) width: 100%;
.join(', '); justify-content: center;
e.target.value = formatted; }
} }
}); </style>
<script>
// 图片预览功能
function openImageModal(imageSrc) {
document.getElementById('modalImage').src = imageSrc;
document.getElementById('imageModal').style.display = 'block';
}
function closeImageModal() {
document.getElementById('imageModal').style.display = 'none';
}
// 点击模态框外部关闭
window.onclick = function(event) {
const imageModal = document.getElementById('imageModal');
if (event.target === imageModal) {
closeImageModal();
}
}
// 表单验证
document.getElementById('editForm').addEventListener('submit', function(e) {
const inputs = this.querySelectorAll('.form-input');
let hasEmptyFields = false;
inputs.forEach(input => {
if (!input.value.trim()) {
hasEmptyFields = true;
input.style.borderColor = '#dc3545';
} else {
input.style.borderColor = '#e9ecef';
}
}); });
if (hasEmptyFields) {
e.preventDefault();
alert('请填写所有字段!');
return false;
}
});
// 输入时清除错误状态
document.querySelectorAll('.form-input').forEach(input => {
input.addEventListener('input', function() {
if (this.value.trim()) {
this.style.borderColor = '#e9ecef';
}
});
});
</script> </script>
{% endblock %} {% endblock %}

256
templates/edited.html Normal file
View File

@@ -0,0 +1,256 @@
{% extends "base.html" %}
{% block title %}编辑成果信息 - 紫金·稷下薪火·云枢智海师生成果共创系统{% endblock %}
{% block content %}
<style>
/* 基础样式重置 */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* 容器样式 */
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
/* 标题样式 */
h2 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
margin-bottom: 20px;
}
/* 表单样式 */
.form-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #e1e8ed;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-hint {
font-size: 12px;
color: #7f8c8d;
margin-top: 5px;
}
/* 按钮样式 */
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: linear-gradient(to right, #3498db, #2980b9);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.3);
}
.btn-secondary {
background: linear-gradient(to right, #95a5a6, #7f8c8d);
color: white;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(149, 165, 166, 0.3);
}
.btn-danger {
background: linear-gradient(to right, #e74c3c, #c0392b);
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.3);
}
/* 图片预览样式 */
.image-preview {
margin-top: 10px;
text-align: center;
}
.image-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 错误提示样式 */
.error-message {
color: #e74c3c;
font-size: 12px;
margin-top: 5px;
}
/* 必填字段标记 */
.required {
color: #e74c3c;
}
</style>
<div class="container">
<h2>编辑成果信息</h2>
<div class="form-container">
<form action="{{ url_for('update_entry', doc_id=document._id) }}" method="POST" id="editForm">
{% if document.data %}
{# 从原始数据中解析字段 #}
{% set data_string = document.data %}
{% set pairs = data_string.split('|###|') %}
{% for pair in pairs %}
{% if ':' in pair %}
{% set key_value = pair.split(':', 1) %}
{% set field_key = key_value[0].strip() %}
{% set field_value = key_value[1].strip() %}
{# 处理列表格式 [item1|##|item2] #}
{% if field_value.startswith('[') and field_value.endswith(']') %}
{% set list_content = field_value[1:-1] %}
{% set field_value = list_content.split('|##|')|join(', ') %}
{% endif %}
<div class="form-group">
<label for="field_{{ loop.index }}">{{ field_key }} <span class="required">*</span></label>
<input type="text" id="field_{{ loop.index }}" name="field_{{ loop.index }}" value="{{ field_value }}" required>
<input type="hidden" name="key_{{ loop.index }}" value="{{ field_key }}">
</div>
{% endif %}
{% endfor %}
{% else %}
{# 如果没有data字段显示提示信息 #}
<div class="form-group">
<p style="color: #e74c3c; text-align: center;">该记录没有可编辑的数据</p>
</div>
{% endif %}
{% if document.image %}
<div class="form-group">
<label>原图片预览</label>
<div class="image-preview">
<img src="{{ url_for('serve_image', filename=document.image) }}" alt="原图片" onerror="this.style.display='none'">
</div>
<div class="form-hint">当前关联的图片,编辑时无法修改图片</div>
</div>
{% endif %}
<div class="button-group">
<button type="submit" class="btn btn-primary">保存修改</button>
<a href="{{ url_for('show_all') }}" class="btn btn-secondary">取消返回</a>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">删除记录</button>
</div>
</form>
</div>
</div>
<script>
// 表单验证
document.getElementById('editForm').addEventListener('submit', function(e) {
// 检查所有字段是否都有值
const inputs = document.querySelectorAll('input[type="text"]');
let hasEmptyField = false;
inputs.forEach(input => {
if (!input.value.trim()) {
hasEmptyField = true;
input.style.borderColor = '#e74c3c';
} else {
input.style.borderColor = '#e1e8ed';
}
});
if (hasEmptyField) {
e.preventDefault();
alert('所有字段都必须填写!');
return false;
}
return true;
});
// 删除确认
function confirmDelete() {
if (confirm('确定要删除这条记录吗?此操作不可撤销!')) {
// 创建删除表单并提交
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("delete_entry", doc_id=document._id) }}';
document.body.appendChild(form);
form.submit();
}
}
// 自动格式化逗号分隔的值
document.querySelectorAll('input[type="text"]').forEach(input => {
input.addEventListener('blur', function(e) {
const value = e.target.value.trim();
if (value && value.includes(',')) {
// 格式化逗号分隔的值
const formatted = value
.split(',')
.map(item => item.trim())
.filter(item => item)
.join(', ');
e.target.value = formatted;
}
});
});
</script>
{% endblock %}

162
templates/login.html Normal file
View File

@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录 - 成果录入系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.login-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
.login-btn:hover {
transform: translateY(-2px);
}
.flash-messages {
margin-bottom: 20px;
}
.flash-message {
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
}
.flash-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.flash-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.flash-message.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.login-footer {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>用户登录</h1>
<p>成果录入系统</p>
</div>
<!-- Flash消息显示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="flash-message {{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="login-btn">登录</button>
</form>
<div class="login-footer">
<p>默认管理员账号admin / admin</p>
</div>
</div>
</body>
</html>

486
templates/my_data.html Normal file
View File

@@ -0,0 +1,486 @@
{% extends "base.html" %}
{% block title %}我的数据{% endblock %}
{% block content %}
<div class="content-header">
<h1>我的数据</h1>
<p>查看和管理您录入的所有数据</p>
</div>
<!-- 搜索框 -->
<div class="search-container">
<form method="GET" action="{{ url_for('my_data') }}" class="search-form">
<div class="search-input-group">
<input type="text" name="keyword" value="{{ keyword }}" placeholder="搜索我的数据..." class="search-input">
<button type="submit" class="search-btn">
<i class="search-icon">🔍</i>
搜索
</button>
</div>
</form>
{% if keyword %}
<div class="search-info">
<span>搜索关键词: "{{ keyword }}"</span>
<a href="{{ url_for('my_data') }}" class="clear-search">清除搜索</a>
</div>
{% endif %}
</div>
<!-- 数据统计 -->
<div class="data-stats">
<div class="stat-item">
<span class="stat-number">{{ data|length }}</span>
<span class="stat-label">条记录</span>
</div>
</div>
<!-- 数据列表 -->
<div class="data-container">
{% if data %}
<div class="data-grid">
{% for item in data %}
<div class="data-card">
<!-- 图片显示 -->
{% if item.image %}
<div class="card-image">
<img src="{{ url_for('serve_image', filename=item.image) }}" alt="数据图片" onclick="openImageModal('{{ url_for('serve_image', filename=item.image) }}')">
</div>
{% endif %}
<!-- 数据内容 -->
<div class="card-content">
{% for key, value in item.items() %}
{% if key not in ['_id', 'image', 'user_id'] %}
<div class="data-field">
<span class="field-label">{{ key }}:</span>
<span class="field-value">{{ value }}</span>
</div>
{% endif %}
{% endfor %}
</div>
<!-- 操作按钮 -->
<div class="card-actions">
<a href="{{ url_for('edit_entry', doc_id=item._id) }}" class="btn btn-edit">
<i class="icon">✏️</i>
编辑
</a>
<button onclick="confirmDelete('{{ item._id }}')" class="btn btn-delete">
<i class="icon">🗑️</i>
删除
</button>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">📝</div>
<h3>暂无数据</h3>
<p>{% if keyword %}没有找到匹配 "{{ keyword }}" 的数据{% else %}您还没有录入任何数据{% endif %}</p>
<a href="{{ url_for('index') }}" class="btn btn-primary">开始录入数据</a>
</div>
{% endif %}
</div>
<!-- 图片预览模态框 -->
<div id="imageModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeImageModal()">&times;</span>
<img id="modalImage" src="" alt="图片预览">
</div>
</div>
<!-- 删除确认模态框 -->
<div id="deleteModal" class="modal">
<div class="modal-content modal-small">
<h3>确认删除</h3>
<p>您确定要删除这条数据吗?此操作不可撤销。</p>
<div class="modal-actions">
<button onclick="closeDeleteModal()" class="btn btn-secondary">取消</button>
<form id="deleteForm" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">确认删除</button>
</form>
</div>
</div>
</div>
<style>
.content-header {
margin-bottom: 30px;
text-align: center;
}
.content-header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.content-header p {
color: #666;
font-size: 16px;
}
.search-container {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.search-form {
margin-bottom: 15px;
}
.search-input-group {
display: flex;
gap: 10px;
max-width: 500px;
margin: 0 auto;
}
.search-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
.search-btn {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: transform 0.2s ease;
}
.search-btn:hover {
transform: translateY(-2px);
}
.search-info {
text-align: center;
color: #666;
font-size: 14px;
}
.clear-search {
color: #667eea;
text-decoration: none;
margin-left: 10px;
}
.clear-search:hover {
text-decoration: underline;
}
.data-stats {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-item {
display: inline-flex;
flex-direction: column;
align-items: center;
}
.stat-number {
font-size: 32px;
font-weight: 700;
color: #667eea;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 5px;
}
.data-container {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.data-card {
border: 2px solid #e9ecef;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
background: #f8f9fa;
}
.data-card:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
}
.card-image {
height: 200px;
overflow: hidden;
background: #f0f0f0;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
transition: transform 0.3s ease;
}
.card-image img:hover {
transform: scale(1.05);
}
.card-content {
padding: 20px;
}
.data-field {
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.field-label {
font-weight: 600;
color: #555;
min-width: 80px;
}
.field-value {
color: #333;
flex: 1;
word-break: break-word;
}
.card-actions {
padding: 15px 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.btn-edit {
background: #28a745;
color: white;
}
.btn-edit:hover {
background: #218838;
transform: translateY(-1px);
}
.btn-delete {
background: #dc3545;
color: white;
}
.btn-delete:hover {
background: #c82333;
transform: translateY(-1px);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 24px;
font-size: 16px;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-icon {
font-size: 64px;
margin-bottom: 20px;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
color: #333;
}
.empty-state p {
font-size: 16px;
margin-bottom: 30px;
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
position: relative;
margin: 5% auto;
padding: 20px;
width: 90%;
max-width: 800px;
background: white;
border-radius: 12px;
text-align: center;
}
.modal-small {
max-width: 400px;
margin: 15% auto;
}
.close {
position: absolute;
top: 15px;
right: 25px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #666;
}
.close:hover {
color: #333;
}
#modalImage {
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
@media (max-width: 768px) {
.data-grid {
grid-template-columns: 1fr;
}
.search-input-group {
flex-direction: column;
}
.card-actions {
justify-content: center;
}
}
</style>
<script>
// 图片预览功能
function openImageModal(imageSrc) {
document.getElementById('modalImage').src = imageSrc;
document.getElementById('imageModal').style.display = 'block';
}
function closeImageModal() {
document.getElementById('imageModal').style.display = 'none';
}
// 删除确认功能
function confirmDelete(docId) {
document.getElementById('deleteForm').action = '/delete/' + docId;
document.getElementById('deleteModal').style.display = 'block';
}
function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none';
}
// 点击模态框外部关闭
window.onclick = function(event) {
const imageModal = document.getElementById('imageModal');
const deleteModal = document.getElementById('deleteModal');
if (event.target === imageModal) {
closeImageModal();
}
if (event.target === deleteModal) {
closeDeleteModal();
}
}
</script>
{% endblock %}

538
templates/profile.html Normal file
View File

@@ -0,0 +1,538 @@
{% extends "base.html" %}
{% block title %}个人设置{% endblock %}
{% block content %}
<div class="content-header">
<h1>个人设置</h1>
<p>管理您的个人信息和账户设置</p>
</div>
<!-- 用户信息卡片 -->
<div class="card">
<h3 style="margin-bottom: 20px; color: var(--primary); font-size: 18px;">
<i style="margin-right: 8px;">👤</i>
用户信息
</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">用户名:</span>
<span class="info-value">{{ session.username }}</span>
</div>
<div class="info-item">
<span class="info-label">权限级别:</span>
<span class="permission-badge {{ 'admin' if session.permission == 0 else 'user' }}">
{{ '管理员' if session.permission == 0 else '普通用户' }}
</span>
</div>
</div>
</div>
<!-- 修改密码卡片 -->
<div class="card">
<h3 style="margin-bottom: 20px; color: var(--primary); font-size: 18px;">
<i style="margin-right: 8px;">🔒</i>
修改密码
</h3>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'error' }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('change_own_password') }}" class="password-form" id="passwordForm">
<div class="form-group">
<label for="old_password">当前密码:</label>
<input type="password" id="old_password" name="old_password" required class="form-input">
</div>
<div class="form-group">
<label for="new_password">新密码:</label>
<input type="password" id="new_password" name="new_password" required class="form-input" minlength="6">
<small class="form-help">密码长度至少6位</small>
</div>
<div class="form-group">
<label for="confirm_password">确认新密码:</label>
<input type="password" id="confirm_password" name="confirm_password" required class="form-input">
<small class="form-help" id="password-match-msg"></small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="submitBtn">
<i style="margin-right: 5px;">💾</i>
修改密码
</button>
</div>
</form>
</div>
<style>
.content-header {
margin-bottom: 30px;
}
.content-header h1 {
color: var(--primary);
font-size: 28px;
margin-bottom: 8px;
font-weight: 600;
}
.content-header p {
color: #666;
font-size: 16px;
margin: 0;
}
.info-grid {
display: grid;
gap: 15px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: #555;
}
.info-value {
color: #333;
font-weight: 600;
}
.permission-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.permission-badge.admin {
background: var(--accent);
color: white;
}
.permission-badge.user {
background: var(--success);
color: white;
}
.password-form {
max-width: 400px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-input {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: var(--radius);
font-size: 14px;
transition: var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
}
.form-actions {
margin-top: 25px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius);
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
transition: var(--transition);
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--secondary);
transform: translateY(-1px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.alert {
padding: 12px 16px;
border-radius: var(--radius);
margin-bottom: 20px;
font-size: 14px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@media (max-width: 768px) {
.password-form {
max-width: none;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const newPassword = document.getElementById('new_password');
const confirmPassword = document.getElementById('confirm_password');
const submitBtn = document.getElementById('submitBtn');
const matchMsg = document.getElementById('password-match-msg');
function checkPasswordMatch() {
if (confirmPassword.value === '') {
matchMsg.textContent = '';
matchMsg.style.color = '#666';
return;
}
if (newPassword.value === confirmPassword.value) {
matchMsg.textContent = '✓ 密码匹配';
matchMsg.style.color = '#28a745';
submitBtn.disabled = false;
} else {
matchMsg.textContent = '✗ 密码不匹配';
matchMsg.style.color = '#dc3545';
submitBtn.disabled = true;
}
}
newPassword.addEventListener('input', checkPasswordMatch);
confirmPassword.addEventListener('input', checkPasswordMatch);
// 表单验证
document.getElementById('passwordForm').addEventListener('submit', function(e) {
if (newPassword.value !== confirmPassword.value) {
e.preventDefault();
alert('新密码和确认密码不匹配!');
}
if (newPassword.value.length < 6) {
e.preventDefault();
alert('密码长度至少6位');
}
});
});
</script>
{% endblock %}
.permission-admin {
background: #e3f2fd;
color: #1976d2;
}
.permission-user {
background: #f3e5f5;
color: #7b1fa2;
}
.password-form {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 25px;
}
.password-form h3 {
color: #333;
margin-bottom: 20px;
font-size: 18px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: #f8f9fa;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
width: 100%;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
margin-top: 15px;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.flash-messages {
margin-bottom: 20px;
}
.flash-message {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 10px;
font-weight: 500;
}
.flash-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.flash-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.flash-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.navigation-links {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
}
.nav-link {
display: inline-block;
margin: 0 15px;
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.nav-link:hover {
color: #764ba2;
text-decoration: underline;
}
@media (max-width: 600px) {
.profile-container {
padding: 30px 20px;
margin: 10px;
}
.profile-header h1 {
font-size: 24px;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
}
</style>
</head>
<body>
<div class="profile-container">
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="flash-message flash-{{ category }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="profile-header">
<h1>个人设置</h1>
<p>管理您的账户信息和密码</p>
</div>
<!-- 用户信息显示 -->
<div class="user-info">
<h3>账户信息</h3>
<div class="info-item">
<span class="info-label">用户名:</span>
<span class="info-value">{{ session.user_id }}</span>
</div>
<div class="info-item">
<span class="info-label">权限级别:</span>
<span class="permission-badge {{ 'permission-admin' if session.permission == 0 else 'permission-user' }}">
{{ '管理员' if session.permission == 0 else '普通用户' }}
</span>
</div>
</div>
<!-- 修改密码表单 -->
<form method="POST" action="{{ url_for('change_own_password') }}" class="password-form" id="passwordForm">
<h3>修改密码</h3>
<div class="form-group">
<label for="old_password">当前密码:</label>
<input type="password" id="old_password" name="old_password" required>
</div>
<div class="form-group">
<label for="new_password">新密码:</label>
<input type="password" id="new_password" name="new_password" required minlength="6">
</div>
<div class="form-group">
<label for="confirm_password">确认新密码:</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="6">
</div>
<button type="submit" class="btn btn-primary">更新密码</button>
</form>
<!-- 导航链接 -->
<div class="navigation-links">
<a href="{{ url_for('index') }}" class="nav-link">返回首页</a>
<a href="{{ url_for('my_data') }}" class="nav-link">我的数据</a>
{% if session.permission == 0 %}
<a href="{{ url_for('user_management') }}" class="nav-link">用户管理</a>
{% endif %}
</div>
</div>
<script>
// 密码确认验证
document.getElementById('passwordForm').addEventListener('submit', function(e) {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
e.preventDefault();
alert('新密码和确认密码不一致,请重新输入!');
return false;
}
if (newPassword.length < 6) {
e.preventDefault();
alert('新密码长度至少6位');
return false;
}
});
// 输入实时验证
document.getElementById('confirm_password').addEventListener('input', function() {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = this.value;
if (confirmPassword && newPassword !== confirmPassword) {
this.style.borderColor = '#dc3545';
this.style.background = '#fff5f5';
} else {
this.style.borderColor = '#e9ecef';
this.style.background = '#f8f9fa';
}
});
</script>
</body>
</html>

440
templates/register.html Normal file
View File

@@ -0,0 +1,440 @@
{% extends "base.html" %}
{% block title %}注册新用户{% endblock %}
{% block content %}
<div class="register-container">
<div class="register-card">
<div class="register-header">
<h1>注册新用户</h1>
<p>创建新的系统用户账户</p>
</div>
<form method="POST" class="register-form">
<div class="form-group">
<label for="username">
<i class="fas fa-user"></i>
用户名
</label>
<input type="text"
id="username"
name="username"
required
minlength="3"
maxlength="20"
placeholder="请输入用户名3-20个字符">
<small class="form-text">用户名长度为3-20个字符只能包含字母、数字和下划线</small>
</div>
<div class="form-group">
<label for="password">
<i class="fas fa-lock"></i>
密码
</label>
<input type="password"
id="password"
name="password"
required
minlength="6"
placeholder="请输入密码至少6位">
<small class="form-text">密码长度至少6位建议包含字母和数字</small>
</div>
<div class="form-group">
<label for="confirm_password">
<i class="fas fa-lock"></i>
确认密码
</label>
<input type="password"
id="confirm_password"
name="confirm_password"
required
minlength="6"
placeholder="请再次输入密码">
<small class="form-text">请再次输入相同的密码进行确认</small>
</div>
<div class="form-group">
<label for="permission">
<i class="fas fa-user-cog"></i>
权限级别
</label>
<select id="permission" name="permission" required>
<option value="">请选择权限级别</option>
<option value="1">普通用户 - 可以录入和查询数据</option>
<option value="0">管理员 - 拥有所有权限</option>
</select>
<small class="form-text">
<strong>普通用户:</strong>可以上传图片、录入数据、查询数据<br>
<strong>管理员:</strong>拥有所有权限,包括用户管理和数据管理
</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus"></i>
创建用户
</button>
<a href="{{ url_for('user_management') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
返回用户管理
</a>
</div>
</form>
</div>
</div>
<style>
.register-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 200px);
padding: 20px;
}
.register-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 40px;
width: 100%;
max-width: 500px;
}
.register-header {
text-align: center;
margin-bottom: 30px;
}
.register-header h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.register-header p {
color: #666;
font-size: 16px;
margin: 0;
}
.register-form {
width: 100%;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.form-group label i {
margin-right: 8px;
color: #007bff;
width: 16px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
}
.form-group input:invalid {
border-color: #dc3545;
}
.form-group input:valid {
border-color: #28a745;
}
.form-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
flex: 1;
}
.btn i {
margin-right: 8px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(108,117,125,0.3);
}
/* 响应式设计 */
@media (max-width: 768px) {
.register-container {
padding: 10px;
}
.register-card {
padding: 30px 20px;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}
/* 密码强度指示器 */
.password-strength {
margin-top: 5px;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
}
.password-strength-bar {
height: 100%;
width: 0%;
transition: all 0.3s ease;
}
.strength-weak {
background-color: #dc3545;
width: 33%;
}
.strength-medium {
background-color: #ffc107;
width: 66%;
}
.strength-strong {
background-color: #28a745;
width: 100%;
}
/* 表单验证样式 */
.form-group.error input,
.form-group.error select {
border-color: #dc3545;
box-shadow: 0 0 0 3px rgba(220,53,69,0.1);
}
.form-group.success input,
.form-group.success select {
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40,167,69,0.1);
}
.error-message {
color: #dc3545;
font-size: 12px;
margin-top: 5px;
display: none;
}
.success-message {
color: #28a745;
font-size: 12px;
margin-top: 5px;
display: none;
}
</style>
<script>
// 表单验证
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('.register-form');
const username = document.getElementById('username');
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm_password');
const permission = document.getElementById('permission');
// 用户名验证
username.addEventListener('input', function() {
const value = this.value;
const formGroup = this.closest('.form-group');
if (value.length < 3) {
setFieldError(formGroup, '用户名至少需要3个字符');
} else if (value.length > 20) {
setFieldError(formGroup, '用户名不能超过20个字符');
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
setFieldError(formGroup, '用户名只能包含字母、数字和下划线');
} else {
setFieldSuccess(formGroup, '用户名格式正确');
}
});
// 密码验证
password.addEventListener('input', function() {
const value = this.value;
const formGroup = this.closest('.form-group');
if (value.length < 6) {
setFieldError(formGroup, '密码至少需要6个字符');
} else {
setFieldSuccess(formGroup, '密码长度符合要求');
}
// 检查确认密码
if (confirmPassword.value) {
validateConfirmPassword();
}
});
// 确认密码验证
confirmPassword.addEventListener('input', validateConfirmPassword);
function validateConfirmPassword() {
const formGroup = confirmPassword.closest('.form-group');
if (confirmPassword.value !== password.value) {
setFieldError(formGroup, '两次输入的密码不一致');
} else if (confirmPassword.value.length >= 6) {
setFieldSuccess(formGroup, '密码确认正确');
}
}
// 权限选择验证
permission.addEventListener('change', function() {
const formGroup = this.closest('.form-group');
if (this.value === '') {
setFieldError(formGroup, '请选择权限级别');
} else {
setFieldSuccess(formGroup, '权限级别已选择');
}
});
// 表单提交验证
form.addEventListener('submit', function(e) {
let isValid = true;
// 验证用户名
if (username.value.length < 3 || username.value.length > 20 || !/^[a-zA-Z0-9_]+$/.test(username.value)) {
isValid = false;
setFieldError(username.closest('.form-group'), '用户名格式不正确');
}
// 验证密码
if (password.value.length < 6) {
isValid = false;
setFieldError(password.closest('.form-group'), '密码长度至少6位');
}
// 验证确认密码
if (password.value !== confirmPassword.value) {
isValid = false;
setFieldError(confirmPassword.closest('.form-group'), '两次输入的密码不一致');
}
// 验证权限选择
if (permission.value === '') {
isValid = false;
setFieldError(permission.closest('.form-group'), '请选择权限级别');
}
if (!isValid) {
e.preventDefault();
}
});
function setFieldError(formGroup, message) {
formGroup.classList.remove('success');
formGroup.classList.add('error');
let errorMsg = formGroup.querySelector('.error-message');
if (!errorMsg) {
errorMsg = document.createElement('div');
errorMsg.className = 'error-message';
formGroup.appendChild(errorMsg);
}
errorMsg.textContent = message;
errorMsg.style.display = 'block';
const successMsg = formGroup.querySelector('.success-message');
if (successMsg) {
successMsg.style.display = 'none';
}
}
function setFieldSuccess(formGroup, message) {
formGroup.classList.remove('error');
formGroup.classList.add('success');
let successMsg = formGroup.querySelector('.success-message');
if (!successMsg) {
successMsg = document.createElement('div');
successMsg.className = 'success-message';
formGroup.appendChild(successMsg);
}
successMsg.textContent = message;
successMsg.style.display = 'block';
const errorMsg = formGroup.querySelector('.error-message');
if (errorMsg) {
errorMsg.style.display = 'none';
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,356 @@
{% extends "base.html" %}
{% block title %}用户管理{% endblock %}
{% block content %}
<div class="container">
<div class="header-section">
<h1>用户管理</h1>
<a href="{{ url_for('register') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i> 注册新用户
</a>
</div>
<div class="users-table">
<table class="table">
<thead>
<tr>
<th>用户ID</th>
<th>用户名</th>
<th>权限级别</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.user_id }}</td>
<td>{{ user.username }}</td>
<td>
<span class="permission-badge {% if user.premission == 0 %}admin{% else %}user{% endif %}">
{% if user.premission == 0 %}管理员{% else %}普通用户{% endif %}
</span>
</td>
<td class="actions">
{% if user.username != 'admin' %}
<!-- 修改密码按钮 -->
<button class="btn btn-sm btn-warning" onclick="showPasswordModal('{{ user.username }}')">
<i class="fas fa-key"></i> 修改密码
</button>
<!-- 修改权限按钮 -->
<button class="btn btn-sm btn-info" onclick="showPermissionModal('{{ user.username }}', {{ user.premission }})">
<i class="fas fa-user-cog"></i> 修改权限
</button>
<!-- 删除用户按钮 -->
<button class="btn btn-sm btn-danger" onclick="confirmDelete('{{ user.username }}')">
<i class="fas fa-trash"></i> 删除
</button>
{% else %}
<span class="text-muted">系统管理员</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 修改密码模态框 -->
<div id="passwordModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('passwordModal')">&times;</span>
<h2>修改用户密码</h2>
<form id="passwordForm" method="POST">
<div class="form-group">
<label for="new_password">新密码:</label>
<input type="password" id="new_password" name="new_password" required minlength="6">
</div>
<div class="form-group">
<label for="confirm_password">确认密码:</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="6">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">确认修改</button>
<button type="button" class="btn btn-secondary" onclick="closeModal('passwordModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 修改权限模态框 -->
<div id="permissionModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('permissionModal')">&times;</span>
<h2>修改用户权限</h2>
<form id="permissionForm" method="POST">
<div class="form-group">
<label for="permission">权限级别:</label>
<select id="permission" name="permission" required>
<option value="0">管理员</option>
<option value="1">普通用户</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">确认修改</button>
<button type="button" class="btn btn-secondary" onclick="closeModal('permissionModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 删除确认模态框 -->
<div id="deleteModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('deleteModal')">&times;</span>
<h2>确认删除</h2>
<p>确定要删除用户 <strong id="deleteUsername"></strong> 吗?此操作不可撤销。</p>
<div class="form-actions">
<form id="deleteForm" method="POST">
<button type="submit" class="btn btn-danger">确认删除</button>
<button type="button" class="btn btn-secondary" onclick="closeModal('deleteModal')">取消</button>
</form>
</div>
</div>
</div>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header-section h1 {
color: #333;
margin: 0;
}
.users-table {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
.permission-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.permission-badge.admin {
background-color: #dc3545;
color: white;
}
.permission-badge.user {
background-color: #28a745;
color: white;
}
.actions {
white-space: nowrap;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
margin-right: 5px;
font-size: 14px;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.btn:hover {
opacity: 0.8;
transform: translateY(-1px);
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 500px;
position: relative;
}
.close {
position: absolute;
right: 15px;
top: 15px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #aaa;
}
.close:hover {
color: #000;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.text-muted {
color: #6c757d;
font-style: italic;
}
</style>
<script>
function showPasswordModal(username) {
document.getElementById('passwordForm').action = `/change_password/${username}`;
document.getElementById('passwordModal').style.display = 'block';
}
function showPermissionModal(username, currentPermission) {
document.getElementById('permissionForm').action = `/change_permission/${username}`;
document.getElementById('permission').value = currentPermission;
document.getElementById('permissionModal').style.display = 'block';
}
function confirmDelete(username) {
document.getElementById('deleteUsername').textContent = username;
document.getElementById('deleteForm').action = `/delete_user/${username}`;
document.getElementById('deleteModal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
// 清空表单
const forms = document.querySelectorAll(`#${modalId} form`);
forms.forEach(form => form.reset());
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
}
// 密码确认验证
document.getElementById('passwordForm').addEventListener('submit', function(e) {
const password = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (password !== confirmPassword) {
e.preventDefault();
alert('两次输入的密码不一致!');
return false;
}
if (password.length < 6) {
e.preventDefault();
alert('密码长度至少6位');
return false;
}
});
</script>
{% endblock %}