初始提交
141
Untitled-1.py
Normal file
@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
检查所有相关文件中的权重默认值是否已从5改为100
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
# 初始化colorama
|
||||
init()
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('check_weights')
|
||||
|
||||
# 需要检查的文件和目录
|
||||
CHECK_PATHS = [
|
||||
'templates/admin/characters.html',
|
||||
'templates/weight_management.html',
|
||||
'static/js/admin/characters.js',
|
||||
'static/js/game.js',
|
||||
'app_sqlite.py',
|
||||
]
|
||||
|
||||
# 权重相关的正则表达式模式
|
||||
WEIGHT_PATTERNS = [
|
||||
# HTML中的默认值
|
||||
(r'id="character-weight"[^>]*value="(\d+)"', 'HTML表单默认值'),
|
||||
(r'min="1" max="(\d+)" value="(\d+)"', 'HTML数字输入范围'),
|
||||
|
||||
# JavaScript中的默认值引用
|
||||
(r'character\.weight \|\| (\d+)', 'JS默认权重引用'),
|
||||
(r'weight: parseInt\([^)]+\) \|\| (\d+)', 'JS解析默认权重'),
|
||||
|
||||
# Python中的默认值
|
||||
(r"character\['weight'\] = (\d+)", 'Python默认权重赋值'),
|
||||
|
||||
# 描述文本中的范围
|
||||
(r'权重 \(1-(\d+)\)', '权重范围描述'),
|
||||
(r'默认值为(\d+)', '默认值描述'),
|
||||
|
||||
# 范围验证
|
||||
(r'weight < 1 \|\| weight > (\d+)', 'JS权重验证'),
|
||||
]
|
||||
|
||||
def check_file(file_path):
|
||||
"""检查单个文件中的权重默认值"""
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"文件不存在: {file_path}")
|
||||
return [], []
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
correct_matches = []
|
||||
incorrect_matches = []
|
||||
|
||||
# 检查所有权重模式
|
||||
for pattern, description in WEIGHT_PATTERNS:
|
||||
matches = re.finditer(pattern, content)
|
||||
|
||||
for match in matches:
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
line_text = content.splitlines()[line_num-1]
|
||||
|
||||
# 根据不同的模式检查不同的组
|
||||
if 'max' in pattern and 'value' in pattern:
|
||||
# 处理同时有max和value的情况
|
||||
max_val = match.group(1)
|
||||
value = match.group(2)
|
||||
|
||||
if max_val == '1000' and value == '100':
|
||||
correct_matches.append((file_path, line_num, description, line_text))
|
||||
else:
|
||||
incorrect_matches.append((file_path, line_num, description, line_text))
|
||||
else:
|
||||
# 普通情况,检查第一个捕获组
|
||||
weight_value = match.group(1)
|
||||
|
||||
if description == '权重范围描述' and weight_value == '1000':
|
||||
correct_matches.append((file_path, line_num, description, line_text))
|
||||
elif weight_value == '100':
|
||||
correct_matches.append((file_path, line_num, description, line_text))
|
||||
else:
|
||||
incorrect_matches.append((file_path, line_num, description, line_text))
|
||||
|
||||
return correct_matches, incorrect_matches
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
logger.info("开始检查权重默认值")
|
||||
|
||||
all_correct_matches = []
|
||||
all_incorrect_matches = []
|
||||
|
||||
# 查找所有匹配的文件
|
||||
for path in CHECK_PATHS:
|
||||
if os.path.isdir(path):
|
||||
# 处理目录
|
||||
for root, _, files in os.walk(path):
|
||||
for file in files:
|
||||
if file.endswith(('.html', '.js', '.py', '.css')):
|
||||
file_path = os.path.join(root, file)
|
||||
correct, incorrect = check_file(file_path)
|
||||
all_correct_matches.extend(correct)
|
||||
all_incorrect_matches.extend(incorrect)
|
||||
else:
|
||||
# 处理单个文件
|
||||
correct, incorrect = check_file(path)
|
||||
all_correct_matches.extend(correct)
|
||||
all_incorrect_matches.extend(incorrect)
|
||||
|
||||
# 输出结果
|
||||
print("\n" + "="*80)
|
||||
print(f"{Fore.GREEN}正确的权重默认值 (100 或 1000): {len(all_correct_matches)}{Style.RESET_ALL}")
|
||||
for file_path, line_num, desc, line_text in all_correct_matches:
|
||||
print(f"{Fore.GREEN}✓ {file_path}:{line_num} - {desc}{Style.RESET_ALL}")
|
||||
print(f" {line_text.strip()}")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print(f"{Fore.RED}不正确的权重默认值 (非 100 或 1000): {len(all_incorrect_matches)}{Style.RESET_ALL}")
|
||||
for file_path, line_num, desc, line_text in all_incorrect_matches:
|
||||
print(f"{Fore.RED}✗ {file_path}:{line_num} - {desc}{Style.RESET_ALL}")
|
||||
print(f" {line_text.strip()}")
|
||||
|
||||
# 总结
|
||||
print("\n" + "="*80)
|
||||
if len(all_incorrect_matches) == 0:
|
||||
print(f"{Fore.GREEN}所有权重相关默认值检查通过!{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}发现 {len(all_incorrect_matches)} 个需要修改的地方。{Style.RESET_ALL}")
|
||||
|
||||
return len(all_incorrect_matches) == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
__pycache__/app_sqlite.cpython-312.pyc
Normal file
BIN
__pycache__/db_adapter.cpython-312.pyc
Normal file
77
add_weights_script.py
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
给所有角色添加默认权重100的脚本
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('add_weights')
|
||||
|
||||
# 卡牌文件路径 - 根据实际情况修改
|
||||
CARDS_FILE = 'data/cards.json'
|
||||
|
||||
def add_default_weights():
|
||||
"""给所有角色添加默认权重100"""
|
||||
try:
|
||||
# 确保文件存在
|
||||
if not os.path.exists(CARDS_FILE):
|
||||
logger.error(f"文件不存在: {CARDS_FILE}")
|
||||
return False
|
||||
|
||||
# 读取卡牌数据
|
||||
with open(CARDS_FILE, 'r', encoding='utf-8') as f:
|
||||
cards_data = json.load(f)
|
||||
|
||||
characters = cards_data.get('characters', [])
|
||||
logger.info(f"读取到 {len(characters)} 个角色")
|
||||
|
||||
# 记录修改数量
|
||||
updated_count = 0
|
||||
|
||||
# 为每个角色添加默认权重
|
||||
for character in characters:
|
||||
if 'weight' not in character:
|
||||
character['weight'] = 100
|
||||
updated_count += 1
|
||||
|
||||
logger.info(f"已为 {updated_count} 个角色添加默认权重100")
|
||||
|
||||
# 如果有修改,保存回文件
|
||||
if updated_count > 0:
|
||||
# 先创建备份
|
||||
backup_file = f"{CARDS_FILE}.bak"
|
||||
with open(backup_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(cards_data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"已创建备份文件: {backup_file}")
|
||||
|
||||
# 保存修改后的文件
|
||||
with open(CARDS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(cards_data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"已保存修改到文件: {CARDS_FILE}")
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.info("没有角色需要添加权重,所有角色都已有权重设置")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"添加默认权重失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
logger.info("开始运行添加默认权重脚本")
|
||||
result = add_default_weights()
|
||||
if result:
|
||||
logger.info("脚本执行成功")
|
||||
else:
|
||||
logger.warning("脚本执行完成,但未进行修改或出现错误")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1816
app_sqlite.py
Normal file
176
data/achievements.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"achievements": [
|
||||
{
|
||||
"id": "death_loyalty_low_virus_bomb",
|
||||
"name": "帝皇裁决",
|
||||
"description": "您的星球被帝国执行了灭绝令。忠诚度不足导致您被视为异端。",
|
||||
"icon": "☣️",
|
||||
"death_scenario_id": "loyalty_low_virus_bomb",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_loyalty_high_chicken",
|
||||
"name": "鸡贼之灾",
|
||||
"description": "忠诚度过高,只有一种可能。",
|
||||
"icon": "🐔",
|
||||
"death_scenario_id": "loyalty_high_chicken",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_chaos_low_tyranid",
|
||||
"name": "生物资源",
|
||||
"description": "混沌侵染过低,您的星球成为了泰伦虫族的完美食物。",
|
||||
"icon": "🍰",
|
||||
"death_scenario_id": "chaos_low_tyranid",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_chaos_high_khorne",
|
||||
"name": "血与骷髅",
|
||||
"description": "混沌侵染过高,您的星球被恐虐军团屠戮。",
|
||||
"icon": "💀",
|
||||
"death_scenario_id": "chaos_high_khorne",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_population_low_revolt",
|
||||
"name": "人民之怒",
|
||||
"description": "民众控制过低,您被起义军推翻。",
|
||||
"icon": "⚔️",
|
||||
"death_scenario_id": "population_low_revolt",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_population_high_rival",
|
||||
"name": "权力的游戏",
|
||||
"description": "民众控制过高,您被政治对手暗杀。",
|
||||
"icon": "🗡️",
|
||||
"death_scenario_id": "population_high_rival",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_military_low_orks",
|
||||
"name": "绿皮入侵",
|
||||
"description": "军事力量过低,您的星球被兽人侵略。",
|
||||
"icon": "👹",
|
||||
"death_scenario_id": "military_low_orks",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_military_high_coup",
|
||||
"name": "一言不合",
|
||||
"description": "军事力量过高,您的军队发动了政变。",
|
||||
"icon": "👑",
|
||||
"death_scenario_id": "military_high_coup",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_resources_low_chaos",
|
||||
"name": "饥饿的混沌",
|
||||
"description": "资源储备过低,饥饿的民众转向了混沌。",
|
||||
"icon": "🦑",
|
||||
"death_scenario_id": "resources_low_chaos",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "death_resources_high_tyranid",
|
||||
"name": "虫族的大餐",
|
||||
"description": "资源储备过高,您吸引了泰伦虫族的注意。",
|
||||
"icon": "🐲",
|
||||
"death_scenario_id": "resources_high_tyranid",
|
||||
"hidden": 0
|
||||
},
|
||||
{
|
||||
"id": "reign_single_5",
|
||||
"name": "初露锋芒",
|
||||
"description": "单次统治达到5年,帝国开始记住您的名字。",
|
||||
"icon": "🌱",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_single",
|
||||
"requirement": 5
|
||||
},
|
||||
{
|
||||
"id": "reign_single_10",
|
||||
"name": "初级执政官",
|
||||
"description": "单次统治达到10年,您已经开始学会如何治理一颗行星。",
|
||||
"icon": "🌿",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_single",
|
||||
"requirement": 10
|
||||
},
|
||||
{
|
||||
"id": "reign_single_50",
|
||||
"name": "熟练统治者",
|
||||
"description": "单次统治达到50年,您的统治已成为当地传说。",
|
||||
"icon": "🌲",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_single",
|
||||
"requirement": 50
|
||||
},
|
||||
{
|
||||
"id": "reign_single_100",
|
||||
"name": "百年老人",
|
||||
"description": "单次统治达到100年,您已经成为帝国历史上最长寿的总督之一。",
|
||||
"icon": "👑",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_single",
|
||||
"requirement": 100
|
||||
},
|
||||
{
|
||||
"id": "reign_single_200",
|
||||
"name": "始皇帝",
|
||||
"description": "单次统治达到200年,不朽的荣光永远铭刻在帝国史册。",
|
||||
"icon": "⚜️",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_single",
|
||||
"requirement": 200
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "reign_total_50",
|
||||
"name": "见习总督",
|
||||
"description": "累计统治50年,您已崭露头角。",
|
||||
"icon": "📚",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_total",
|
||||
"requirement": 50
|
||||
},
|
||||
{
|
||||
"id": "reign_total_100",
|
||||
"name": "资深总督",
|
||||
"description": "累计统治100年,您的统治经验得到帝国的认可。",
|
||||
"icon": "📜",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_total",
|
||||
"requirement": 100
|
||||
},
|
||||
{
|
||||
"id": "reign_total_500",
|
||||
"name": "传奇总督",
|
||||
"description": "累计统治500年,您的名字将被铭记。",
|
||||
"icon": "🏆",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_total",
|
||||
"requirement": 500
|
||||
},
|
||||
{
|
||||
"id": "reign_total_1000",
|
||||
"name": "千年老店",
|
||||
"description": "累计统治1000年,成为星区传说。",
|
||||
"icon": "🌟",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_total",
|
||||
"requirement": 1000
|
||||
},
|
||||
{
|
||||
"id": "reign_total_114514",
|
||||
"name": "永恒总督",
|
||||
"description": "累计统治114514年,这已经超越了凡人的理解。",
|
||||
"icon": "🔮",
|
||||
"hidden": 0,
|
||||
"achievement_type": "reign_total",
|
||||
"requirement": 114514
|
||||
}
|
||||
]
|
||||
}
|
||||
17396
data/backup/card_votes.json.20250617_17.ed6d7d5f.bak
Normal file
17402
data/backup/card_votes.json.20250617_18.ed6d7d5f.bak
Normal file
17481
data/backup/card_votes.json.20250617_19.ed6d7d5f.bak
Normal file
17489
data/backup/card_votes.json.20250617_20.ed6d7d5f.bak
Normal file
17497
data/backup/card_votes.json.20250617_23.ed6d7d5f.bak
Normal file
17501
data/backup/card_votes.json.20250618_14.ed6d7d5f.bak
Normal file
17505
data/backup/card_votes.json.20250618_16.ed6d7d5f.bak
Normal file
17509
data/backup/card_votes.json.20250618_18.ed6d7d5f.bak
Normal file
17511
data/backup/card_votes.json.20250618_22.ed6d7d5f.bak
Normal file
17515
data/backup/card_votes.json.20250619_02.ed6d7d5f.bak
Normal file
1726
data/backup/cards.json.20250506_17.7a670aff.bak
Normal file
1862
data/backup/cards.json.20250507_14.7a670aff.bak
Normal file
1953
data/backup/cards.json.20250507_15.7a670aff.bak
Normal file
2075
data/backup/cards.json.20250510_19.677b1890.bak
Normal file
2200
data/backup/cards.json.20250518_12.677b1890.bak
Normal file
2235
data/backup/cards.json.20250518_13.677b1890.bak
Normal file
18946
data/backup/custom_cards.json.20250614_00.85999cb1.bak
Normal file
19012
data/backup/custom_cards.json.20250614_14.85999cb1.bak
Normal file
19045
data/backup/custom_cards.json.20250614_17.85999cb1.bak
Normal file
19078
data/backup/custom_cards.json.20250614_19.85999cb1.bak
Normal file
19111
data/backup/custom_cards.json.20250615_01.85999cb1.bak
Normal file
19144
data/backup/custom_cards.json.20250615_11.85999cb1.bak
Normal file
19177
data/backup/custom_cards.json.20250615_17.85999cb1.bak
Normal file
19210
data/backup/custom_cards.json.20250616_15.85999cb1.bak
Normal file
19276
data/backup/custom_cards.json.20250617_02.85999cb1.bak
Normal file
19309
data/backup/custom_cards.json.20250617_19.85999cb1.bak
Normal file
780166
data/backup/game_states.json.20250618_21.30c9da13.bak
Normal file
780331
data/backup/game_states.json.20250618_22.30c9da13.bak
Normal file
780466
data/backup/game_states.json.20250618_23.30c9da13.bak
Normal file
780500
data/backup/game_states.json.20250619_00.30c9da13.bak
Normal file
780504
data/backup/game_states.json.20250619_02.30c9da13.bak
Normal file
780504
data/backup/game_states.json.20250619_03.30c9da13.bak
Normal file
780487
data/backup/game_states.json.20250619_04.30c9da13.bak
Normal file
780480
data/backup/game_states.json.20250619_05.30c9da13.bak
Normal file
780480
data/backup/game_states.json.20250619_07.30c9da13.bak
Normal file
780484
data/backup/game_states.json.20250619_08.30c9da13.bak
Normal file
12
data/backup/sessions.json.20250618_21.ea5aa18c.bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"active_sessions": [
|
||||
{
|
||||
"user_id": "a37c5dcd-eb43-4d95-849e-a010ffc89348",
|
||||
"login_time": "2025-06-18T12:20:22.096735",
|
||||
"last_activity": "2025-06-18T12:59:40.831810",
|
||||
"ip_address": "127.0.0.1",
|
||||
"status": "idle",
|
||||
"game_start_time": "2025-06-18T12:54:56.050882"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
data/backup/sessions.json.20250618_22.ea5aa18c.bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"active_sessions": [
|
||||
{
|
||||
"user_id": "10de216c-a8b9-4634-99d2-6b86a18eb86e",
|
||||
"login_time": "2025-06-18T13:55:23.062172",
|
||||
"last_activity": "2025-06-18T13:59:51.668179",
|
||||
"ip_address": "127.0.0.1",
|
||||
"status": "playing",
|
||||
"game_start_time": "2025-06-18T13:55:50.142290"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
data/backup/sessions.json.20250618_23.ea5aa18c.bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"active_sessions": [
|
||||
{
|
||||
"user_id": "a7cf3c3e-5a14-4158-b87d-633e9795dd0d",
|
||||
"login_time": "2025-06-18T14:54:12.853911",
|
||||
"last_activity": "2025-06-18T14:56:32.481719",
|
||||
"ip_address": "127.0.0.1",
|
||||
"status": "idle",
|
||||
"game_start_time": "2025-06-18T14:54:21.287141"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
data/backup/sessions.json.20250619_00.ea5aa18c.bak
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
3
data/backup/sessions.json.20250619_02.ea5aa18c.bak
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
12
data/backup/sessions.json.20250619_03.ea5aa18c.bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"active_sessions": [
|
||||
{
|
||||
"user_id": "3ba996ca-a6b0-403f-9c54-5ee80deea7f3",
|
||||
"login_time": "2025-06-18T18:52:57.798773",
|
||||
"last_activity": "2025-06-18T18:54:08.613434",
|
||||
"ip_address": "127.0.0.1",
|
||||
"status": "playing",
|
||||
"game_start_time": "2025-06-18T18:55:03.595785"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
data/backup/sessions.json.20250619_04.ea5aa18c.bak
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
12
data/backup/sessions.json.20250619_05.ea5aa18c.bak
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"active_sessions": [
|
||||
{
|
||||
"user_id": "3ba996ca-a6b0-403f-9c54-5ee80deea7f3",
|
||||
"login_time": "2025-06-18T20:57:36.598863",
|
||||
"last_activity": "2025-06-18T20:59:27.939257",
|
||||
"ip_address": "127.0.0.1",
|
||||
"status": "idle",
|
||||
"game_start_time": "2025-06-18T20:57:39.507870"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
data/backup/sessions.json.20250619_07.ea5aa18c.bak
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
3
data/backup/sessions.json.20250619_08.ea5aa18c.bak
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
220865
data/backup/users.json.20250618_21.4eaebcac.bak
Normal file
220898
data/backup/users.json.20250618_22.4eaebcac.bak
Normal file
220956
data/backup/users.json.20250618_23.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_00.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_02.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_03.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_04.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_05.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_07.4eaebcac.bak
Normal file
220976
data/backup/users.json.20250619_08.4eaebcac.bak
Normal file
17517
data/card_votes.json
Normal file
2200
data/cards.json
Normal file
2225
data/cards.json.bak
Normal file
19342
data/custom_cards.json
Normal file
324
data/death_methods.json
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"death_scenarios": [
|
||||
{
|
||||
"id": "loyalty_low_virus_bomb",
|
||||
"death_type": "loyalty_low",
|
||||
"name": "病毒炸弹",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "审判官",
|
||||
"title": "帝国异端审判庭",
|
||||
"avatar": "⚖️",
|
||||
"avatar_path": "deaths/inquisitor.png"
|
||||
},
|
||||
"text": "总督大人!帝国审判庭已将您认定为异端!",
|
||||
"options": [
|
||||
{"text": "什么?"},
|
||||
{"text": "什么?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "病毒炸弹",
|
||||
"title": "帝国裁决",
|
||||
"avatar": "☣️",
|
||||
"avatar_path": "deaths/virus_bomb.png"
|
||||
},
|
||||
"text": "您的星球被帝国执行了灭绝令。亚空间中能听到的,只有您行星的哀嚎。",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "loyalty_high_chicken",
|
||||
"death_type": "loyalty_high",
|
||||
"name": "鸡贼当道",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "异端审查官",
|
||||
"title": "基因鉴定官",
|
||||
"avatar": "🧬",
|
||||
"avatar_path": "deaths/gene_inquisitor.png"
|
||||
},
|
||||
"text": "总督大人,这次的基因核查除了问题。",
|
||||
"options": [
|
||||
{"text": "这不可能!"},
|
||||
{"text": "这不可能!"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "鸡贼当道",
|
||||
"title": "泰伦虫族的晚餐",
|
||||
"avatar": "🐔",
|
||||
"avatar_path": "deaths/tyranid_dinner.png"
|
||||
},
|
||||
"text": "当你的人民极其忠诚,政府运转高效,只有一种可能,全是鸡贼,哦,泰伦虫族的生物舰已经来了",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chaos_low_tyranid",
|
||||
"death_type": "chaos_low",
|
||||
"name": "虫族甜点",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "帝国海军将军",
|
||||
"title": "战区指挥官",
|
||||
"avatar": "🎖️",
|
||||
"avatar_path": "deaths/navy_general.png"
|
||||
},
|
||||
"text": "总督!天空中出现了大量不明飞行物!",
|
||||
"options": [
|
||||
{"text": "帝皇保佑我们!"},
|
||||
{"text": "帝皇保佑我们!"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "虫族甜点",
|
||||
"title": "泰伦虫族的完美猎物",
|
||||
"avatar": "🍰",
|
||||
"avatar_path": "deaths/tyranid_dessert.png"
|
||||
},
|
||||
"text": "您的星球被虫族视为完美的食物来源。没有混沌的气息让您成为了最理想的猎物。",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chaos_high_khorne",
|
||||
"death_type": "chaos_high",
|
||||
"name": "恐虐魔神",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "PDF军官",
|
||||
"title": "行星防卫军指挥官",
|
||||
"avatar": "👮",
|
||||
"avatar_path": "deaths/pdf_officer.png"
|
||||
},
|
||||
"text": "总督!我看见了,我听见了!亚空间的声音!它...它们在召唤我们!",
|
||||
"options": [
|
||||
{"text": "什么?"},
|
||||
{"text": "什么?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "恐虐魔神",
|
||||
"title": "混沌四神之一",
|
||||
"avatar": "💀",
|
||||
"avatar_path": "deaths/khorne.png"
|
||||
},
|
||||
"text": "您的星球被8只恐虐军团屠戮。您的头颅成为了恐虐祭坛上的新装饰品。",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "population_low_revolt",
|
||||
"death_type": "population_low",
|
||||
"name": "叛军领袖",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "警备军官",
|
||||
"title": "城防指挥官",
|
||||
"avatar": "🛡️",
|
||||
"avatar_path": "deaths/guard_officer.png"
|
||||
},
|
||||
"text": "总督大人!民众暴动已经扩散到整个行星!我们控制不住了!",
|
||||
"options": [
|
||||
{"text": "派出所有军队!"},
|
||||
{"text": "派出所有军队!"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "叛军领袖",
|
||||
"title": "人民之声",
|
||||
"avatar": "⚔️",
|
||||
"avatar_path": "deaths/rebel_leader.png"
|
||||
},
|
||||
"text": "民众的怒火最终吞噬了您。这颗行星上的新篇章将不再有您的名字。新任总督不合法?只要能补上仕一税,帝国不在乎谁是总督。",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "population_high_rival",
|
||||
"death_type": "population_high",
|
||||
"name": "政敌暗杀",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "政敌",
|
||||
"title": "贵族议会领袖",
|
||||
"avatar": "🎭",
|
||||
"avatar_path": "deaths/political_rival.png"
|
||||
},
|
||||
"text": "总督,您的独裁统治已经让其他贵族感到不安。我们认为您的权力过大了。",
|
||||
"options": [
|
||||
{"text": "你算什么东西?"},
|
||||
{"text": "你算什么东西?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "政敌",
|
||||
"title": "新任总督",
|
||||
"avatar": "🗡️",
|
||||
"avatar_path": "deaths/assassin.png"
|
||||
},
|
||||
"text": "您被政治对手暗中除掉,尽管民众爱戴您,但权力的游戏从不怜悯任何人。",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "military_low_orks",
|
||||
"death_type": "military_low",
|
||||
"name": "兽人老大",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "海军将领",
|
||||
"title": "轨道防御指挥官",
|
||||
"avatar": "🔭",
|
||||
"avatar_path": "deaths/navy_commander.png"
|
||||
},
|
||||
"text": "总督大人!天上那是什么?",
|
||||
"options": [
|
||||
{"text": "月亮?"},
|
||||
{"text": "月亮?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "兽人老大",
|
||||
"title": "绿皮入侵者",
|
||||
"avatar": "👹",
|
||||
"avatar_path": "deaths/ork_warboss.png"
|
||||
},
|
||||
"text": "兽人的战斗月亮轻而易举地摧毁了您的军队,他们评价道:这真是一场无趣的Waghhh",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "military_high_coup",
|
||||
"death_type": "military_high",
|
||||
"name": "军事政变",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "PDF将军",
|
||||
"title": "行星防卫军最高指挥官",
|
||||
"avatar": "👨✈️",
|
||||
"avatar_path": "deaths/pdf_general.png"
|
||||
},
|
||||
"text": "总督,我觉得您不太适合这个位置了,我有一各推荐人选。",
|
||||
"options": [
|
||||
{"text": "谁?"},
|
||||
{"text": "谁?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "PDF将军",
|
||||
"title": "军政府领袖",
|
||||
"avatar": "👑",
|
||||
"avatar_path": "deaths/military_coup.png"
|
||||
},
|
||||
"text": "我",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "resources_low_chaos",
|
||||
"death_type": "resources_low",
|
||||
"name": "饥荒动乱",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "法务部法警",
|
||||
"title": "治安官",
|
||||
"avatar": "👮",
|
||||
"avatar_path": "deaths/arbites.png"
|
||||
},
|
||||
"text": "总督大人!暴民们缺衣少食,正在冲进上巢的街区!",
|
||||
"options": [
|
||||
{"text": "什么?"},
|
||||
{"text": "什么?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "被混沌侵染,浑身触手起义军",
|
||||
"title": "饥饿的暴民首领",
|
||||
"avatar": "🦑",
|
||||
"avatar_path": "deaths/chaos_rioters.png"
|
||||
},
|
||||
"text": "饭都吃不饱人家凭什么跟帝国混?",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "resources_high_tyranid",
|
||||
"death_type": "resources_high",
|
||||
"name": "虫巢暴君",
|
||||
"weight": 100,
|
||||
"first_card": {
|
||||
"character": {
|
||||
"name": "帝国海军将军",
|
||||
"title": "舰队指挥官",
|
||||
"avatar": "🚀",
|
||||
"avatar_path": "deaths/fleet_commander.png"
|
||||
},
|
||||
"text": "总督,星系已经被泰伦虫族包围了!",
|
||||
"options": [
|
||||
{"text": "什么?"},
|
||||
{"text": "什么?"}
|
||||
]
|
||||
},
|
||||
"second_card": {
|
||||
"character": {
|
||||
"name": "虫巢暴君",
|
||||
"title": "虫族最高生物形态",
|
||||
"avatar": "🐲",
|
||||
"avatar_path": "deaths/hive_tyrant.png"
|
||||
},
|
||||
"text": "好吃!",
|
||||
"options": [
|
||||
{"text": ""},
|
||||
{"text": ""}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
780551
data/game_states.json
Normal file
3
data/sessions.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
220996
data/users.json
Normal file
BIN
data/warhammer.db
Normal file
0
db_adapter.log
Normal file
725
db_adapter.py
Normal file
@ -0,0 +1,725 @@
|
||||
import sqlite3
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import datetime
|
||||
import uuid
|
||||
import random
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
filename='db_adapter.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('db_adapter')
|
||||
|
||||
# 数据库文件路径
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "warhammer.db")
|
||||
|
||||
# 线程本地存储,确保每个线程使用独立的数据库连接
|
||||
_thread_local = threading.local()
|
||||
|
||||
def get_db_connection():
|
||||
"""获取数据库连接(每个线程一个)"""
|
||||
if not hasattr(_thread_local, 'connection'):
|
||||
_thread_local.connection = sqlite3.connect(DB_PATH)
|
||||
# 启用外键约束
|
||||
_thread_local.connection.execute("PRAGMA foreign_keys = ON")
|
||||
# 配置连接返回行为字典格式
|
||||
_thread_local.connection.row_factory = sqlite3.Row
|
||||
|
||||
return _thread_local.connection
|
||||
|
||||
def close_db_connection():
|
||||
"""关闭当前线程的数据库连接"""
|
||||
if hasattr(_thread_local, 'connection'):
|
||||
_thread_local.connection.close()
|
||||
delattr(_thread_local, 'connection')
|
||||
|
||||
# 用户数据操作
|
||||
def get_users():
|
||||
"""获取所有用户数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT * FROM users")
|
||||
users = [dict(row) for row in cursor.fetchall()]
|
||||
return users
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户数据失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def save_users(users):
|
||||
"""保存用户数据(批量更新)"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
for user in users:
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO users (id, username, password, created_at, high_score, total_games, last_year, last_game_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
user['id'],
|
||||
user['username'],
|
||||
user['password'],
|
||||
user.get('created_at', datetime.datetime.now().isoformat()),
|
||||
user.get('high_score', 0),
|
||||
user.get('total_games', 0),
|
||||
user.get('last_year', 41000),
|
||||
user.get('last_game_time', None)
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存用户数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 游戏状态操作
|
||||
def get_game_states():
|
||||
"""获取所有游戏状态数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT * FROM game_states")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
game_states = []
|
||||
for row in rows:
|
||||
state = dict(row)
|
||||
# 将JSON字符串转换为字典
|
||||
state['game_data'] = json.loads(state['game_data'])
|
||||
game_states.append(state)
|
||||
|
||||
return game_states
|
||||
except Exception as e:
|
||||
logger.error(f"获取游戏状态数据失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def save_game_states(states):
|
||||
"""保存游戏状态数据(批量更新)"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
for state in states:
|
||||
# 将游戏数据转换为JSON字符串
|
||||
game_data_json = json.dumps(state['game_data'], ensure_ascii=False)
|
||||
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO game_states (id, user_id, game_data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
state['id'],
|
||||
state['user_id'],
|
||||
game_data_json,
|
||||
state.get('created_at', datetime.datetime.now().isoformat()),
|
||||
state.get('updated_at', datetime.datetime.now().isoformat())
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存游戏状态数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 卡牌数据操作
|
||||
def get_cards():
|
||||
"""获取卡牌基础数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT cards_data FROM game_cards WHERE id = 1")
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return json.loads(row['cards_data'])
|
||||
else:
|
||||
logger.warning("未找到卡牌数据,返回空字典")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"获取卡牌数据失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
# 自定义卡牌操作
|
||||
def get_custom_cards():
|
||||
"""获取自定义卡牌数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT c.*, vs.upvotes, vs.downvotes
|
||||
FROM custom_cards c
|
||||
LEFT JOIN card_vote_stats vs ON c.id = vs.card_id
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
cards = []
|
||||
for row in rows:
|
||||
card = {
|
||||
'id': row['id'],
|
||||
'title': row['title'],
|
||||
'character': json.loads(row['character_data']),
|
||||
'description': row['description'],
|
||||
'option_a': json.loads(row['option_a']),
|
||||
'option_b': json.loads(row['option_b']),
|
||||
'creator_id': row['creator_id'],
|
||||
'creator_name': row['creator_name'],
|
||||
'created_at': row['created_at'],
|
||||
'upvotes': row['upvotes'] or 0,
|
||||
'downvotes': row['downvotes'] or 0
|
||||
}
|
||||
cards.append(card)
|
||||
|
||||
return {'cards': cards}
|
||||
except Exception as e:
|
||||
logger.error(f"获取自定义卡牌数据失败: {str(e)}")
|
||||
return {'cards': []}
|
||||
|
||||
def save_custom_cards(data):
|
||||
"""保存自定义卡牌数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
# 清空现有数据(可选,根据需要修改)
|
||||
# cursor.execute("DELETE FROM custom_cards")
|
||||
|
||||
# 插入新数据
|
||||
cards = data.get('cards', [])
|
||||
for card in cards:
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO custom_cards
|
||||
(id, title, character_data, description, option_a, option_b, creator_id, creator_name, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
card['id'],
|
||||
card['title'],
|
||||
json.dumps(card['character'], ensure_ascii=False),
|
||||
card['description'],
|
||||
json.dumps(card['option_a'], ensure_ascii=False),
|
||||
json.dumps(card['option_b'], ensure_ascii=False),
|
||||
card['creator_id'],
|
||||
card['creator_name'],
|
||||
card.get('created_at', datetime.datetime.now().isoformat())
|
||||
)
|
||||
)
|
||||
|
||||
# 确保有对应的投票统计记录
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
|
||||
(card['id'], card.get('upvotes', 0), card.get('downvotes', 0))
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存自定义卡牌数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 卡牌投票操作
|
||||
def get_card_votes():
|
||||
"""获取卡牌投票数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 获取用户投票
|
||||
cursor.execute("SELECT user_id, card_id, vote_type FROM card_votes")
|
||||
vote_rows = cursor.fetchall()
|
||||
|
||||
user_votes = {}
|
||||
for row in vote_rows:
|
||||
user_id = row['user_id']
|
||||
if user_id not in user_votes:
|
||||
user_votes[user_id] = {}
|
||||
user_votes[user_id][row['card_id']] = row['vote_type']
|
||||
|
||||
# 获取投票统计
|
||||
cursor.execute("SELECT card_id, upvotes, downvotes FROM card_vote_stats")
|
||||
stat_rows = cursor.fetchall()
|
||||
|
||||
card_votes = {}
|
||||
for row in stat_rows:
|
||||
card_id = row['card_id']
|
||||
card_votes[card_id] = {
|
||||
'upvotes': row['upvotes'] or 0,
|
||||
'downvotes': row['downvotes'] or 0,
|
||||
'users': {} # 这部分可能需要额外填充,取决于原来的数据结构
|
||||
}
|
||||
|
||||
return {'user_votes': user_votes, 'card_votes': card_votes}
|
||||
except Exception as e:
|
||||
logger.error(f"获取卡牌投票数据失败: {str(e)}")
|
||||
return {'user_votes': {}, 'card_votes': {}}
|
||||
|
||||
def save_card_votes(data):
|
||||
"""保存卡牌投票数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
# 更新用户投票
|
||||
user_votes = data.get('user_votes', {})
|
||||
for user_id, votes in user_votes.items():
|
||||
for card_id, vote_type in votes.items():
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO card_votes
|
||||
(card_id, user_id, vote_type, created_at)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(
|
||||
card_id,
|
||||
user_id,
|
||||
vote_type,
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
# 更新投票统计
|
||||
card_votes = data.get('card_votes', {})
|
||||
for card_id, votes in card_votes.items():
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
|
||||
(
|
||||
card_id,
|
||||
votes.get('upvotes', 0),
|
||||
votes.get('downvotes', 0)
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存卡牌投票数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 会话操作
|
||||
def get_sessions():
|
||||
"""获取会话数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT * FROM sessions")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
active_sessions = []
|
||||
for row in rows:
|
||||
session = {
|
||||
'user_id': row['user_id'],
|
||||
'login_time': row['login_time'],
|
||||
'last_activity': row['last_activity'],
|
||||
'ip_address': row['ip_address'],
|
||||
'status': row['status']
|
||||
}
|
||||
active_sessions.append(session)
|
||||
|
||||
return {'active_sessions': active_sessions}
|
||||
except Exception as e:
|
||||
logger.error(f"获取会话数据失败: {str(e)}")
|
||||
return {'active_sessions': []}
|
||||
|
||||
def save_sessions(data):
|
||||
"""保存会话数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
# 清空旧会话
|
||||
cursor.execute("DELETE FROM sessions")
|
||||
|
||||
# 插入新会话
|
||||
active_sessions = data.get('active_sessions', [])
|
||||
for session in active_sessions:
|
||||
# 生成会话ID
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
cursor.execute(
|
||||
"""INSERT INTO sessions
|
||||
(id, user_id, login_time, last_activity, ip_address, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
session_id,
|
||||
session['user_id'],
|
||||
session.get('login_time', datetime.datetime.now().isoformat()),
|
||||
session.get('last_activity', datetime.datetime.now().isoformat()),
|
||||
session.get('ip_address', ''),
|
||||
session.get('status', 'idle')
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存会话数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 投票卡牌的实用函数
|
||||
def vote_card(card_id, user_id, vote_type):
|
||||
"""对卡牌进行投票或取消投票"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
# 检查是否已存在投票
|
||||
cursor.execute("SELECT vote_type FROM card_votes WHERE card_id = ? AND user_id = ?", (card_id, user_id))
|
||||
existing_vote = cursor.fetchone()
|
||||
|
||||
# 检查卡牌投票统计
|
||||
cursor.execute("SELECT upvotes, downvotes FROM card_vote_stats WHERE card_id = ?", (card_id,))
|
||||
vote_stats = cursor.fetchone()
|
||||
|
||||
upvotes = 0
|
||||
downvotes = 0
|
||||
|
||||
if vote_stats:
|
||||
upvotes = vote_stats['upvotes'] or 0
|
||||
downvotes = vote_stats['downvotes'] or 0
|
||||
|
||||
new_vote = None
|
||||
|
||||
if vote_type == 'none' or (existing_vote and existing_vote['vote_type'] == vote_type):
|
||||
# 取消投票
|
||||
if existing_vote:
|
||||
if existing_vote['vote_type'] == 'upvote':
|
||||
upvotes = max(0, upvotes - 1)
|
||||
else:
|
||||
downvotes = max(0, downvotes - 1)
|
||||
|
||||
# 删除投票记录
|
||||
cursor.execute("DELETE FROM card_votes WHERE card_id = ? AND user_id = ?", (card_id, user_id))
|
||||
|
||||
else:
|
||||
# 如果之前投过票,先取消之前的投票
|
||||
if existing_vote:
|
||||
if existing_vote['vote_type'] == 'upvote':
|
||||
upvotes = max(0, upvotes - 1)
|
||||
else:
|
||||
downvotes = max(0, downvotes - 1)
|
||||
|
||||
# 添加新投票
|
||||
if vote_type == 'upvote':
|
||||
upvotes += 1
|
||||
new_vote = 'upvote'
|
||||
else:
|
||||
downvotes += 1
|
||||
new_vote = 'downvote'
|
||||
|
||||
# 更新或添加投票记录
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO card_votes
|
||||
(card_id, user_id, vote_type, created_at)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(card_id, user_id, vote_type, datetime.datetime.now().isoformat())
|
||||
)
|
||||
|
||||
# 更新投票统计
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO card_vote_stats
|
||||
(card_id, upvotes, downvotes)
|
||||
VALUES (?, ?, ?)""",
|
||||
(card_id, upvotes, downvotes)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'upvotes': upvotes,
|
||||
'downvotes': downvotes,
|
||||
'newVote': new_vote
|
||||
}
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"投票失败: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
# 死亡场景操作
|
||||
def get_death_scenarios(death_type=None):
|
||||
"""获取死亡场景数据,可选按死亡类型筛选"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
if death_type:
|
||||
cursor.execute("SELECT * FROM death_scenarios WHERE death_type = ?", (death_type,))
|
||||
else:
|
||||
cursor.execute("SELECT * FROM death_scenarios")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
scenarios = []
|
||||
for row in rows:
|
||||
scenario = {
|
||||
'id': row['id'],
|
||||
'death_type': row['death_type'],
|
||||
'name': row['name'],
|
||||
'weight': row['weight'] or 100,
|
||||
'first_card': json.loads(row['first_card']),
|
||||
'second_card': json.loads(row['second_card'])
|
||||
}
|
||||
scenarios.append(scenario)
|
||||
|
||||
return scenarios
|
||||
except Exception as e:
|
||||
logger.error(f"获取死亡场景数据失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_random_death_scenario(death_type):
|
||||
"""根据死亡类型获取随机死亡场景,考虑权重"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 获取指定类型的所有死亡场景
|
||||
cursor.execute("SELECT id, weight FROM death_scenarios WHERE death_type = ?", (death_type,))
|
||||
scenarios = cursor.fetchall()
|
||||
|
||||
if not scenarios:
|
||||
return None
|
||||
|
||||
# 计算权重总和
|
||||
total_weight = sum(s['weight'] or 100 for s in scenarios)
|
||||
|
||||
# 随机选择一个场景,考虑权重
|
||||
rand_val = random.randint(1, total_weight)
|
||||
|
||||
# 根据权重选择
|
||||
current_weight = 0
|
||||
selected_id = None
|
||||
|
||||
for scenario in scenarios:
|
||||
current_weight += scenario['weight'] or 100
|
||||
if rand_val <= current_weight:
|
||||
selected_id = scenario['id']
|
||||
break
|
||||
|
||||
# 获取选中的场景详情
|
||||
if selected_id:
|
||||
cursor.execute("SELECT * FROM death_scenarios WHERE id = ?", (selected_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'id': row['id'],
|
||||
'death_type': row['death_type'],
|
||||
'name': row['name'],
|
||||
'weight': row['weight'] or 100,
|
||||
'first_card': json.loads(row['first_card']),
|
||||
'second_card': json.loads(row['second_card'])
|
||||
}
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取随机死亡场景失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def save_death_scenarios(scenarios):
|
||||
"""保存死亡场景数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
for scenario in scenarios:
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO death_scenarios
|
||||
(id, death_type, name, weight, first_card, second_card)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
scenario['id'],
|
||||
scenario['death_type'],
|
||||
scenario['name'],
|
||||
scenario.get('weight', 100),
|
||||
json.dumps(scenario['first_card'], ensure_ascii=False),
|
||||
json.dumps(scenario['second_card'], ensure_ascii=False)
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存死亡场景数据失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 成就系统操作
|
||||
def get_achievements():
|
||||
"""获取所有成就数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("SELECT * FROM achievements")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
achievements = []
|
||||
for row in rows:
|
||||
achievement = {
|
||||
'id': row['id'],
|
||||
'name': row['name'],
|
||||
'description': row['description'],
|
||||
'icon': row['icon'],
|
||||
'death_scenario_id': row['death_scenario_id'],
|
||||
'hidden': row['hidden']
|
||||
}
|
||||
achievements.append(achievement)
|
||||
|
||||
return achievements
|
||||
except Exception as e:
|
||||
logger.error(f"获取成就数据失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_user_achievements(user_id):
|
||||
"""获取用户已解锁的成就"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT a.*, ua.unlock_time
|
||||
FROM achievements a
|
||||
JOIN user_achievements ua ON a.id = ua.achievement_id
|
||||
WHERE ua.user_id = ?
|
||||
""", (user_id,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
unlocked = []
|
||||
for row in rows:
|
||||
achievement = {
|
||||
'id': row['id'],
|
||||
'name': row['name'],
|
||||
'description': row['description'],
|
||||
'icon': row['icon'],
|
||||
'death_scenario_id': row['death_scenario_id'],
|
||||
'unlock_time': row['unlock_time']
|
||||
}
|
||||
unlocked.append(achievement)
|
||||
|
||||
return unlocked
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户成就数据失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def unlock_achievement(user_id, achievement_id):
|
||||
"""解锁用户成就"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 检查成就是否存在
|
||||
cursor.execute("SELECT id FROM achievements WHERE id = ?", (achievement_id,))
|
||||
if not cursor.fetchone():
|
||||
logger.error(f"成就不存在: {achievement_id}")
|
||||
return False
|
||||
|
||||
# 检查用户是否已解锁该成就
|
||||
cursor.execute(
|
||||
"SELECT * FROM user_achievements WHERE user_id = ? AND achievement_id = ?",
|
||||
(user_id, achievement_id)
|
||||
)
|
||||
|
||||
if cursor.fetchone():
|
||||
# 已解锁,无需重复操作
|
||||
return True
|
||||
|
||||
# 解锁新成就
|
||||
cursor.execute(
|
||||
"""INSERT INTO user_achievements (user_id, achievement_id, unlock_time)
|
||||
VALUES (?, ?, ?)""",
|
||||
(
|
||||
user_id,
|
||||
achievement_id,
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error(f"解锁成就失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def save_achievements(achievements):
|
||||
"""保存成就数据"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
|
||||
for achievement in achievements:
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO achievements
|
||||
(id, name, description, icon, death_scenario_id, hidden)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
achievement['id'],
|
||||
achievement['name'],
|
||||
achievement['description'],
|
||||
achievement['icon'],
|
||||
achievement.get('death_scenario_id'),
|
||||
achievement.get('hidden', 0)
|
||||
)
|
||||
)
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
conn.rollback()
|
||||
logger.error(f"保存成就数据失败: {str(e)}")
|
||||
return False
|
||||
4
frp启动.bat
Normal file
@ -0,0 +1,4 @@
|
||||
@echo on
|
||||
cd /d "C:\Users\KOJO JOTARO\Desktop\vps\frp_0.61.2_windows_amd64"
|
||||
frpc.exe -c frpc.ini
|
||||
pause
|
||||
305
migrate_to_sqlite.py
Normal file
@ -0,0 +1,305 @@
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
filename='migration.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('migration')
|
||||
|
||||
# 数据文件路径
|
||||
DATA_DIR = 'data'
|
||||
DB_PATH = os.path.join(DATA_DIR, 'warhammer.db')
|
||||
USERS_FILE = os.path.join(DATA_DIR, 'users.json')
|
||||
GAME_STATES_FILE = os.path.join(DATA_DIR, 'game_states.json')
|
||||
CARDS_FILE = os.path.join(DATA_DIR, 'cards.json')
|
||||
CARD_VOTES_FILE = os.path.join(DATA_DIR, 'card_votes.json')
|
||||
CUSTOM_CARDS_FILE = os.path.join(DATA_DIR, 'custom_cards.json')
|
||||
SESSIONS_FILE = os.path.join(DATA_DIR, 'sessions.json')
|
||||
|
||||
def create_tables(conn):
|
||||
"""创建数据库表结构"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 用户表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
high_score INTEGER DEFAULT 0,
|
||||
total_games INTEGER DEFAULT 0,
|
||||
last_year INTEGER DEFAULT 41000,
|
||||
last_game_time TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# 游戏状态表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS game_states (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
game_data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 自定义卡牌表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS custom_cards (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
character_data TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
option_a TEXT NOT NULL,
|
||||
option_b TEXT NOT NULL,
|
||||
creator_id TEXT NOT NULL,
|
||||
creator_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (creator_id) REFERENCES users(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 卡牌投票表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS card_votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
card_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
vote_type TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (card_id) REFERENCES custom_cards(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE(card_id, user_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 卡牌投票统计表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS card_vote_stats (
|
||||
card_id TEXT PRIMARY KEY,
|
||||
upvotes INTEGER DEFAULT 0,
|
||||
downvotes INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (card_id) REFERENCES custom_cards(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 会话表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
login_time TEXT NOT NULL,
|
||||
last_activity TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
status TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 卡牌数据表 - 只存储一条记录,因为这是游戏基础数据
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS game_cards (
|
||||
id INTEGER PRIMARY KEY,
|
||||
cards_data TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
logger.info("数据库表创建完成")
|
||||
|
||||
def load_json_data(file_path, default_value=None):
|
||||
"""从JSON文件安全地加载数据"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
logger.warning(f"加载文件 {file_path} 失败: {str(e)}")
|
||||
return default_value if default_value is not None else {}
|
||||
|
||||
def migrate_users(conn, users_data):
|
||||
"""迁移用户数据"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
for user in users_data:
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO users (id, username, password, created_at, high_score, total_games, last_year, last_game_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
user['id'],
|
||||
user['username'],
|
||||
user['password'],
|
||||
user.get('created_at', datetime.datetime.utcnow().isoformat()),
|
||||
user.get('high_score', 0),
|
||||
user.get('total_games', 0),
|
||||
user.get('last_year', 41000),
|
||||
user.get('last_game_time', None)
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"用户数据迁移完成,共 {len(users_data)} 条记录")
|
||||
|
||||
def migrate_game_states(conn, game_states_data):
|
||||
"""迁移游戏状态数据"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
for state in game_states_data:
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO game_states (id, user_id, game_data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
state['id'],
|
||||
state['user_id'],
|
||||
json.dumps(state['game_data'], ensure_ascii=False),
|
||||
state.get('created_at', datetime.datetime.utcnow().isoformat()),
|
||||
state.get('updated_at', datetime.datetime.utcnow().isoformat())
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"游戏状态数据迁移完成,共 {len(game_states_data)} 条记录")
|
||||
|
||||
def migrate_custom_cards(conn, custom_cards_data):
|
||||
"""迁移自定义卡牌数据"""
|
||||
cursor = conn.cursor()
|
||||
cards = custom_cards_data.get('cards', [])
|
||||
|
||||
for card in cards:
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO custom_cards (id, title, character_data, description, option_a, option_b, creator_id, creator_name, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
card['id'],
|
||||
card['title'],
|
||||
json.dumps(card['character'], ensure_ascii=False),
|
||||
card['description'],
|
||||
json.dumps(card['option_a'], ensure_ascii=False),
|
||||
json.dumps(card['option_b'], ensure_ascii=False),
|
||||
card['creator_id'],
|
||||
card['creator_name'],
|
||||
card.get('created_at', datetime.datetime.utcnow().isoformat())
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"自定义卡牌数据迁移完成,共 {len(cards)} 条记录")
|
||||
|
||||
def migrate_card_votes(conn, card_votes_data):
|
||||
"""迁移卡牌投票数据"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 迁移用户投票记录
|
||||
user_votes = card_votes_data.get('user_votes', {})
|
||||
for user_id, votes in user_votes.items():
|
||||
for card_id, vote_type in votes.items():
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO card_votes (card_id, user_id, vote_type, created_at) VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
card_id,
|
||||
user_id,
|
||||
vote_type,
|
||||
datetime.datetime.utcnow().isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
# 迁移卡牌投票统计
|
||||
card_votes = card_votes_data.get('card_votes', {})
|
||||
for card_id, votes in card_votes.items():
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO card_vote_stats (card_id, upvotes, downvotes) VALUES (?, ?, ?)",
|
||||
(
|
||||
card_id,
|
||||
votes.get('upvotes', 0),
|
||||
votes.get('downvotes', 0)
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"卡牌投票数据迁移完成")
|
||||
|
||||
def migrate_sessions(conn, sessions_data):
|
||||
"""迁移会话数据"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
active_sessions = sessions_data.get('active_sessions', [])
|
||||
for session in active_sessions:
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO sessions (id, user_id, login_time, last_activity, ip_address, status) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
str(hash(session['user_id'] + session.get('login_time', ''))), # 创建唯一ID
|
||||
session['user_id'],
|
||||
session.get('login_time', datetime.datetime.utcnow().isoformat()),
|
||||
session.get('last_activity', datetime.datetime.utcnow().isoformat()),
|
||||
session.get('ip_address', ''),
|
||||
session.get('status', 'idle')
|
||||
)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"会话数据迁移完成,共 {len(active_sessions)} 条记录")
|
||||
|
||||
def migrate_cards_data(conn, cards_data):
|
||||
"""迁移卡牌基础数据"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO game_cards (id, cards_data) VALUES (?, ?)",
|
||||
(1, json.dumps(cards_data, ensure_ascii=False))
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"卡牌基础数据迁移完成")
|
||||
|
||||
def main():
|
||||
"""主迁移函数"""
|
||||
logger.info("开始数据迁移")
|
||||
|
||||
# 检查数据库文件路径
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
|
||||
# 创建/连接到数据库
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
|
||||
try:
|
||||
# 创建表结构
|
||||
create_tables(conn)
|
||||
|
||||
# 加载并迁移用户数据
|
||||
users_data = load_json_data(USERS_FILE, [])
|
||||
migrate_users(conn, users_data)
|
||||
|
||||
# 加载并迁移游戏状态数据
|
||||
game_states_data = load_json_data(GAME_STATES_FILE, [])
|
||||
migrate_game_states(conn, game_states_data)
|
||||
|
||||
# 加载并迁移自定义卡牌数据
|
||||
custom_cards_data = load_json_data(CUSTOM_CARDS_FILE, {"cards": []})
|
||||
migrate_custom_cards(conn, custom_cards_data)
|
||||
|
||||
# 加载并迁移卡牌投票数据
|
||||
card_votes_data = load_json_data(CARD_VOTES_FILE, {"user_votes": {}, "card_votes": {}})
|
||||
migrate_card_votes(conn, card_votes_data)
|
||||
|
||||
# 加载并迁移会话数据
|
||||
sessions_data = load_json_data(SESSIONS_FILE, {"active_sessions": []})
|
||||
migrate_sessions(conn, sessions_data)
|
||||
|
||||
# 加载并迁移卡牌基础数据
|
||||
cards_data = load_json_data(CARDS_FILE, {})
|
||||
migrate_cards_data(conn, cards_data)
|
||||
|
||||
logger.info("数据迁移成功完成")
|
||||
except Exception as e:
|
||||
logger.error(f"迁移失败: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
migration.log
Normal file
@ -0,0 +1,9 @@
|
||||
2025-05-08 14:24:54,743 - migration - INFO - 开始数据迁移
|
||||
2025-05-08 14:24:54,802 - migration - INFO - 数据库表创建完成
|
||||
2025-05-08 14:24:54,966 - migration - INFO - 用户数据迁移完成,共 5113 条记录
|
||||
2025-05-08 14:24:55,346 - migration - INFO - 游戏状态数据迁移完成,共 4918 条记录
|
||||
2025-05-08 14:24:55,356 - migration - INFO - 自定义卡牌数据迁移完成,共 30 条记录
|
||||
2025-05-08 14:24:55,374 - migration - INFO - 卡牌投票数据迁移完成
|
||||
2025-05-08 14:24:55,381 - migration - INFO - 会话数据迁移完成,共 1 条记录
|
||||
2025-05-08 14:24:55,392 - migration - INFO - 卡牌基础数据迁移完成
|
||||
2025-05-08 14:24:55,393 - migration - INFO - 数据迁移成功完成
|
||||
268
monitor.py
Normal file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask, render_template_string, jsonify
|
||||
import sqlite3
|
||||
import datetime
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>战锤40K行星总督 - 游戏监控</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
background-image: linear-gradient(to bottom, #0a0a0a, #1a1a1a);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(30, 30, 35, 0.9);
|
||||
border: 2px solid #600;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px rgba(153, 0, 0, 0.5);
|
||||
}
|
||||
h1, h2 {
|
||||
color: #d4af37;
|
||||
text-shadow: 0 0 10px rgba(212, 175, 55, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 30px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat-card {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid #600;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
width: 200px;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #d4af37;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 1rem;
|
||||
color: #999;
|
||||
}
|
||||
.user-list {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.user-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.playing {
|
||||
color: #4caf50;
|
||||
}
|
||||
.idle {
|
||||
color: #ff9800;
|
||||
}
|
||||
.refresh-time {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.btn {
|
||||
background-color: #600;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #900;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>战锤40K行星总督</h1>
|
||||
<h2>服务器监控面板</h2>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总在线用户</div>
|
||||
<div class="stat-value" id="total-users">{{ total_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">游戏中用户</div>
|
||||
<div class="stat-value" id="playing-users">{{ playing_users }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">注册用户总数</div>
|
||||
<div class="stat-value">{{ registered_users }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-list">
|
||||
<h3>当前在线用户:</h3>
|
||||
{% if active_users %}
|
||||
{% for user in active_users %}
|
||||
<div class="user-item">
|
||||
<span>{{ user.username }}</span>
|
||||
<span class="{{ user.status }}">{{ user.status }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="user-item">当前没有用户在线</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button class="btn" onclick="refreshData()">刷新数据</button>
|
||||
|
||||
<div class="refresh-time">上次更新时间: {{ refresh_time }}</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function refreshData() {
|
||||
fetch('/monitor/api/stats')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('total-users').textContent = data.total_users;
|
||||
document.getElementById('playing-users').textContent = data.playing_users;
|
||||
|
||||
// 更新刷新时间
|
||||
const refreshTimeElement = document.querySelector('.refresh-time');
|
||||
refreshTimeElement.textContent = '上次更新时间: ' + data.refresh_time;
|
||||
|
||||
// 自动刷新整个页面以更新用户列表
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 100);
|
||||
})
|
||||
.catch(error => console.error('获取数据失败:', error));
|
||||
}
|
||||
|
||||
// 每60秒自动刷新一次
|
||||
setInterval(refreshData, 60000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def get_db_connection():
|
||||
"""连接到SQLite数据库"""
|
||||
db_path = os.path.join('data', 'warhammer.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_stats():
|
||||
"""获取游戏统计数据"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
|
||||
# 获取注册用户总数
|
||||
registered_users = conn.execute('SELECT COUNT(*) as count FROM users').fetchone()['count']
|
||||
|
||||
# 获取活跃会话 (假设数据库中有sessions表,根据实际情况调整)
|
||||
# 如果没有sessions表,需要创建或使用其他方式跟踪在线用户
|
||||
try:
|
||||
cursor = conn.execute('''
|
||||
SELECT s.*, u.username
|
||||
FROM sessions s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.last_activity > ?
|
||||
''', (datetime.datetime.now() - datetime.timedelta(minutes=30),))
|
||||
active_sessions = cursor.fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
# 如果sessions表不存在,尝试备选方案
|
||||
try:
|
||||
# 尝试从game_states表获取活跃用户
|
||||
cursor = conn.execute('''
|
||||
SELECT gs.*, u.username, u.id as user_id
|
||||
FROM game_states gs
|
||||
JOIN users u ON gs.user_id = u.id
|
||||
WHERE gs.updated_at > ?
|
||||
''', ((datetime.datetime.now() - datetime.timedelta(minutes=30)).isoformat(),))
|
||||
active_sessions = cursor.fetchall()
|
||||
|
||||
# 添加状态字段模拟
|
||||
for session in active_sessions:
|
||||
# 假设最近30分钟内更新的都是在玩游戏的
|
||||
session_dict = dict(session)
|
||||
session_dict['status'] = 'playing'
|
||||
session = session_dict
|
||||
except:
|
||||
# 备选方案也失败,返回空列表
|
||||
active_sessions = []
|
||||
|
||||
# 计算游戏中的用户数
|
||||
playing_users = sum(1 for session in active_sessions if session['status'] == 'playing')
|
||||
|
||||
# 格式化活跃用户列表
|
||||
active_users = []
|
||||
for session in active_sessions:
|
||||
user_info = {
|
||||
'username': session['username'],
|
||||
'status': session['status'],
|
||||
'user_id': session['user_id']
|
||||
}
|
||||
active_users.append(user_info)
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
'registered_users': registered_users,
|
||||
'total_users': len(active_sessions),
|
||||
'playing_users': playing_users,
|
||||
'active_users': active_users,
|
||||
'refresh_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取统计数据时出错: {str(e)}")
|
||||
return {
|
||||
'registered_users': 0,
|
||||
'total_users': 0,
|
||||
'playing_users': 0,
|
||||
'active_users': [],
|
||||
'refresh_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
@app.route('/monitor')
|
||||
def monitor_page():
|
||||
"""监控页面"""
|
||||
stats = get_stats()
|
||||
return render_template_string(HTML_TEMPLATE, **stats)
|
||||
|
||||
@app.route('/monitor/api/stats')
|
||||
def api_stats():
|
||||
"""统计数据API"""
|
||||
return jsonify(get_stats())
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='127.0.0.1', port=5050, debug=False)
|
||||
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Flask==2.3.3
|
||||
Werkzeug==2.3.7
|
||||
3
start.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /var/www/warhammer
|
||||
gunicorn --bind 0.0.0.0:8080 app_sqlite:app
|
||||
BIN
static/.DS_Store
vendored
Normal file
1250
static/css/admin.css
Normal file
645
static/css/custom_cards.css
Normal file
@ -0,0 +1,645 @@
|
||||
/* 自制卡牌通用样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 卡牌列表页样式 */
|
||||
.filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
position: sticky; /* 使筛选器固定在顶部 */
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background-color: rgba(10, 10, 10, 0.95);
|
||||
padding: 15px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px;
|
||||
background-color: #222;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cards-wrapper {
|
||||
margin-top: 20px;
|
||||
overflow-y: auto; /* 确保内容可滚动 */
|
||||
padding-bottom: 50px; /* 添加底部空间 */
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 修改为一行显示两张卡牌 */
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
background-color: rgba(30, 30, 35, 0.8);
|
||||
border: 1px solid #600;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* 确保所有卡片高度相同 */
|
||||
}
|
||||
|
||||
.card-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(153, 0, 0, 0.4);
|
||||
background-color: rgba(96, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card-info h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.card-info p {
|
||||
margin: 0;
|
||||
color: #999;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 10px 0;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin: 0;
|
||||
color: #e0e0e0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 10px 15px;
|
||||
border-top: 1px solid #333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-creator {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-votes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.vote-positive {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.vote-negative {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.vote-neutral {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
grid-column: 1 / -1;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.card-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: rgba(30, 30, 35, 0.95);
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border: 2px solid #600;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
box-shadow: 0 0 30px rgba(153, 0, 0, 0.7);
|
||||
animation: slideIn 0.3s;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
color: #999;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.card-detail-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card-detail-container .card-title {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.card-detail-container .card-creator,
|
||||
.card-detail-container .card-date {
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-options {
|
||||
margin: 20px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.card-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card-options .option {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.card-options .option:first-child {
|
||||
background-color: rgba(74, 111, 181, 0.2);
|
||||
border: 1px solid rgba(74, 111, 181, 0.5);
|
||||
}
|
||||
|
||||
.card-options .option:last-child {
|
||||
background-color: rgba(179, 57, 57, 0.2);
|
||||
border: 1px solid rgba(179, 57, 57, 0.5);
|
||||
}
|
||||
|
||||
.card-options .option h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.effects {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.effect-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 新的效果标签样式 */
|
||||
.effect-tag.positive {
|
||||
background-color: rgba(40, 60, 40, 0.9); /* 深绿色/几乎黑色的背景 */
|
||||
color: #ffffff; /* 纯白色文字 */
|
||||
border: 1px solid #4caf50;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* 添加文字阴影增强可读性 */
|
||||
}
|
||||
|
||||
.effect-tag.negative {
|
||||
background-color: rgba(60, 40, 40, 0.9); /* 深红色/几乎黑色的背景 */
|
||||
color: #ffffff; /* 纯白色文字 */
|
||||
border: 1px solid #f44336;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* 添加文字阴影增强可读性 */
|
||||
}
|
||||
|
||||
.card-voting {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.vote-count {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vote-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn-vote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 15px;
|
||||
font-size: 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upvote {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.downvote {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
border: 1px solid rgba(244, 67, 54, 0.5);
|
||||
color: #e57373;
|
||||
}
|
||||
|
||||
.upvote:hover {
|
||||
background-color: rgba(76, 175, 80, 0.4);
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.downvote:hover {
|
||||
background-color: rgba(244, 67, 54, 0.4);
|
||||
box-shadow: 0 0 10px rgba(244, 67, 54, 0.5);
|
||||
}
|
||||
|
||||
.upvote.voted {
|
||||
background-color: rgba(76, 175, 80, 0.6);
|
||||
box-shadow: 0 0 10px rgba(76, 175, 80, 0.7);
|
||||
}
|
||||
|
||||
.downvote.voted {
|
||||
background-color: rgba(244, 67, 54, 0.6);
|
||||
box-shadow: 0 0 10px rgba(244, 67, 54, 0.7);
|
||||
}
|
||||
|
||||
/* 创建卡牌页面样式 */
|
||||
.card-form-container {
|
||||
background-color: rgba(30, 30, 35, 0.8);
|
||||
border: 1px solid #600;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px; /* 添加底部空间确保滚动 */
|
||||
overflow-y: visible; /* 确保内容可以滚动 */
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #222;
|
||||
border: 1px solid #444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #600;
|
||||
box-shadow: 0 0 5px rgba(153, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.form-group input[type="number"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.character-count {
|
||||
text-align: right;
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.effects-group h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2rem;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.effects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.effects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.effect-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.effect-item label {
|
||||
width: 120px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.total-effects {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.effect-warning {
|
||||
color: #f44336;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 表情符号选择器 */
|
||||
.emoji-picker {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.emoji-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(2, 1fr); /* 保持一行两张卡牌 */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 10% auto;
|
||||
}
|
||||
|
||||
.card-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(2, 1fr); /* 即使在小屏幕上也保持两列 */
|
||||
gap: 10px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 调整卡片内容,让它在小屏幕上更紧凑 */
|
||||
.card-header {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card-info h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-info p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 浅色主题适配 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
.card-item, .card-form-container, .modal-content {
|
||||
background-color: rgba(240, 240, 245, 0.9);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-title, h1, h2, h3 {
|
||||
color: #600;
|
||||
}
|
||||
|
||||
.card-text, .form-group label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保可以滚动的页面设置 */
|
||||
.custom-cards-page, .create-card-page {
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* 页面底部添加额外空间 */
|
||||
.custom-cards-page footer, .create-card-page footer {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* 空状态处理 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.empty-state .btn {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 错误消息样式 */
|
||||
.error-message {
|
||||
background-color: rgba(153, 0, 0, 0.3);
|
||||
border: 1px solid #600;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
color: #ff9999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 消息弹出提示样式 */
|
||||
.message-popup {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
transition: transform 0.3s ease;
|
||||
text-align: center;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-popup.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
1343
static/css/style.css
Normal file
BIN
static/images/.DS_Store
vendored
Normal file
BIN
static/images/characters/apostle.png
Normal file
|
After Width: | Height: | Size: 1024 KiB |
BIN
static/images/characters/astartes_representative.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/bohai.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/images/characters/casto.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/crazy.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/images/characters/da.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/images/characters/davis.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/duran.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/images/characters/elai.png
Normal file
|
After Width: | Height: | Size: 1017 KiB |
BIN
static/images/characters/emperor.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/ferrum.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/fex.png
Normal file
|
After Width: | Height: | Size: 1020 KiB |
BIN
static/images/characters/gw.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/characters/hawk.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |