fix: 修复特征分析页面图表 + 补充派生特征计算脚本
- src/routes.py: 补充火箭炮分析API缺失的8个字段(射速/射程/发动机/机动参数) - AnalysisPage.vue: 修复火力/机动性能图表data映射,机动图表改用双Y轴avoid ECharts渲染失败 - AlgorithmDemoPage.vue: 移除算法卡片中的平均绝对误差和均方根误差显示 - scripts/calculate_features.py: 新增派生特征计算脚本(min-max归一化评分、巡飞弹缺失字段估算)
This commit is contained in:
parent
48ba547c36
commit
ca8cf02aa4
@ -86,10 +86,7 @@
|
||||
<el-tag v-if="row.key === result.best_model" size="small" type="success">最佳</el-tag>
|
||||
</div>
|
||||
<strong>{{ formatScore(row.r2) }}</strong>
|
||||
<div class="metric-values">
|
||||
<span>平均绝对误差 {{ formatMoney(row.mae) }}</span>
|
||||
<span>均方根误差 {{ formatMoney(row.rmse) }}</span>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
@ -735,7 +735,13 @@ const renderCharts = () => {
|
||||
mobilityScore: analysisResult.value.mobility_score || [],
|
||||
deploymentScore: analysisResult.value.deployment_score || [],
|
||||
terrainScore: analysisResult.value.terrain_adaptability_score || [],
|
||||
combatReadinessScore: analysisResult.value.combat_readiness_score || []
|
||||
combatReadinessScore: analysisResult.value.combat_readiness_score || [],
|
||||
rocketWeight: analysisResult.value.rocket_weight_kg || [],
|
||||
rocketDiameter: analysisResult.value.rocket_diameter_mm || [],
|
||||
rocketLength: analysisResult.value.rocket_length_m || [],
|
||||
speedKmh: analysisResult.value.speed_kmh || [],
|
||||
powerHp: analysisResult.value.power_hp || [],
|
||||
travelRangeKm: analysisResult.value.travel_range_km || []
|
||||
}
|
||||
|
||||
// 火力性能分析图表配置
|
||||
@ -849,7 +855,7 @@ const renderCharts = () => {
|
||||
top: 30,
|
||||
data: [
|
||||
'机动性评分', '部署评分', '地形适应性评分', '战备状态评分',
|
||||
'行驶速度', '功率', '行程' // 新增参数
|
||||
'行驶速度', '功率', '行程'
|
||||
]
|
||||
},
|
||||
grid: {
|
||||
@ -861,12 +867,20 @@ const renderCharts = () => {
|
||||
type: 'category',
|
||||
data: chartData.names
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '评分',
|
||||
min: 0,
|
||||
max: 10
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '评分',
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 10
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '速度/功率/行程',
|
||||
position: 'right'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '机动性评分',
|
||||
@ -891,16 +905,19 @@ const renderCharts = () => {
|
||||
{
|
||||
name: '行驶速度',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: chartData.speedKmh
|
||||
},
|
||||
{
|
||||
name: '功率',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: chartData.powerHp
|
||||
},
|
||||
{
|
||||
name: '行程',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: chartData.travelRangeKm
|
||||
}
|
||||
]
|
||||
|
||||
383
scripts/calculate_features.py
Normal file
383
scripts/calculate_features.py
Normal file
@ -0,0 +1,383 @@
|
||||
"""
|
||||
计算并填充数据库中的派生/计算特征字段,包括:
|
||||
1. 火箭炮:火力密度、射程比、各项评分
|
||||
2. 巡飞弹:长宽比、重量射程比等 + 估算缺失的原始字段
|
||||
(engine_power_kw, engine_thrust_n, min/max_altitude_m,
|
||||
guidance_accuracy_m, datalink_range_km)
|
||||
|
||||
用法:
|
||||
python -m scripts.calculate_features
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.database.db_connection import get_db_connection, init_db
|
||||
from src.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def clamp(value, low=1, high=10):
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def safe_float(val, default=0.0):
|
||||
if val is None:
|
||||
return default
|
||||
return float(val)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# 火箭炮
|
||||
# ────────────────────────────────────────────────
|
||||
|
||||
def calculate_rocket_features(conn):
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT rap.id, rap.equipment_id,
|
||||
rap.rate_of_fire, rap.rocket_weight_kg, rap.rocket_length_m,
|
||||
rap.rocket_diameter_mm, rap.min_range_km, rap.max_range_km,
|
||||
rap.speed_kmh, rap.power_hp, rap.combat_weight_kg,
|
||||
rap.travel_range_km, rap.firing_angle_horizontal,
|
||||
rap.firing_angle_vertical,
|
||||
cp.length_m, cp.width_m, cp.weight_kg
|
||||
FROM rocket_artillery_params rap
|
||||
LEFT JOIN common_params cp ON rap.equipment_id = cp.equipment_id
|
||||
""")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# ── 第一遍:收集原始值,计算 min/max(用于归一化)──
|
||||
all_vals = {
|
||||
'spd': [], 'power_ratio': [], 'tr': [],
|
||||
'angle_cov': [], 'deploy_factor': [], 'rof': [],
|
||||
'cw': []
|
||||
}
|
||||
for row in rows:
|
||||
spd = safe_float(row['speed_kmh'])
|
||||
pwr = safe_float(row['power_hp'])
|
||||
cw = safe_float(row['combat_weight_kg'])
|
||||
tr = safe_float(row['travel_range_km'])
|
||||
ah = safe_float(row['firing_angle_horizontal'])
|
||||
av = safe_float(row['firing_angle_vertical'])
|
||||
rof = safe_float(row['rate_of_fire'])
|
||||
|
||||
all_vals['spd'].append(spd)
|
||||
all_vals['power_ratio'].append(pwr / max(1, cw))
|
||||
all_vals['tr'].append(tr)
|
||||
all_vals['angle_cov'].append((ah / 360) * (av / 90))
|
||||
all_vals['deploy_factor'].append(1.0 / max(1, cw)) # 越小越难部署
|
||||
all_vals['rof'].append(rof)
|
||||
all_vals['cw'].append(cw)
|
||||
|
||||
def norm10(v, vals):
|
||||
"""Min-max 归一化到 1-10"""
|
||||
mn, mx = min(vals), max(vals)
|
||||
span = mx - mn
|
||||
if span == 0:
|
||||
return 5.0
|
||||
return 1.0 + (v - mn) / span * 9.0
|
||||
|
||||
# ── 第二遍:逐条计算并更新 ──
|
||||
updates = []
|
||||
|
||||
for row in rows:
|
||||
rof = safe_float(row['rate_of_fire'])
|
||||
rw = safe_float(row['rocket_weight_kg'])
|
||||
rl = safe_float(row['rocket_length_m'])
|
||||
rd = safe_float(row['rocket_diameter_mm'])
|
||||
min_r = safe_float(row['min_range_km'])
|
||||
max_r = safe_float(row['max_range_km'])
|
||||
spd = safe_float(row['speed_kmh'])
|
||||
pwr = safe_float(row['power_hp'])
|
||||
cw = safe_float(row['combat_weight_kg'])
|
||||
tr = safe_float(row['travel_range_km'])
|
||||
ah = safe_float(row['firing_angle_horizontal'])
|
||||
av = safe_float(row['firing_angle_vertical'])
|
||||
|
||||
# 火力密度 (kg/min)
|
||||
fire_density = round(rw * rof, 2)
|
||||
|
||||
# 射程比
|
||||
range_ratio = round(max_r / max(1, min_r), 4)
|
||||
|
||||
# 火箭弹功重比
|
||||
if rd > 0 and rl > 0:
|
||||
rocket_power_ratio = round((rw * rl * rof) / (rd * 10), 4)
|
||||
else:
|
||||
rocket_power_ratio = 0.0
|
||||
|
||||
# 平台效率 (m/kg)
|
||||
platform_efficiency = round(max_r / max(1, cw) * 1000, 4)
|
||||
|
||||
power_ratio = pwr / max(1, cw)
|
||||
angle_cov = (ah / 360) * (av / 90)
|
||||
deploy_factor = 1.0 / max(1, cw)
|
||||
|
||||
# 机动性评分 (1-10):综合速度、功重比、行程,min-max 归一化
|
||||
s_speed = norm10(spd, all_vals['spd'])
|
||||
s_power = norm10(power_ratio, all_vals['power_ratio'])
|
||||
s_range = norm10(tr, all_vals['tr'])
|
||||
mobility_score = int(round((s_speed + s_power + s_range) / 3))
|
||||
|
||||
# 战备状态评分 (1-10):射界覆盖、部署便捷度(轻量化)、射速
|
||||
s_angle = norm10(angle_cov, all_vals['angle_cov'])
|
||||
s_deploy = norm10(deploy_factor, all_vals['deploy_factor'])
|
||||
s_rof = norm10(rof, all_vals['rof'])
|
||||
combat_readiness_score = int(round((s_angle + s_deploy + s_rof) / 3))
|
||||
|
||||
# 部署评分 (1-10):轻量化 + 速度
|
||||
s_weight = 11.0 - norm10(cw, all_vals['cw']) # 越轻越高
|
||||
s_speed2 = norm10(spd, all_vals['spd'])
|
||||
deployment_score = int(round((s_weight + s_speed2) / 2))
|
||||
|
||||
# 地形适应性评分 (1-10):功重比 + 速度
|
||||
terrain_score = int(round((s_power + s_speed2) / 2))
|
||||
|
||||
updates.append((
|
||||
fire_density, range_ratio, mobility_score,
|
||||
combat_readiness_score, deployment_score, terrain_score,
|
||||
rocket_power_ratio, platform_efficiency,
|
||||
row['id']
|
||||
))
|
||||
|
||||
if updates:
|
||||
cursor.executemany("""
|
||||
UPDATE rocket_artillery_params
|
||||
SET fire_density = ?, range_ratio = ?, mobility_score = ?,
|
||||
combat_readiness_score = ?, deployment_score = ?,
|
||||
terrain_adaptability_score = ?, rocket_power_ratio = ?,
|
||||
platform_efficiency = ?
|
||||
WHERE id = ?
|
||||
""", updates)
|
||||
logger.info(f"火箭炮:更新了 {len(updates)} 条记录")
|
||||
|
||||
return len(updates)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# 巡飞弹
|
||||
# ────────────────────────────────────────────────
|
||||
|
||||
def estimate_guidance_accuracy(guidance_system):
|
||||
"""根据制导系统文字估算制导精度 (m)"""
|
||||
gs = (guidance_system or '').lower()
|
||||
# 统计制导模式数量作为精度指标
|
||||
modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
|
||||
mode_count = len(modes)
|
||||
|
||||
# 基础精度:模式越多越精确
|
||||
base_accuracy = {
|
||||
1: 50, 2: 30, 3: 20, 4: 15, 5: 10, 6: 5, 7: 3, 8: 2, 9: 1.5, 10: 1,
|
||||
}.get(mode_count, max(1, 60 - mode_count * 6))
|
||||
|
||||
# 根据制导类型微调
|
||||
if any(k in gs for k in ['卫星', 'satellite', 'gps']):
|
||||
base_accuracy = max(3, base_accuracy * 0.7)
|
||||
if any(k in gs for k in ['激光', 'laser']):
|
||||
base_accuracy = max(1, base_accuracy * 0.4)
|
||||
if any(k in gs for k in ['红外', 'infrared', 'ir', '光电']):
|
||||
base_accuracy = max(2, base_accuracy * 0.6)
|
||||
if any(k in gs for k in ['雷达', 'radar']):
|
||||
base_accuracy = max(5, base_accuracy * 0.8)
|
||||
if any(k in gs for k in ['ai', '辅助']):
|
||||
base_accuracy = base_accuracy * 0.8
|
||||
|
||||
return round(base_accuracy, 1)
|
||||
|
||||
|
||||
def calculate_guidance_score(guidance_system, estimated_accuracy):
|
||||
"""计算制导系统评分 (1-10)"""
|
||||
gs = (guidance_system or '').lower()
|
||||
modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
|
||||
|
||||
# 制导模式数量 (1-7 映射到 1-6 分)
|
||||
mode_score = clamp(len(modes) / 2, 0.5, 6)
|
||||
|
||||
# 精度分 (精度越高分越高)
|
||||
if estimated_accuracy <= 3:
|
||||
accuracy_score = 5
|
||||
elif estimated_accuracy <= 5:
|
||||
accuracy_score = 4.5
|
||||
elif estimated_accuracy <= 10:
|
||||
accuracy_score = 4
|
||||
elif estimated_accuracy <= 20:
|
||||
accuracy_score = 3
|
||||
elif estimated_accuracy <= 30:
|
||||
accuracy_score = 2
|
||||
elif estimated_accuracy <= 50:
|
||||
accuracy_score = 1
|
||||
else:
|
||||
accuracy_score = 0.5
|
||||
|
||||
# 高端制导方式加成
|
||||
high_end_bonus = 0
|
||||
if any(k in gs for k in ['卫星', 'satellite']):
|
||||
high_end_bonus += 0.5
|
||||
|
||||
return int(round(clamp(mode_score + accuracy_score + high_end_bonus, 1, 10)))
|
||||
|
||||
|
||||
def calculate_warhead_score(warhead_type, warhead_weight_kg):
|
||||
"""计算战斗部威力评分 (1-10)"""
|
||||
wt_lower = (warhead_type or '').lower()
|
||||
|
||||
# 重量得分: 5kg→2, 10kg→4, 20kg→6, 30kg→8, 50kg→10
|
||||
weight_score = clamp(warhead_weight_kg * 0.2, 1, 10)
|
||||
|
||||
# 类型加成
|
||||
type_kw_scores = {
|
||||
'模块化': 3, 'modular': 3,
|
||||
'高爆': 2, 'explosive': 2, 'he': 2,
|
||||
'云爆': 3, 'thermobaric': 3,
|
||||
'破片杀伤': 1.5, 'fragmentation': 1.5,
|
||||
'穿甲': 2.5, 'armor': 2.5, 'penetrat': 2.5,
|
||||
'动能': 2, 'kinetic': 2,
|
||||
'双用': 2, 'dual': 2,
|
||||
'破甲': 2, 'heat': 2,
|
||||
'子母': 2, 'cluster': 2, 'submunition': 2
|
||||
}
|
||||
type_bonus = 0
|
||||
for kw, val in type_kw_scores.items():
|
||||
if kw in wt_lower:
|
||||
type_bonus = max(type_bonus, val)
|
||||
|
||||
return int(round(clamp(weight_score + type_bonus, 1, 10)))
|
||||
|
||||
|
||||
def calculate_loitering_features(conn):
|
||||
"""计算巡飞弹的派生特征 + 估算缺失的原始字段"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT lmp.id, lmp.equipment_id,
|
||||
lmp.wingspan_m, lmp.warhead_weight_kg, lmp.max_speed_ms,
|
||||
lmp.max_range_km, lmp.guidance_accuracy_m, lmp.datalink_range_km,
|
||||
lmp.engine_power_kw, lmp.engine_thrust_n,
|
||||
lmp.min_altitude_m, lmp.max_altitude_m,
|
||||
lmp.guidance_system, lmp.warhead_type,
|
||||
lmp.cruise_speed_kmh, lmp.endurance_min,
|
||||
lmp.ceiling_altitude_m, lmp.max_payload_kg,
|
||||
lmp.combat_radius_km,
|
||||
cp.length_m, cp.width_m, cp.weight_kg
|
||||
FROM loitering_munition_params lmp
|
||||
LEFT JOIN common_params cp ON lmp.equipment_id = cp.equipment_id
|
||||
""")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
updates = []
|
||||
|
||||
for row in rows:
|
||||
length = safe_float(row['length_m'])
|
||||
width = safe_float(row['width_m'])
|
||||
weight = safe_float(row['weight_kg'])
|
||||
warhead_weight = safe_float(row['warhead_weight_kg'])
|
||||
max_speed = safe_float(row['max_speed_ms'])
|
||||
cruise_speed = safe_float(row['cruise_speed_kmh'])
|
||||
max_range = safe_float(row['max_range_km'])
|
||||
endurance = safe_float(row['endurance_min'])
|
||||
ceiling = safe_float(row['ceiling_altitude_m'])
|
||||
payload = safe_float(row['max_payload_kg'])
|
||||
combat_radius = safe_float(row['combat_radius_km'])
|
||||
guidance_system = row['guidance_system'] or ''
|
||||
warhead_type = row['warhead_type'] or ''
|
||||
|
||||
# ── 1. 长宽比 ──
|
||||
length_width_ratio = round(length / max(0.1, width), 4)
|
||||
|
||||
# ── 2. 重量射程比 (kg/km) ──
|
||||
weight_range_ratio = round(weight / max(1, max_range), 4)
|
||||
|
||||
# ── 3. 速度重量比 ──
|
||||
speed_weight_ratio = round(max_speed / max(1, weight), 4)
|
||||
|
||||
# ── 4. 估算缺失的原始字段 ──
|
||||
|
||||
# 制导精度 (m):当前全为 0,根据制导系统文字估算
|
||||
estimated_accuracy = estimate_guidance_accuracy(guidance_system)
|
||||
|
||||
# 数据链距离 (km):用战斗半径或最大射程的 80% 估算
|
||||
base_ref = combat_radius if combat_radius > 0 else max_range
|
||||
estimated_datalink = round(base_ref * 0.85, 1)
|
||||
|
||||
# 发动机功率 (kw):基于重量和速度估算
|
||||
# 小型无人机/巡飞弹功率密度约 0.05-0.3 kW/kg
|
||||
total_weight = weight + payload
|
||||
power_density = 0.08 + (max_speed / 200) * 0.12 # 越快功率密度越高
|
||||
power_density = min(power_density, 0.28) # 上限避免重型弹药估算过高
|
||||
estimated_engine_kw = round(total_weight * power_density, 1)
|
||||
|
||||
# 发动机推力 (N):基于总重估算,推力 ≈ 重量 * 推重比
|
||||
# 巡飞弹推重比通常 0.2-0.5,但重型滑翔弹药不具备大推力发动机
|
||||
thrust_ratio = 0.15 + (max_speed / 150) * 0.20
|
||||
thrust_ratio = min(thrust_ratio, 0.3) # 上限
|
||||
estimated_thrust_n = round(total_weight * 9.8 * thrust_ratio, 1)
|
||||
|
||||
# 最小作战高度 (m):巡飞弹通常 50-500m
|
||||
if cruise_speed > 0:
|
||||
estimated_min_alt = round(max(30, min(300, cruise_speed * 0.8)), 0)
|
||||
else:
|
||||
estimated_min_alt = round(max(30, min(300, max_speed * 2.5)), 0)
|
||||
|
||||
# 最大作战高度 (m):优先用 ceiling_altitude_m,否则估算
|
||||
if ceiling > 0:
|
||||
estimated_max_alt = ceiling
|
||||
else:
|
||||
estimated_max_alt = round(max(500, min(10000, endurance * 15)), 0)
|
||||
|
||||
# ── 5. 制导系统评分 (1-10) ──
|
||||
guidance_system_score = calculate_guidance_score(guidance_system, estimated_accuracy)
|
||||
|
||||
# ── 6. 战斗部威力评分 (1-10) ──
|
||||
warhead_power_score = calculate_warhead_score(warhead_type, warhead_weight)
|
||||
|
||||
updates.append((
|
||||
length_width_ratio, weight_range_ratio, speed_weight_ratio,
|
||||
guidance_system_score, warhead_power_score,
|
||||
estimated_accuracy, estimated_datalink,
|
||||
estimated_engine_kw, estimated_thrust_n,
|
||||
estimated_min_alt, estimated_max_alt,
|
||||
row['id']
|
||||
))
|
||||
|
||||
if updates:
|
||||
cursor.executemany("""
|
||||
UPDATE loitering_munition_params
|
||||
SET length_width_ratio = ?, weight_range_ratio = ?,
|
||||
speed_weight_ratio = ?, guidance_system_score = ?,
|
||||
warhead_power_score = ?,
|
||||
guidance_accuracy_m = ?, datalink_range_km = ?,
|
||||
engine_power_kw = ?, engine_thrust_n = ?,
|
||||
min_altitude_m = ?, max_altitude_m = ?
|
||||
WHERE id = ?
|
||||
""", updates)
|
||||
logger.info(f"巡飞弹:更新了 {len(updates)} 条记录")
|
||||
|
||||
return len(updates)
|
||||
|
||||
|
||||
def main():
|
||||
init_db()
|
||||
conn = None
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
rc = calculate_rocket_features(conn)
|
||||
lc = calculate_loitering_features(conn)
|
||||
conn.commit()
|
||||
print(f"\n完成!")
|
||||
print(f" 火箭炮:{rc} 条记录已更新")
|
||||
print(f" 巡飞弹:{lc} 条记录已更新")
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
logger.error(f"特征计算失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -259,7 +259,15 @@ def analyze_features():
|
||||
'mobility_score': [float(item['mobility_score']) if item['mobility_score'] is not None else 0 for item in equipment_data],
|
||||
'combat_readiness_score': [float(item['combat_readiness_score']) if item['combat_readiness_score'] is not None else 0 for item in equipment_data],
|
||||
'deployment_score': [float(item['deployment_score']) if item['deployment_score'] is not None else 0 for item in equipment_data],
|
||||
'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data]
|
||||
'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data],
|
||||
'rate_of_fire': [float(item['rate_of_fire']) if item['rate_of_fire'] is not None else 0 for item in equipment_data],
|
||||
'max_range_km': [float(item['max_range_km']) if item['max_range_km'] is not None else 0 for item in equipment_data],
|
||||
'rocket_length_m': [float(item['rocket_length_m']) if item['rocket_length_m'] is not None else 0 for item in equipment_data],
|
||||
'rocket_diameter_mm': [float(item['rocket_diameter_mm']) if item['rocket_diameter_mm'] is not None else 0 for item in equipment_data],
|
||||
'rocket_weight_kg': [float(item['rocket_weight_kg']) if item['rocket_weight_kg'] is not None else 0 for item in equipment_data],
|
||||
'speed_kmh': [float(item['speed_kmh']) if item['speed_kmh'] is not None else 0 for item in equipment_data],
|
||||
'power_hp': [float(item['power_hp']) if item['power_hp'] is not None else 0 for item in equipment_data],
|
||||
'travel_range_km': [float(item['travel_range_km']) if item['travel_range_km'] is not None else 0 for item in equipment_data],
|
||||
}
|
||||
analysis_result.update(rocket_data)
|
||||
else:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user