refactor sect in vue
This commit is contained in:
@@ -120,6 +120,23 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
_action_cd_last_months: dict[str, int] = field(default_factory=dict)
|
||||
# 不缓存 effects;实时从宗门与功法合并
|
||||
|
||||
def join_sect(self, sect: Sect, rank: "SectRank") -> None:
|
||||
"""加入宗门"""
|
||||
if self.sect:
|
||||
self.leave_sect()
|
||||
self.sect = sect
|
||||
self.sect_rank = rank
|
||||
sect.add_member(self)
|
||||
# 更新阵营倾向为宗门阵营(如果之前是中立或不同)- 可选,视设计而定
|
||||
# 这里暂不强制修改个人阵营,除非完全不兼容
|
||||
|
||||
def leave_sect(self) -> None:
|
||||
"""退出宗门"""
|
||||
if self.sect:
|
||||
self.sect.remove_member(self)
|
||||
self.sect = None
|
||||
self.sect_rank = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""
|
||||
在Avatar创建后自动初始化tile和HP
|
||||
@@ -142,6 +159,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
if self.technique is None:
|
||||
self.technique = get_technique_by_sect(self.sect)
|
||||
|
||||
# 确保宗门引用同步
|
||||
if self.sect:
|
||||
self.sect.add_member(self)
|
||||
|
||||
# 若未设定阵营,则依据宗门/无门无派规则设置,避免后续为 None
|
||||
if self.alignment is None:
|
||||
if self.sect is not None:
|
||||
@@ -772,6 +793,16 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
|
||||
rank_name = get_rank_display_name(self.sect_rank, self.sect)
|
||||
return f"{self.sect.name}{rank_name}"
|
||||
|
||||
def get_sect_rank_name(self) -> str:
|
||||
"""
|
||||
获取宗门职位的显示名称
|
||||
"""
|
||||
if self.sect is None or self.sect_rank is None:
|
||||
return "散修"
|
||||
|
||||
from src.classes.sect_ranks import get_rank_display_name
|
||||
return get_rank_display_name(self.sect_rank, self.sect)
|
||||
|
||||
def set_relation(self, other: "Avatar", relation: Relation) -> None:
|
||||
"""
|
||||
设置与另一个角色的关系。
|
||||
|
||||
@@ -358,7 +358,7 @@ class CultivateRegion(Region):
|
||||
info = super().get_structured_info()
|
||||
info["type_name"] = "修炼区域"
|
||||
info["essence"] = {
|
||||
"type": self.essence_type.value,
|
||||
"type": str(self.essence_type), # EssenceType.__str__ 已经返回中文名
|
||||
"density": self.essence_density
|
||||
}
|
||||
return info
|
||||
|
||||
@@ -7,6 +7,11 @@ from src.utils.df import game_configs, get_str, get_float, get_int
|
||||
from src.classes.effect import load_effect_from_str
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
from src.classes.technique import Technique
|
||||
from src.classes.sect_ranks import SectRank
|
||||
|
||||
"""
|
||||
宗门、宗门总部基础数据。
|
||||
@@ -46,6 +51,25 @@ class Sect:
|
||||
effect_desc: str = ""
|
||||
# 宗门自定义职位名称(可选):SectRank -> 名称
|
||||
rank_names: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# 运行时成员列表:Avatar ID -> Avatar
|
||||
members: dict[str, "Avatar"] = field(default_factory=dict, init=False)
|
||||
# 功法对象列表:Technique
|
||||
techniques: list["Technique"] = field(default_factory=list, init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.members = {}
|
||||
self.techniques = []
|
||||
|
||||
def add_member(self, avatar: "Avatar") -> None:
|
||||
"""添加成员到宗门"""
|
||||
if avatar.id not in self.members:
|
||||
self.members[avatar.id] = avatar
|
||||
|
||||
def remove_member(self, avatar: "Avatar") -> None:
|
||||
"""从宗门移除成员"""
|
||||
if avatar.id in self.members:
|
||||
del self.members[avatar.id]
|
||||
|
||||
def get_info(self) -> str:
|
||||
hq = self.headquarter
|
||||
@@ -73,14 +97,66 @@ class Sect:
|
||||
|
||||
def get_structured_info(self) -> dict:
|
||||
hq = self.headquarter
|
||||
|
||||
from src.classes.sect_ranks import RANK_ORDER
|
||||
from src.server.main import resolve_avatar_pic_id
|
||||
from src.classes.technique import techniques_by_name
|
||||
|
||||
# 成员列表:直接从 self.members 获取
|
||||
members_list = []
|
||||
for a in self.members.values():
|
||||
rank_enum = getattr(a, "sect_rank", None)
|
||||
sort_val = 999
|
||||
if rank_enum and rank_enum in RANK_ORDER:
|
||||
sort_val = RANK_ORDER[rank_enum]
|
||||
|
||||
members_list.append({
|
||||
"id": str(a.id),
|
||||
"name": a.name,
|
||||
"pic_id": resolve_avatar_pic_id(a),
|
||||
"gender": a.gender.value if hasattr(a.gender, "value") else "male",
|
||||
"rank": a.get_sect_rank_name(),
|
||||
"realm": a.cultivation_progress.realm.value if hasattr(a, 'cultivation_progress') else "未知",
|
||||
"_sort_val": sort_val
|
||||
})
|
||||
# 按职位排序
|
||||
members_list.sort(key=lambda x: x["_sort_val"])
|
||||
# 清理排序字段
|
||||
for m in members_list:
|
||||
del m["_sort_val"]
|
||||
|
||||
# 填充 techniques
|
||||
# 使用 technique_names 从全局字典中查找对应的 Technique 对象并序列化
|
||||
techniques_data = []
|
||||
for t_name in self.technique_names:
|
||||
t_obj = techniques_by_name.get(t_name)
|
||||
if t_obj:
|
||||
techniques_data.append(t_obj.get_structured_info())
|
||||
else:
|
||||
# Fallback for missing techniques: create a minimal structure
|
||||
techniques_data.append({
|
||||
"name": t_name,
|
||||
"desc": "(未知功法)",
|
||||
"grade": "",
|
||||
"color": (200, 200, 200), # Gray
|
||||
"attribute": "",
|
||||
"effect_desc": ""
|
||||
})
|
||||
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"desc": self.desc,
|
||||
"alignment": self.alignment.value,
|
||||
"alignment": str(self.alignment), # 直接返回中文
|
||||
"style": self.member_act_style,
|
||||
"hq_name": hq.name,
|
||||
"hq_desc": hq.desc,
|
||||
"effect_desc": self.effect_desc,
|
||||
"techniques": techniques_data,
|
||||
# 兼容旧字段,如果前端还要用的话(建议迁移后废弃)
|
||||
"technique_names": self.technique_names,
|
||||
"preferred_weapon": self.preferred_weapon,
|
||||
"members": members_list
|
||||
}
|
||||
|
||||
def _split_names(value: object) -> list[str]:
|
||||
|
||||
@@ -11,6 +11,7 @@ class SectRegion(Region):
|
||||
无额外操作或属性。
|
||||
"""
|
||||
sect_name: str
|
||||
sect_id: int = -1
|
||||
image_path: str | None = None
|
||||
|
||||
def get_region_type(self) -> str:
|
||||
@@ -28,4 +29,5 @@ class SectRegion(Region):
|
||||
info = super().get_structured_info()
|
||||
info["type_name"] = "宗门驻地"
|
||||
info["sect_name"] = self.sect_name
|
||||
info["sect_id"] = self.sect_id
|
||||
return info
|
||||
|
||||
@@ -133,6 +133,7 @@ def add_sect_headquarters(game_map: Map, enabled_sects: list[Sect]):
|
||||
north_west_cor=f"{nw_x},{nw_y}",
|
||||
south_east_cor=f"{se_x},{se_y}",
|
||||
sect_name=sect_name_for_region,
|
||||
sect_id=sect.id,
|
||||
image_path=str(getattr(sect.headquarter, "image", None)),
|
||||
)
|
||||
game_map.regions[region.id] = region
|
||||
@@ -372,6 +373,8 @@ def _scale_loaded_regions(game_map: Map) -> None:
|
||||
# SectRegion 透传特有字段
|
||||
if hasattr(region, "sect_name"):
|
||||
params["sect_name"] = getattr(region, "sect_name")
|
||||
if hasattr(region, "sect_id"):
|
||||
params["sect_id"] = getattr(region, "sect_id")
|
||||
if hasattr(region, "image_path"):
|
||||
params["image_path"] = getattr(region, "image_path")
|
||||
new_region = cls(**params) # 重新构建以刷新 cors/area/center
|
||||
|
||||
@@ -615,12 +615,19 @@ def get_detail_info(
|
||||
target = regions.get(int(target_id))
|
||||
except (ValueError, TypeError):
|
||||
target = None
|
||||
elif target_type == "sect":
|
||||
try:
|
||||
sid = int(target_id)
|
||||
target = sects_by_id.get(sid)
|
||||
except (ValueError, TypeError):
|
||||
target = None
|
||||
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail="Target not found")
|
||||
|
||||
if hasattr(target, "get_structured_info"):
|
||||
return target.get_structured_info()
|
||||
info = target.get_structured_info()
|
||||
return info
|
||||
else:
|
||||
# 回退到 hover info 如果没有结构化信息
|
||||
if hasattr(target, "get_hover_info"):
|
||||
|
||||
@@ -124,6 +124,24 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L
|
||||
# 将所有avatar添加到world
|
||||
world.avatar_manager.avatars = all_avatars
|
||||
|
||||
# 重建宗门成员关系与功法列表
|
||||
from src.classes.technique import techniques_by_name
|
||||
|
||||
# 1. 重建成员
|
||||
for avatar in all_avatars.values():
|
||||
if avatar.sect:
|
||||
# 存档中 avatar.sect 已经被 Avatar.from_save_dict 恢复为 Sect 对象引用
|
||||
# 但 Sect.members 是空的(因为 Sect 是重新加载配置生成的)
|
||||
avatar.sect.add_member(avatar)
|
||||
|
||||
# 2. 重建功法对象列表(兼容旧存档)
|
||||
for sect in existed_sects:
|
||||
if not sect.techniques and sect.technique_names:
|
||||
sect.techniques = []
|
||||
for t_name in sect.technique_names:
|
||||
if t_name in techniques_by_name:
|
||||
sect.techniques.append(techniques_by_name[t_name])
|
||||
|
||||
# 重建事件历史
|
||||
events_data = save_data.get("events", [])
|
||||
for event_data in events_data:
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AvatarDetail, EffectEntity } from '@/types/core';
|
||||
import { formatHp, formatAge } from '@/utils/formatters/number';
|
||||
import StatItem from './components/StatItem.vue';
|
||||
import EntityRow from './components/EntityRow.vue';
|
||||
import RelationRow from './components/RelationRow.vue';
|
||||
import TagList from './components/TagList.vue';
|
||||
import SecondaryPopup from './components/SecondaryPopup.vue';
|
||||
import { gameApi } from '@/api/game';
|
||||
@@ -30,6 +31,10 @@ function jumpToAvatar(id: string) {
|
||||
uiStore.select('avatar', id);
|
||||
}
|
||||
|
||||
function jumpToSect(id: string) {
|
||||
uiStore.select('sect', id);
|
||||
}
|
||||
|
||||
async function handleSetObjective() {
|
||||
if (!objectiveContent.value.trim()) return;
|
||||
try {
|
||||
@@ -85,7 +90,7 @@ async function handleClearObjective() {
|
||||
label="宗门"
|
||||
:value="data.sect?.name || '散修'"
|
||||
:sub-value="data.sect?.rank"
|
||||
:on-click="data.sect ? () => showDetail(data.sect) : undefined"
|
||||
:on-click="data.sect ? () => jumpToSect(data.sect.id) : undefined"
|
||||
/>
|
||||
|
||||
<StatItem
|
||||
@@ -154,18 +159,14 @@ async function handleClearObjective() {
|
||||
<div class="section" v-if="data.relations?.length">
|
||||
<div class="section-title">关系</div>
|
||||
<div class="list-container">
|
||||
<div
|
||||
<RelationRow
|
||||
v-for="rel in data.relations"
|
||||
:key="rel.target_id"
|
||||
class="relation-item"
|
||||
:name="rel.name"
|
||||
:meta="rel.relation"
|
||||
:sub="`${rel.sect} · ${rel.realm}`"
|
||||
@click="jumpToAvatar(rel.target_id)"
|
||||
>
|
||||
<div class="rel-head">
|
||||
<span class="rel-name">{{ rel.name }}</span>
|
||||
<span class="rel-type">{{ rel.relation }}</span>
|
||||
</div>
|
||||
<div class="rel-sub">{{ rel.sect }} · {{ rel.realm }}</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -283,42 +284,6 @@ async function handleClearObjective() {
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
/* Relations */
|
||||
.relation-item {
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-left: 2px solid #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.relation-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left-color: #666;
|
||||
}
|
||||
|
||||
.rel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rel-name {
|
||||
font-size: 13px;
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rel-type {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.rel-sub {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: absolute;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useUiStore } from '../../../../stores/ui';
|
||||
// Sub-components
|
||||
import AvatarDetailView from './AvatarDetail.vue';
|
||||
import RegionDetailView from './RegionDetail.vue';
|
||||
import SectDetailView from './SectDetail.vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const panelRef = ref<HTMLElement | null>(null);
|
||||
@@ -15,6 +16,7 @@ let lastOpenAt = 0;
|
||||
const currentComponent = computed(() => {
|
||||
if (uiStore.selectedTarget?.type === 'avatar') return AvatarDetailView;
|
||||
if (uiStore.selectedTarget?.type === 'region') return RegionDetailView;
|
||||
if (uiStore.selectedTarget?.type === 'sect') return SectDetailView;
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ import { ref } from 'vue';
|
||||
import type { RegionDetail, EffectEntity } from '@/types/core';
|
||||
import EntityRow from './components/EntityRow.vue';
|
||||
import SecondaryPopup from './components/SecondaryPopup.vue';
|
||||
import { translateElement } from '@/utils/formatters/dictionary';
|
||||
import { useUiStore } from '@/stores/ui';
|
||||
|
||||
defineProps<{
|
||||
data: RegionDetail;
|
||||
}>();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const secondaryItem = ref<EffectEntity | null>(null);
|
||||
|
||||
function showDetail(item: EffectEntity | undefined) {
|
||||
@@ -16,6 +17,10 @@ function showDetail(item: EffectEntity | undefined) {
|
||||
secondaryItem.value = item;
|
||||
}
|
||||
}
|
||||
|
||||
function jumpToSect(id: number) {
|
||||
uiStore.select('sect', id.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,13 +34,18 @@ function showDetail(item: EffectEntity | undefined) {
|
||||
<div class="section">
|
||||
<div class="section-title">{{ data.type_name }}</div>
|
||||
<div class="desc">{{ data.desc }}</div>
|
||||
|
||||
<!-- Sect Jump Button -->
|
||||
<div v-if="data.sect_id" class="actions">
|
||||
<button class="btn primary" @click="jumpToSect(data.sect_id!)">查看宗门详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Essence -->
|
||||
<div class="section" v-if="data.essence">
|
||||
<div class="section-title">灵气环境</div>
|
||||
<div class="essence-info">
|
||||
{{ translateElement(data.essence.type) }}行灵气 · 浓度 {{ data.essence.density }}
|
||||
{{ data.essence.type }}行灵气 · 浓度 {{ data.essence.density }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -110,4 +120,34 @@ function showDetail(item: EffectEntity | undefined) {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ccc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #177ddc;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: #1890ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
182
web/src/components/game/panels/info/SectDetail.vue
Normal file
182
web/src/components/game/panels/info/SectDetail.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { SectDetail, EffectEntity } from '@/types/core';
|
||||
import { useUiStore } from '@/stores/ui';
|
||||
import StatItem from './components/StatItem.vue';
|
||||
import SecondaryPopup from './components/SecondaryPopup.vue';
|
||||
import EntityRow from './components/EntityRow.vue';
|
||||
import RelationRow from './components/RelationRow.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: SectDetail;
|
||||
}>();
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const secondaryItem = ref<EffectEntity | null>(null);
|
||||
|
||||
function jumpToAvatar(id: string) {
|
||||
uiStore.select('avatar', id);
|
||||
}
|
||||
|
||||
function showDetail(item: EffectEntity | undefined) {
|
||||
if (item) {
|
||||
secondaryItem.value = item;
|
||||
}
|
||||
}
|
||||
|
||||
const alignmentText = props.data.alignment;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sect-detail">
|
||||
<SecondaryPopup
|
||||
:item="secondaryItem"
|
||||
@close="secondaryItem = null"
|
||||
/>
|
||||
|
||||
<div class="content-scroll">
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid">
|
||||
<StatItem label="阵营" :value="alignmentText" :class="data.alignment" />
|
||||
<StatItem label="风格" :value="data.style" />
|
||||
<StatItem label="擅长" :value="data.preferred_weapon || '无'" />
|
||||
<StatItem label="成员" :value="data.members?.length || 0" />
|
||||
</div>
|
||||
|
||||
<!-- Intro -->
|
||||
<div class="section">
|
||||
<div class="section-title">宗门简介</div>
|
||||
<div class="text-content">{{ data.desc }}</div>
|
||||
</div>
|
||||
|
||||
<!-- HQ -->
|
||||
<div class="section">
|
||||
<div class="section-title">驻地:{{ data.hq_name }}</div>
|
||||
<div class="text-content">{{ data.hq_desc }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Effects -->
|
||||
<div class="section">
|
||||
<div class="section-title">宗门加成</div>
|
||||
<div class="text-content highlight">{{ data.effect_desc || '无特殊加成' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Techniques -->
|
||||
<div class="section">
|
||||
<div class="section-title">独门绝学</div>
|
||||
<div class="list-container" v-if="data.techniques?.length">
|
||||
<EntityRow
|
||||
v-for="t in data.techniques"
|
||||
:key="t.id"
|
||||
:item="t"
|
||||
@click="showDetail(t)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="text-content">无</div>
|
||||
</div>
|
||||
|
||||
<!-- Members -->
|
||||
<div class="section" v-if="data.members?.length">
|
||||
<div class="section-title">门下弟子</div>
|
||||
<div class="list-container">
|
||||
<RelationRow
|
||||
v-for="m in data.members"
|
||||
:key="m.id"
|
||||
:name="m.name"
|
||||
:meta="m.rank"
|
||||
:sub="m.realm"
|
||||
@click="jumpToAvatar(m.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sect-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.text-content.highlight {
|
||||
color: #e6f7ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Tech List */
|
||||
.tech-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
font-size: 13px;
|
||||
color: #eee;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tech-item.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tech-item.clickable:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tech-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
name: string;
|
||||
meta?: string;
|
||||
sub?: string;
|
||||
}>();
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relation-row" @click="$emit('click')">
|
||||
<div class="rel-head">
|
||||
<span class="rel-name">{{ name }}</span>
|
||||
<span v-if="meta" class="rel-type">{{ meta }}</span>
|
||||
</div>
|
||||
<div v-if="sub" class="rel-sub">{{ sub }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.relation-row {
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-left: 2px solid #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.relation-row:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left-color: #666;
|
||||
}
|
||||
|
||||
.rel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rel-name {
|
||||
font-size: 13px;
|
||||
color: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rel-type {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.rel-sub {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,6 +85,28 @@ export interface SectInfo extends EffectEntity {
|
||||
rank: string;
|
||||
}
|
||||
|
||||
export interface SectMember {
|
||||
id: string;
|
||||
name: string;
|
||||
pic_id: number;
|
||||
gender: string;
|
||||
rank: string;
|
||||
realm: string;
|
||||
}
|
||||
|
||||
export interface SectDetail extends EntityBase {
|
||||
desc: string;
|
||||
alignment: string;
|
||||
style: string;
|
||||
hq_name: string;
|
||||
hq_desc: string;
|
||||
effect_desc: string;
|
||||
technique_names?: string[]; // Deprecated
|
||||
techniques: EffectEntity[];
|
||||
preferred_weapon: string;
|
||||
members: SectMember[];
|
||||
}
|
||||
|
||||
export interface RelationInfo {
|
||||
target_id: string;
|
||||
name: string;
|
||||
@@ -106,6 +128,7 @@ export interface RegionDetail extends EntityBase {
|
||||
desc: string;
|
||||
type: string;
|
||||
type_name: string; // 中文类型名
|
||||
sect_id?: number;
|
||||
|
||||
essence?: {
|
||||
type: string;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 字典映射与翻译工具
|
||||
*/
|
||||
|
||||
const ELEMENT_MAP: Record<string, string> = {
|
||||
'metal': '金',
|
||||
'wood': '木',
|
||||
'water': '水',
|
||||
'fire': '火',
|
||||
'earth': '土',
|
||||
'none': '无',
|
||||
// Capitalized versions just in case
|
||||
'Metal': '金',
|
||||
'Wood': '木',
|
||||
'Water': '水',
|
||||
'Fire': '火',
|
||||
'Earth': '土',
|
||||
'None': '无',
|
||||
// Map internal key names if they leak (e.g. GOLD from RootElement)
|
||||
'gold': '金',
|
||||
'Gold': '金',
|
||||
'GOLD': '金'
|
||||
};
|
||||
|
||||
export function translateElement(type: string): string {
|
||||
return ELEMENT_MAP[type] || type;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user