Unverified Commit 5ffdbeb6 authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

deploy update (#355)

1、frontend vue+vite
2、share task & template
3、x264 rtc stream push
parent 39683e24
<script setup>
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { watch, onMounted } from 'vue'
// Props
const props = defineProps({
query: {
type: Object,
default: () => ({})
},
templateId: {
type: String,
default: null
}
})
const { t, locale } = useI18n()
const route = useRoute()
const router = useRouter()
import {
goToInspirationPage,
getVisibleInspirationPages,
getTemplateFileUrl,
handleThumbnailError,
inspirationSearchQuery,
selectedInspirationCategory,
inspirationItems,
InspirationCategories,
selectInspirationCategory,
handleInspirationSearch,
inspirationPaginationInfo,
inspirationCurrentPage,
previewTemplateDetail,
useTemplate,
applyTemplateImage,
applyTemplateAudio,
playVideo,
pauseVideo,
toggleVideoPlay,
onVideoLoaded,
onVideoError,
onVideoEnded,
openTemplateFromRoute,
copyShareLink
} from '../utils/other'
// 监听模板详情路由
watch(() => route.params.templateId, (newTemplateId) => {
if (newTemplateId && route.name === 'TemplateDetail') {
openTemplateFromRoute(newTemplateId)
}
}, { immediate: true })
// 路由监听和URL同步
watch(() => route.query, (newQuery) => {
// 同步URL参数到组件状态
if (newQuery.search) {
inspirationSearchQuery.value = newQuery.search
}
if (newQuery.category) {
selectedInspirationCategory.value = newQuery.category
}
if (newQuery.page) {
const page = parseInt(newQuery.page)
if (page > 0 && page !== inspirationCurrentPage.value) {
goToInspirationPage(page)
}
}
}, { immediate: true })
// 监听组件状态变化,同步到URL
watch([inspirationSearchQuery, selectedInspirationCategory, inspirationCurrentPage], () => {
const query = {}
if (inspirationSearchQuery.value) {
query.search = inspirationSearchQuery.value
}
if (selectedInspirationCategory.value && selectedInspirationCategory.value !== 'all') {
query.category = selectedInspirationCategory.value
}
if (inspirationCurrentPage.value > 1) {
query.page = inspirationCurrentPage.value.toString()
}
// 更新URL但不触发路由监听
router.replace({ query })
})
// 组件挂载时初始化
onMounted(() => {
// 确保URL参数正确同步
const query = route.query
if (query.search) {
inspirationSearchQuery.value = query.search
}
if (query.category) {
selectedInspirationCategory.value = query.category
}
if (query.page) {
const page = parseInt(query.page)
if (page > 0) {
goToInspirationPage(page)
}
}
})
</script>
<template>
<!-- 灵感广场区域 -->
<div class="flex-1 flex flex-col min-h-0 mobile-content">
<!-- 内容区域 -->
<div class="flex-1 overflow-y-auto p-6 content-area main-scrollbar">
<!-- 灵感广场功能区 -->
<div class="max-w-4xl mx-auto" id="inspiration-gallery">
<!-- 固定功能区 -->
<div class="flex-shrink-0 p-1">
<!-- 标题区域 -->
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-white mb-2">{{ t('inspirationGallery') }}</h1>
<p class="text-gray-400">{{ t('discoverCreativity') }}</p>
</div>
<!-- 搜索和筛选区域 -->
<div class="flex flex-col md:flex-row gap-4 mb-6">
<!-- 搜索框 -->
<div class="relative flex-1">
<i
class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none z-10"></i>
<input v-model="inspirationSearchQuery"
@keyup.enter="handleInspirationSearch"
@input="handleInspirationSearch"
class="w-full bg-dark-light border border-laser-purple/30 rounded-lg py-3 pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-laser-purple/50 transition-all focus:border-laser focus:shadow-laser"
:placeholder="t('searchInspiration')" type="text" />
</div>
<!-- 分类筛选 -->
<div class="flex gap-2 flex-wrap">
<button v-for="category in InspirationCategories" :key="category"
@click="selectInspirationCategory(category)"
:class="selectedInspirationCategory === category
? 'bg-laser-purple/20 text-white border-laser-purple/40'
: 'bg-dark-light text-gray-400 hover:text-white hover:bg-dark-light/80'"
class="px-4 py-2 rounded-lg border border-transparent transition-all duration-200 text-sm font-medium">
{{ category }}
</button>
</div>
</div>
<!-- 灵感广场分页组件 -->
<div v-if="inspirationPaginationInfo">
<div class="flex items-center justify-between text-xs text-gray-400">
<div class="flex items-center space-x-1 text-gray-500">
<span>{{ inspirationPaginationInfo.total }} {{ t('records') }}</span>
</div>
</div>
<div v-if="inspirationPaginationInfo.total_pages > 1" class="flex justify-center">
<nav class="isolate inline-flex -space-x-px rounded-md" aria-label="Pagination">
<!-- 上一页按钮 -->
<button @click="goToInspirationPage(inspirationCurrentPage - 1)"
:disabled="inspirationCurrentPage <= 1"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 inset-ring inset-ring-gray-700 hover:bg-white/5 focus:z-20 focus:outline-offset-0"
:class="{ 'opacity-50 cursor-not-allowed': inspirationCurrentPage <= 1 }"
:title="t('previousPage')">
<span class="sr-only">{{ t('previousPage') }}</span>
<i class="fas fa-chevron-left text-sm" aria-hidden="true"></i>
</button>
<!-- 页码按钮 -->
<template v-for="page in getVisibleInspirationPages()" :key="page">
<button v-if="page !== '...'" @click="goToInspirationPage(page)"
:class="[
'relative inline-flex items-center px-4 py-2 text-sm font-semibold focus:z-20 focus:outline-offset-0',
page === inspirationCurrentPage
? 'z-10 text-white focus-visible:outline-2 focus-visible:outline-offset-2 bg-laser-purple focus-visible:outline-laser-purple'
: 'text-gray-200 inset-ring inset-ring-gray-700 hover:bg-white/5'
]"
:aria-current="page === inspirationCurrentPage ? 'page' : undefined">
{{ page }}
</button>
<span v-else class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-400 inset-ring inset-ring-gray-700 focus:outline-offset-0">...</span>
</template>
<!-- 下一页按钮 -->
<button @click="goToInspirationPage(inspirationCurrentPage + 1)"
:disabled="inspirationCurrentPage >= inspirationPaginationInfo.total_pages"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 inset-ring inset-ring-gray-700 hover:bg-white/5 focus:z-20 focus:outline-offset-0"
:class="{ 'opacity-50 cursor-not-allowed': inspirationCurrentPage >= inspirationPaginationInfo.total_pages }"
:title="t('nextPage')">
<span class="sr-only">{{ t('nextPage') }}</span>
<i class="fas fa-chevron-right text-sm" aria-hidden="true"></i>
</button>
</nav>
</div>
</div>
<!-- 灵感内容网格 -->
<div class="flex-1 overflow-y-auto main-scrollbar min-h-0 p-4"
style="overflow-x: visible;">
<div class="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-3">
<!-- 灵感卡片 -->
<div v-for="item in inspirationItems" :key="item.task_id"
class="break-inside-avoid mb-3 group relative bg-dark-light rounded-xl overflow-hidden border border-gray-700/50 hover:border-laser-purple/40 transition-all duration-300 hover:shadow-laser/20">
<!-- 视频缩略图区域 -->
<div class="cursor-pointer bg-gray-800 relative flex flex-col"
@click="previewTemplateDetail(item)"
:title="t('viewTemplateDetail')">
<!-- 视频预览 -->
<video v-if="item?.outputs?.output_video"
:src="getTemplateFileUrl(item.outputs.output_video,'videos')"
:poster="getTemplateFileUrl(item.inputs.input_image,'images')"
class="w-full h-auto object-contain group-hover:scale-105 transition-transform duration-300"
preload="auto" playsinline webkit-playsinline
@mouseenter="playVideo($event)" @mouseleave="pauseVideo($event)"
@loadeddata="onVideoLoaded($event)"
@ended="onVideoEnded($event)"
@error="onVideoError($event)"></video>
<!-- 图片缩略图 -->
<img v-else
:src="getTemplateFileUrl(item.inputs.input_image,'images')"
:alt="item.params?.prompt || '模板图片'"
class="w-full h-auto object-contain group-hover:scale-105 transition-transform duration-300"
@error="handleThumbnailError" />
<!-- 移动端播放按钮 -->
<button v-if="item?.outputs?.output_video"
@click.stop="toggleVideoPlay($event)"
class="md:hidden absolute bottom-3 left-1/2 transform -translate-x-1/2 w-10 h-10 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white hover:bg-black/70 transition-colors z-20">
<i class="fas fa-play text-sm"></i>
</button>
<!-- 悬浮操作按钮(下方居中,仅桌面端) -->
<div
class="hidden md:flex absolute bottom-3 left-1/2 transform -translate-x-1/2 items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10 w-full">
<div class="flex space-x-2 pointer-events-auto">
<button @click.stop="applyTemplateImage(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('applyImage')">
<i class="fas fa-image text-sm"></i>
</button>
<button @click.stop="applyTemplateAudio(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('applyAudio')">
<i class="fas fa-music text-sm"></i>
</button>
<button @click.stop="useTemplate(item)"
class="w-10 h-10 rounded-full bg-laser-purple backdrop-blur-sm flex items-center justify-center text-white hover:bg-laser-purple transition-colors"
:title="t('useTemplate')">
<i class="fas fa-clone text-sm"></i>
</button>
<button @click.stop="copyShareLink(item.task_id, 'template')"
class="w-10 h-10 rounded-full bg-blue-500 backdrop-blur-sm flex items-center justify-center text-white hover:bg-blue-600 transition-colors"
:title="t('shareTemplate')">
<i class="fas fa-share-alt text-sm"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { switchToCreateView, switchToProjectsView, switchToInspirationView } from '../utils/other'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 左侧功能区 -->
<div class="relative w-20 pl-5 flex flex-col z-10">
<!-- 功能导航 -->
<div class="p-2 flex flex-col justify-center h-full mobile-nav-buttons" style="margin-top: -10vh;">
<nav class="lg:space-y-3 md:space-y-3 sm:space-x-3 flex flex-col">
<!-- 生成视频功能 -->
<div
@click="switchToCreateView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
hover:shadow-laser-purple/20 mobile-nav-btn"
:class="$route.path === '/generate'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20'
: 'text-gray-400 hover:text-white hover:bg-dark-light hover:border-laser-purple/30'"
:title="t('generateVideo')">
<i class="fi fi-sr-add text-3xl transition-transform duration-300 group-hover:animate-pulse"></i>
</div>
<!-- 我的项目功能 -->
<div
@click="switchToProjectsView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
hover:shadow-laser-purple/20 mobile-nav-btn"
:class="$route.path === '/projects'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20'
: 'text-gray-400 hover:text-white hover:bg-dark-light hover:border-laser-purple/30'"
:title="t('myProjects')">
<i class="fi fi-br-house-chimney-heart text-3xl transition-transform duration-300 group-hover:animate-pulse"></i>
</div>
<!-- 灵感广场功能 -->
<div
@click="switchToInspirationView"
class="w-18 h-18 flex items-center justify-center rounded-lg transition-all duration-300
font-medium text-sm border border-transparent hover:scale-105 hover:shadow-lg
hover:shadow-laser-purple/40 mobile-nav-btn"
:class="$route.path === '/inspirations'
? 'bg-laser-purple/40 text-white border-laser-purple/40 shadow-lg shadow-laser-purple/20'
: 'text-gray-400 hover:text-white hover:bg-dark-light hover:border-laser-purple/30'"
:title="t('inspiration')">
<i class="fi fi-sr-sparkles text-3xl transition-transform duration-300 group-hover:animate-pulse"></i>
</div>
</nav>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 加载状态 -->
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-laser-purple mx-auto mb-4"></div>
<p class="text-gray-400">{{ t('loading') }}</p>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
import { // 登录相关
loginWithGitHub,
loginWithGoogle,
loginWithSms,
phoneNumber,
verifyCode,
smsCountdown,
showSmsForm,
sendSmsCode,
handleLoginCallback,
handlePhoneEnter,
handleVerifyCodeEnter,
toggleSmsLogin,
isLoggedIn,
loginLoading,
isLoading,
initLoading,
downloadLoading} from '../utils/other'
import { ref } from 'vue';
import { useRouter } from 'vue-router'
const router = useRouter();
</script>
<template>
<div class="login-card">
<div class="card-body text-center p-8">
<!-- Logo和标题 -->
<div>
<div class="login-logo">
<i class="fas fa-film me-3"></i>
LightX2V
</div>
<p class="login-subtitle">{{ t('loginSubtitle') }}</p>
</div>
<div class="space-y-6 w-[80%] mx-auto">
<div>
<label for="phoneNumber" class="block text-sm/6 font-medium text-gray-100 text-left">{{ t('phoneNumber') }}</label>
<div class="mt-2">
<input v-model="phoneNumber" type="tel" name="phoneNumber" required maxlength="11" @keyup.enter="handlePhoneEnter" class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6" />
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="verifyCode" class="block text-sm/6 font-medium text-gray-100">{{ t('verifyCode') }}</label>
<div class="text-sm">
<button
@click="sendSmsCode"
class="font-semibold text-[#9a72ff] hover:text-indigo-300"
:disabled="!phoneNumber || smsCountdown > 0 || loginLoading"
>
{{ smsCountdown > 0 ? `${smsCountdown}s` : t('sendSmsCode') }}
</button>
</div>
</div>
<div class="mt-2">
<input v-model="verifyCode" type="text" name="verifyCode" required maxlength="6" @keyup.enter="handleVerifyCodeEnter" class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6" />
</div>
</div>
<div>
<button @click="loginWithSms" :disabled="!phoneNumber || !verifyCode || loginLoading" class="btn-submit flex w-full justify-center rounded-md bg-laser-purple px-3 py-1.5 text-sm/6 font-semibold text-white hover:bg-indigo-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
{{ loginLoading ? t('loginLoading') : t('login') }}
</button>
</div>
</div>
<!-- 分隔线 -->
<div class="divider mb-4 pt-4">
<span class="divider-text">{{ t('orLoginWith') }}</span>
</div>
<!-- 第三方登录按钮 -->
<div class="social-login-buttons">
<button @click="loginWithGitHub" class="btn btn-icon"
:disabled="loginLoading" :title="t('loginWithGitHub')">
<i class="fab fa-github"></i>
</button>
<button @click="loginWithGoogle" class="btn btn-icon"
:disabled="loginLoading" :title="t('loginWithGoogle')">
<i class="fab fa-google"></i>
</button>
</div>
</div>
</div>
</template>
<template>
<DropdownMenu
:items="modelItems"
:selected-value="selectedModel"
:placeholder="t('selectModel')"
:empty-message="t('selectTaskTypeFirst')"
@select-item="handleSelectModel"
/>
</template>
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import DropdownMenu from './DropdownMenu.vue'
const { t } = useI18n()
// Props
const props = defineProps({
availableModels: {
type: Array,
default: () => []
},
selectedModel: {
type: String,
default: ''
}
})
// Emits
const emit = defineEmits(['select-model'])
// Computed
const modelItems = computed(() => {
return props.availableModels.map(model => ({
value: model,
label: model,
icon: 'fas fa-cog'
}))
})
// Methods
const handleSelectModel = (item) => {
emit('select-model', item.value)
}
</script>
This diff is collapsed.
<script setup>
import { showPromptModal,
promptModalTab,
getPromptTemplates,
selectPromptTemplate,
promptHistory,
selectPromptHistory } from '../utils/other'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>
<template>
<!-- 提示词模板和历史记录弹窗 -->
<div v-cloak>
<div v-if="showPromptModal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
@click="showPromptModal = false">
<div class="bg-secondary rounded-xl p-6 max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden"
@click.stop>
<!-- 浮窗头部 -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-white">
<i class="fas fa-lightbulb text-gradient-primary mr-2"></i>
{{ t('promptTemplates') }}
</h3>
<button @click="showPromptModal = false"
class="text-gray-400 hover:text-white transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 标签页切换 -->
<div class="flex border-b border-gray-700 mb-6">
<button @click="promptModalTab = 'templates'"
class="px-4 py-2 text-sm font-medium transition-colors" :class="promptModalTab === 'templates'
? 'text-gradient-primary border-b-2 border-laser-purple'
: 'text-gray-400 hover:text-gray-300'">
<i class="fas fa-layer-group mr-2"></i>
{{ t('templates') }}
</button>
<button @click="promptModalTab = 'history'"
class="px-4 py-2 text-sm font-medium transition-colors" :class="promptModalTab === 'history'
? 'text-gradient-primary border-b-2 border-laser-purple'
: 'text-gray-400 hover:text-gray-300'">
<i class="fas fa-history mr-2"></i>
{{ t('history') }}
</button>
</div>
<!-- 模板内容 -->
<div v-if="promptModalTab === 'templates'" class="overflow-y-auto max-h-[50vh]">
<div v-if="getPromptTemplates(selectedTaskId).length > 0"
class="grid grid-cols-1 md:grid-cols-2 gap-4 overflow-y-auto max-h-[50vh] main-scrollbar">
<button v-for="template in getPromptTemplates(selectedTaskId)" :key="template.id"
@click="selectPromptTemplate(template)"
class="break-inside-avoid mb-4 p-4 text-left bg-dark-light rounded-lg hover:bg-laser-purple/20 transition-all border border-transparent hover:border-laser-purple/40 group">
<div
class="font-medium text-sm mb-2 text-white group-hover:text-gradient-primary transition-colors">
{{ template.title }}
</div>
<div class="text-xs text-gray-400 line-clamp-3 leading-relaxed">
{{ template.prompt }}
</div>
<div class="mt-3 flex items-center justify-between">
<span class="text-xs text-laser-purple/60">{{ t('clickApply') }}</span>
<i
class="fas fa-arrow-right text-xs text-laser-purple/60 group-hover:translate-x-1 transition-transform"></i>
</div>
</button>
</div>
<div v-else class="flex flex-col items-center justify-center py-12 text-center">
<div
class="w-16 h-16 bg-laser-purple/20 rounded-full flex items-center justify-center mb-4">
<i class="fas fa-layer-group text-gradient-primary text-2xl"></i>
</div>
<p class="text-gray-400 text-lg mb-2">{{ t('noAvailableTemplates') }}</p>
<p class="text-gray-500 text-sm">{{ t('pleaseSelectTaskType') }}</p>
</div>
</div>
<!-- 历史记录内容 -->
<div v-if="promptModalTab === 'history'" class="overflow-y-auto max-h-[50vh]">
<div v-if="promptHistory.length === 0"
class="flex flex-col items-center justify-center py-12 text-center">
<div
class="w-16 h-16 bg-laser-purple/20 rounded-full flex items-center justify-center mb-4">
<i class="fas fa-history text-gradient-primary text-2xl"></i>
</div>
<p class="text-gray-400 text-lg mb-2">{{ t('noHistoryRecords') }}</p>
<p class="text-gray-500 text-sm">{{ t('promptHistoryAutoSave') }}</p>
</div>
<div v-else class="space-y-3">
<div class="flex items-center justify-between mb-4">
<span class="text-sm text-gray-400">{{ promptHistory.length }} {{ t('records') }}</span>
<button @click="clearPromptHistory"
class="text-xs text-red-400 hover:text-red-300 transition-colors flex items-center gap-1"
title="{{ t('clearHistory') }}">
<i class="fas fa-trash"></i>
{{ t('clear') }}
</button>
</div>
<button v-for="(history, index) in promptHistory" :key="index"
@click="selectPromptHistory(history)"
class="w-full p-4 text-left bg-dark-light rounded-lg hover:bg-laser-purple/20 transition-all border border-transparent hover:border-laser-purple/40 group">
<div
class="text-sm text-gray-300 line-clamp-3 leading-relaxed group-hover:text-white transition-colors">
{{ history }}
</div>
<div class="mt-2 flex items-center justify-between">
<span class="text-xs text-laser-purple/60">{{ t('clickApply') }}</span>
<i
class="fas fa-arrow-right text-xs text-laser-purple/60 group-hover:translate-x-1 transition-transform"></i>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
This diff is collapsed.
<script setup>
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
const { t, locale } = useI18n()
const router = useRouter()
import { initLanguage,loadLanguageAsync, switchLang, languageOptions } from '../utils/i18n'
import {
currentUser,
logout,
showTemplateDetailModal,
showTaskDetailModal,
login,
} from '../utils/other'
// 导航到主页面
const goToHome = () => {
showTemplateDetailModal.value = false
showTaskDetailModal.value = false
router.push({ name: 'Generate' })
}
</script>
<template>
<!-- 顶部栏 -->
<div class="top-bar">
<div class="top-bar-content">
<div class="top-bar-left">
<button @click="goToHome" class="logo-button" :title="t('goToHome')">
<i class="fas fa-film text-gradient-primary mr-2 text-xl"></i>
<span class="text-lg text-white">LightX2V</span>
</button>
</div>
<!-- 右侧用户信息 -->
<div class="top-bar-right">
<!-- 语言切换按钮 -->
<div class="language-switcher mr-6">
<button @click="switchLang"
class="w-10 h-10 text-gradient-primary items-center px-1 py-1
rounded-lg bg-laser-purple border border-laser-purple hover:border-laser-purple
transition-all duration-200 text-sm hover:scale-105"
:title="t('switchLanguage')">
<span class="text-lg">{{ languageOptions.find(lang => lang.code ===
(locale === 'zh' ? 'en' : 'zh'))?.flag }}</span>
</button>
</div>
<div class="user-info">
<div>
<avatar v-if="currentUser.avatar" :src="getUserAvatarUrl(currentUser)"
:alt="currentUser.username" class="size-10">
</avatar>
<i v-else class="fi fi-rr-circle-user text-2xl"></i>
</div>
<div class="user-details">
<span v-if="currentUser">
{{ currentUser.username || currentUser.email || '用户' }}
</span>
<span v-else>未登录</span>
</div>
<div v-if="currentUser.username">
<button @click="logout" class="text-gradient-primary" :title="t('logout')">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
<div v-else>
<button @click="login" class="text-gradient-primary" :title="t('login')">
<i class="fas fa-sign-in-alt"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.logo-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
border-radius: 0.5rem;
padding: 0.5rem;
}
.logo-button:hover {
background: rgba(139, 92, 246, 0.1);
transform: scale(1.05);
}
.logo-button:active {
transform: scale(0.95);
}
</style>
This diff is collapsed.
This diff is collapsed.
import { createApp } from 'vue'
import router from './router'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import i18n, { initLanguage } from './utils/i18n'
const app = createApp(App)
const pinia = createPinia()
app.use(i18n)
app.use(pinia)
app.use(router)
// 初始化语言
initLanguage().then(() => {
app.mount('#app')
})
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment