|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
import { useCharacterStore } from '@/stores/character'
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
import { ElMessage, ElProgress } from 'element-plus'
|
|
|
|
|
import Particles from '@/components/Particles/Particles.vue'
|
|
|
|
|
import GlareHover from '@/components/GlareHover/GlareHover.vue'
|
|
|
|
|
import ElectricBorder from '@/components/ElectricBorder/ElectricBorder.vue'
|
|
|
|
|
import GlitchText from '@/components/GlitchText/GlitchText.vue'
|
|
|
|
|
import TextType from '@/components/TextType/TextType.vue'
|
|
|
|
|
import StarBorder from '@/components/StarBorder/StarBorder.vue'
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const characterStore = useCharacterStore()
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
|
|
|
|
const showCreateDialog = ref(false)
|
|
|
|
|
const newCharacterName = ref('')
|
|
|
|
|
const newCharacterProfessionId = ref<number | null>(null)
|
|
|
|
|
const showProfessionSelection = ref(false)
|
|
|
|
|
const errorMsg = ref('')
|
|
|
|
|
const deleteDialogVisible = ref(false)
|
|
|
|
|
const characterToDelete = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await characterStore.fetchCharacters()
|
|
|
|
|
await characterStore.fetchProfessions()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const openCreateDialog = () => {
|
|
|
|
|
newCharacterName.value = ''
|
|
|
|
|
newCharacterProfessionId.value = null
|
|
|
|
|
showProfessionSelection.value = false
|
|
|
|
|
errorMsg.value = ''
|
|
|
|
|
showCreateDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleShowProfessionSelection = () => {
|
|
|
|
|
if (newCharacterName.value && newCharacterName.value.trim().length >= 2) {
|
|
|
|
|
showProfessionSelection.value = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSelectProfession = (professionId: number) => {
|
|
|
|
|
newCharacterProfessionId.value = professionId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getRateBarWidth = (rate: number) => {
|
|
|
|
|
return Math.min(rate * 50, 100)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getRateBarColor = (rate: number) => {
|
|
|
|
|
return rate > 1.0 ? 'red' : 'green'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCreateCharacter = async () => {
|
|
|
|
|
errorMsg.value = ''
|
|
|
|
|
|
|
|
|
|
if (!newCharacterName.value || newCharacterName.value.trim().length < 2) {
|
|
|
|
|
errorMsg.value = '角色名称至少2个字符'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!newCharacterProfessionId.value) {
|
|
|
|
|
errorMsg.value = '请选择职业'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const success = await characterStore.createCharacter(newCharacterName.value.trim(), newCharacterProfessionId.value)
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
showCreateDialog.value = false
|
|
|
|
|
newCharacterName.value = ''
|
|
|
|
|
newCharacterProfessionId.value = null
|
|
|
|
|
showProfessionSelection.value = false
|
|
|
|
|
} else {
|
|
|
|
|
errorMsg.value = '创建失败,可能名称已存在或已达角色数量上限(3个)'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSelectCharacter = async (characterId: number) => {
|
|
|
|
|
const success = await characterStore.selectCharacter(characterId)
|
|
|
|
|
if (success) {
|
|
|
|
|
router.push('/game')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteCharacter = (characterId: number, event: Event) => {
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
characterToDelete.value = characterId
|
|
|
|
|
deleteDialogVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmDelete = async () => {
|
|
|
|
|
if (characterToDelete.value) {
|
|
|
|
|
var bo = await characterStore.deleteCharacter(characterToDelete.value)
|
|
|
|
|
if (bo == true) {
|
|
|
|
|
deleteDialogVisible.value = false
|
|
|
|
|
characterToDelete.value = null
|
|
|
|
|
ElMessage.success('删除成功了,如果有一天你后悔了,记得找我')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteCharacter = async (characterId: number) => {
|
|
|
|
|
await characterStore.deleteCharacter(characterId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleLogout = () => {
|
|
|
|
|
authStore.logout()
|
|
|
|
|
characterStore.clearCurrentCharacter()
|
|
|
|
|
window.location.href = '/login'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
|
|
|
if (!dateStr) return '首次登录'
|
|
|
|
|
const date = new Date(dateStr)
|
|
|
|
|
return date.toLocaleDateString('zh-CN')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getExpProgress = (currentExp: number, nextLevelMinExp?: number) => {
|
|
|
|
|
if (!nextLevelMinExp) return 0
|
|
|
|
|
return Math.min(100, (currentExp / nextLevelMinExp) * 100)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatNumber = (num: number) => {
|
|
|
|
|
return Math.floor(num).toLocaleString('zh-CN')
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="character-page">
|
|
|
|
|
<Particles :particle-count="50" :particle-colors="['#ffffff', '#cccccc']" class="particles-bg" />
|
|
|
|
|
|
|
|
|
|
<div class="character-container">
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<TextType :text="['选 择 角 色']" :typingSpeed="75" :pauseDuration="1500" :showCursor="false" cursorCharacter="|"
|
|
|
|
|
style="font-size: 1.5rem; font-weight: bold;" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="character-list">
|
|
|
|
|
<div v-for="char in characterStore.characters" :key="char.id" class="character-card"
|
|
|
|
|
@click="handleSelectCharacter(char.id)">
|
|
|
|
|
<div class="character-info">
|
|
|
|
|
<div class="character-name">{{ char.name }}</div>
|
|
|
|
|
<div class="character-level">{{ char.levelName }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="character-stats">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-label">生命</span>
|
|
|
|
|
<span class="stat-value">{{ formatNumber(char.maxHP) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-label">攻击</span>
|
|
|
|
|
<span class="stat-value">{{ formatNumber(char.attack) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-label">防御</span>
|
|
|
|
|
<span class="stat-value">{{ formatNumber(char.defend) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-label">暴击</span>
|
|
|
|
|
<span class="stat-value">{{ Math.floor(char.criticalRate * 100) }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="character-basic-stats">
|
|
|
|
|
<div v-if="char.professionName">
|
|
|
|
|
<span class="stat-label">职业</span>
|
|
|
|
|
<span class="stat-value" style="margin-left: 10px;">{{ char.professionName }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span class="stat-label">灵石</span>
|
|
|
|
|
<span class="stat-value" style="margin-left: 10px;">{{ formatNumber(char.money) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="character-exp-section">
|
|
|
|
|
<div class="exp-header">
|
|
|
|
|
<span class="stat-label">经验</span>
|
|
|
|
|
<span class="stat-value-small">{{ formatNumber(char.currentExp) }}/{{ formatNumber(char.nextLevelMinExp ||
|
|
|
|
|
0) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-progress :percentage="getExpProgress(char.currentExp, char.nextLevelMinExp)" color="#ff8844"
|
|
|
|
|
:show-text="false" :stroke-width="8" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="character-meta">
|
|
|
|
|
<span class="last-login">上次登录: {{ formatDate(char.lastLogin) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-button type="danger" dashed class="delete-btn" @click="handleDeleteCharacter(char.id, $event)">
|
|
|
|
|
删除
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 创建角色卡片 -->
|
|
|
|
|
<div v-if="characterStore.characters.length < 3" class="character-card create-card" @click="openCreateDialog">
|
|
|
|
|
<div class="create-icon">+</div>
|
|
|
|
|
<div class="create-text">创建角色</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="page-footer">
|
|
|
|
|
<StarBorder as="div" color="#e63d3d" speed="3s" :thickness="3">
|
|
|
|
|
<div class="btn-out">
|
|
|
|
|
退出登录
|
|
|
|
|
</div>
|
|
|
|
|
</StarBorder>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="deleteDialogVisible" title="确认删除" width="500" center>
|
|
|
|
|
<span style="color: #fff; text-align: center; display: block;">
|
|
|
|
|
你真的想删除这个角色吗?
|
|
|
|
|
</span>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
<el-button @click="deleteDialogVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="danger" @click="confirmDelete">
|
|
|
|
|
确定删除
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- 创建角色对话框 -->
|
|
|
|
|
<div v-if="showCreateDialog" class="dialog-overlay" @click="showCreateDialog = false">
|
|
|
|
|
<div class="dialog" @click.stop>
|
|
|
|
|
<h2 class="dialog-title">创建角色</h2>
|
|
|
|
|
|
|
|
|
|
<input v-model="newCharacterName" type="text" class="dialog-input" placeholder="输入角色名称" maxlength="20"
|
|
|
|
|
@input="showProfessionSelection = false; newCharacterProfessionId = null"
|
|
|
|
|
@keyup.enter="handleShowProfessionSelection" />
|
|
|
|
|
|
|
|
|
|
<!-- 选择职业按钮 -->
|
|
|
|
|
<button v-if="newCharacterName.trim().length >= 2 && !showProfessionSelection" class="select-profession-btn"
|
|
|
|
|
@click="handleShowProfessionSelection">
|
|
|
|
|
选择职业
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- 职业卡片列表 -->
|
|
|
|
|
<div v-if="showProfessionSelection" class="profession-cards">
|
|
|
|
|
<div v-for="profession in characterStore.professions" :key="profession.id" class="profession-card"
|
|
|
|
|
:class="{ selected: newCharacterProfessionId === profession.id }"
|
|
|
|
|
@click="handleSelectProfession(profession.id)">
|
|
|
|
|
<div class="profession-header">
|
|
|
|
|
<div class="profession-name">{{ profession.name }}</div>
|
|
|
|
|
<div class="profession-desc">{{ profession.description }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 系数条形图 -->
|
|
|
|
|
<div class="rate-bars">
|
|
|
|
|
<div class="rate-item">
|
|
|
|
|
<span class="rate-label">攻击</span>
|
|
|
|
|
<div class="bar-track">
|
|
|
|
|
<div class="bar-fill" :class="getRateBarColor(profession.attackRate)"
|
|
|
|
|
:style="{ width: getRateBarWidth(profession.attackRate) + '%' }"></div>
|
|
|
|
|
<div class="axis-line"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="rate-value" :class="getRateBarColor(profession.attackRate)">{{ profession.attackRate
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="rate-item">
|
|
|
|
|
<span class="rate-label">防御</span>
|
|
|
|
|
<div class="bar-track">
|
|
|
|
|
<div class="bar-fill" :class="getRateBarColor(profession.defendRate)"
|
|
|
|
|
:style="{ width: getRateBarWidth(profession.defendRate) + '%' }"></div>
|
|
|
|
|
<div class="axis-line"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="rate-value" :class="getRateBarColor(profession.defendRate)">{{ profession.defendRate
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="rate-item">
|
|
|
|
|
<span class="rate-label">生命</span>
|
|
|
|
|
<div class="bar-track">
|
|
|
|
|
<div class="bar-fill" :class="getRateBarColor(profession.healthRate)"
|
|
|
|
|
:style="{ width: getRateBarWidth(profession.healthRate) + '%' }"></div>
|
|
|
|
|
<div class="axis-line"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="rate-value" :class="getRateBarColor(profession.healthRate)">{{ profession.healthRate
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="rate-item">
|
|
|
|
|
<span class="rate-label">暴击</span>
|
|
|
|
|
<div class="bar-track">
|
|
|
|
|
<div class="bar-fill" :class="getRateBarColor(profession.criticalRate)"
|
|
|
|
|
:style="{ width: getRateBarWidth(profession.criticalRate) + '%' }"></div>
|
|
|
|
|
<div class="axis-line"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="rate-value" :class="getRateBarColor(profession.criticalRate)">{{ profession.criticalRate
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="newCharacterProfessionId === profession.id" class="selected-check">
|
|
|
|
|
✓ 已选择
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="errorMsg" class="error-message">{{ errorMsg }}</div>
|
|
|
|
|
|
|
|
|
|
<div class="dialog-actions">
|
|
|
|
|
<button class="cancel-btn" @click="showCreateDialog = false">取消</button>
|
|
|
|
|
<button class="confirm-btn" :disabled="!newCharacterProfessionId" @click="handleCreateCharacter">
|
|
|
|
|
创建
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.character-page {
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
background: #000000;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.particles-bg {
|
|
|
|
|
position: fixed !important;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-header {
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
letter-spacing: 0.15em;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
color: #999999;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-card {
|
|
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-card:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.15);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
padding-right: 60px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-name {
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-level {
|
|
|
|
|
color: #bbbbbb;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-stats {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
gap: 0;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-stats .stat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-stats .stat-item:last-child {
|
|
|
|
|
border-right: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-stats .stat-label {
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-stats .stat-value {
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-basic-stats {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-basic-stats .stat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
color: #888888;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-out {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.last-login {
|
|
|
|
|
color: #666666;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.character-exp-section {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.exp-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value-small {
|
|
|
|
|
color: #666666;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.delete-btn {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 12px;
|
|
|
|
|
right: 12px;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.create-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
border-style: dashed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.create-card:hover {
|
|
|
|
|
border-style: solid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.create-icon {
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
color: #666666;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.create-text {
|
|
|
|
|
color: #999999;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-footer {
|
|
|
|
|
margin-top: 32px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logout-text {
|
|
|
|
|
color: #666666;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Dialog */
|
|
|
|
|
.dialog-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog {
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
width: 90%;
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-title {
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
outline: none;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-input::placeholder {
|
|
|
|
|
color: #444444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-input:focus {
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.select-profession-btn {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.select-profession-btn:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-cards {
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-cards::-webkit-scrollbar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-card {
|
|
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-card:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-card.selected {
|
|
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
|
|
border-color: rgba(100, 200, 100, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-header {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-name {
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.profession-desc {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: #888888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-bars {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-label {
|
|
|
|
|
width: 32px;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
color: #888888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bar-track {
|
|
|
|
|
flex: 1;
|
|
|
|
|
height: 8px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bar-fill {
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bar-fill.green {
|
|
|
|
|
background: linear-gradient(90deg, #4ade80, #22c55e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bar-fill.red {
|
|
|
|
|
background: linear-gradient(90deg, #f87171, #ef4444);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.axis-line {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 50%;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 1px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-left: 1px dashed rgba(255, 255, 255, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-value {
|
|
|
|
|
width: 36px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-value.green {
|
|
|
|
|
color: #4ade80;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rate-value.red {
|
|
|
|
|
color: #f87171;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selected-check {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 8px;
|
|
|
|
|
right: 8px;
|
|
|
|
|
color: #4ade80;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.confirm-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-message {
|
|
|
|
|
color: #888888;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn,
|
|
|
|
|
.confirm-btn {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn {
|
|
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
color: #888888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.confirm-btn {
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.confirm-btn:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
}
|
|
|
|
|
</style>
|