| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- <template>
- <div class="navigation-tv">
- <!-- 应用网格 -->
- <div class="apps-grid">
- <a
- v-for="(link, linkIndex) in sortedLinks(currentGroup.links)"
- :key="link.name"
- :href="link.autoLogin ? '#' : (link.url.startsWith('http') ? link.url : `http://${link.url}`)"
- target="_blank"
- rel="noopener noreferrer"
- class="app-tile"
- :style="getTileStyle(linkIndex)"
- :class="{ 'focused': focusedIndex === linkIndex }"
- @mouseenter="focusedIndex = linkIndex"
- @mouseleave="focusedIndex = -1"
- @click.prevent="handleLinkClick($event, link)"
- >
- <div class="tile-background" :style="getBackgroundStyle(link, linkIndex)"></div>
- <div class="tile-content">
- <div
- class="tile-name"
- :style="getFontSize(link.name)"
- >{{ link.name }}</div>
- </div>
- </a>
- </div>
- <!-- 底部导航栏 -->
- <div class="bottom-nav-bar">
- <div class="nav-title">
- <div class="logo-circle"></div>
- <div class="title-group">
- <h1 class="title">韫珠科技</h1>
- <p class="subtitle">内部导航系统</p>
- </div>
- </div>
- <div class="nav-tabs">
- <button
- v-for="(group, index) in sortedGroups"
- :key="group.name"
- :class="['nav-tab', { active: activeTabIndex === index }]"
- @click="switchTab(index)"
- >
- {{ group.name }}
- </button>
- </div>
- <div class="nav-info">
- <div class="time">{{ currentTime }}</div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted, onUnmounted } from 'vue'
- import navigationData from '../data/navigation.json'
- import { getAutoLoginUrl } from '../config/backend.js'
- const groups = ref([])
- const activeTabIndex = ref(0)
- const focusedIndex = ref(-1)
- const currentTime = ref('')
- const sortedGroups = computed(() => {
- return [...groups.value].sort((a, b) => (a.sort || 0) - (b.sort || 0))
- })
- const currentGroup = computed(() => {
- return sortedGroups.value[activeTabIndex.value] || { links: [] }
- })
- const sortedLinks = (links) => {
- if (!links) return []
- return [...links].sort((a, b) => (a.sort || 0) - (b.sort || 0))
- }
- const switchTab = (index) => {
- activeTabIndex.value = index
- focusedIndex.value = -1
- }
- const handleLinkClick = async (event, link) => {
- // 阻止默认行为
- event.preventDefault();
- event.stopPropagation();
-
- if (link.autoLogin) {
- console.log('========================================');
- console.log('[前端] 检测到自动登录链接');
- console.log('[前端] 链接名称:', link.name);
- console.log('[前端] 自动登录方式:', link.autoLoginMethod || 'backend');
-
- try {
- // 前端自动登录方式(推荐)
- if (link.autoLoginMethod === 'frontend') {
- console.log('[前端] 使用纯前端自动登录');
-
- // 动态导入自动登录工具
- const { autoLoginHomeAssistant } = await import('../utils/homeAssistantAuth.js');
-
- // 获取配置
- const baseUrl = link.url.startsWith('http') ? link.url : `http://${link.url}`;
- const username = link.autoLoginUsername || 'guest';
- const password = link.autoLoginPassword || 'guest-888';
-
- // 执行自动登录
- await autoLoginHomeAssistant(baseUrl, username, password);
-
- } else if (link.autoLoginMethod === 'direct') {
- // 直接跳转(利用 Trusted Networks)
- console.log('[前端] 直接跳转(Trusted Networks)');
- const targetUrl = link.url.startsWith('http') ? link.url : `http://${link.url}`;
- window.open(targetUrl, '_blank', 'noopener,noreferrer');
-
- } else {
- // 使用后端代理方式(原有方式)
- console.log('[前端] 使用后端代理自动登录');
- const autoLoginUrl = getAutoLoginUrl(link.autoLoginEndpoint);
- console.log('[前端] 完整自动登录URL:', autoLoginUrl);
- window.open(autoLoginUrl, '_blank', 'noopener,noreferrer');
- }
-
- } catch (error) {
- console.error('[前端] 处理自动登录时出错:', error);
-
- // 如果是 CORS 错误,提示用户
- if (error.message && (error.message.includes('CORS') || error.message.includes('Failed to fetch'))) {
- alert(
- '自动登录失败:跨域限制\n\n' +
- '由于浏览器安全策略,无法直接访问 Home Assistant API。\n\n' +
- '建议:\n' +
- '1. 使用 Trusted Networks 配置(推荐)\n' +
- '2. 或使用后端代理模式\n\n' +
- '现在将直接跳转到 Home Assistant 登录页面'
- );
- } else {
- alert('自动登录失败: ' + error.message + '\n\n将跳转到登录页面');
- }
-
- // 失败后直接跳转
- const targetUrl = link.url.startsWith('http') ? link.url : `http://${link.url}`;
- window.open(targetUrl, '_blank', 'noopener,noreferrer');
- }
-
- console.log('========================================');
- } else {
- // 普通链接
- console.log('[前端] 普通链接:', link.name, link.url);
- const targetUrl = link.url.startsWith('http') ? link.url : `http://${link.url}`;
- window.open(targetUrl, '_blank', 'noopener,noreferrer');
- }
- }
- const getFontSize = (name) => {
- if (!name) return { fontSize: '40px' }
-
- // 计算字符长度(中文算1.5个字符)
- let charCount = 0
- for (let i = 0; i < name.length; i++) {
- if (/[\u4e00-\u9fa5]/.test(name[i])) {
- charCount += 1.5
- } else {
- charCount += 1
- }
- }
-
- // 根据字符数动态调整字体大小
- // 字符少(1-4个字符):大字体 52-44px
- // 字符中等(5-8个字符):中等字体 40-28px
- // 字符多(9+个字符):小字体 26-20px
- let fontSize
- if (charCount <= 4) {
- fontSize = 52 - (charCount - 1) * 2.5
- } else if (charCount <= 8) {
- fontSize = 40 - (charCount - 4) * 3
- } else {
- fontSize = Math.max(20, 28 - (charCount - 8) * 0.8)
- }
-
- return {
- fontSize: `${fontSize}px`
- }
- }
- const getBackgroundStyle = (link, index) => {
- // 如果有背景图片,优先使用背景图片
- if (link.background && link.background.trim() !== '') {
- return {
- backgroundImage: `url(${link.background})`,
- backgroundSize: 'cover',
- backgroundPosition: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.3)'
- }
- }
-
- // 如果没有背景图片,使用随机渐变背景
- const gradients = [
- 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
- 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
- 'linear-gradient(135deg, #f6d365 0%, #fda085 100%)',
- 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
- 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
- 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
- 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
- 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
- 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)',
- 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
- 'linear-gradient(135deg, #ff8a80 0%, #ea6100 100%)',
- 'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)',
- ]
-
- // 使用索引确保相同系统总是使用相同的渐变
- return {
- background: gradients[index % gradients.length]
- }
- }
- const getTileStyle = (index) => {
- return {
- animationDelay: `${index * 0.05}s`
- }
- }
- const updateTime = () => {
- const now = new Date()
- const year = now.getFullYear()
- const month = String(now.getMonth() + 1).padStart(2, '0')
- const day = String(now.getDate()).padStart(2, '0')
- const hours = String(now.getHours()).padStart(2, '0')
- const minutes = String(now.getMinutes()).padStart(2, '0')
- const seconds = String(now.getSeconds()).padStart(2, '0')
- currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
- }
- onMounted(() => {
- groups.value = navigationData.groups || []
- updateTime()
- const timeInterval = setInterval(updateTime, 1000)
-
- // 添加全屏样式类
- document.body.classList.add('tv-mode')
- const app = document.getElementById('app')
- if (app) {
- app.classList.add('tv-mode')
- }
-
- onUnmounted(() => {
- clearInterval(timeInterval)
- document.body.classList.remove('tv-mode')
- const app = document.getElementById('app')
- if (app) {
- app.classList.remove('tv-mode')
- }
- })
- })
- </script>
- <style scoped>
- .navigation-tv {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: #0f1419;
- background-image:
- radial-gradient(circle at 20% 30%, rgba(30, 144, 255, 0.1) 0%, transparent 50%),
- radial-gradient(circle at 80% 70%, rgba(70, 130, 180, 0.08) 0%, transparent 50%),
- linear-gradient(180deg, #0f1419 0%, #1a1f2e 100%);
- display: flex;
- flex-direction: column;
- padding: 0;
- overflow-y: auto;
- }
- .apps-grid {
- flex: 1;
- padding: 60px 40px 120px;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
- gap: 20px;
- max-width: 100%;
- align-content: start;
- }
- .bottom-nav-bar {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- background: rgba(26, 31, 53, 0.95);
- backdrop-filter: blur(20px);
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- padding: 20px 40px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 30px;
- z-index: 100;
- box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2);
- }
- .nav-title {
- display: flex;
- align-items: center;
- gap: 20px;
- flex-shrink: 0;
- }
- .logo-circle {
- width: 60px;
- height: 60px;
- border-radius: 50%;
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
- backdrop-filter: blur(10px);
- border: 2px solid rgba(255, 255, 255, 0.5);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
- }
- .title-group {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .title {
- font-size: 28px;
- font-weight: 700;
- color: #ffffff;
- margin: 0;
- text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
- letter-spacing: 1px;
- }
- .subtitle {
- font-size: 16px;
- font-weight: 400;
- color: rgba(255, 255, 255, 0.8);
- margin: 0;
- text-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
- }
- .nav-tabs {
- display: flex;
- gap: 30px;
- flex: 1;
- justify-content: center;
- }
- .nav-tab {
- background: transparent;
- border: none;
- color: rgba(255, 255, 255, 0.6);
- font-size: 24px;
- font-weight: 500;
- padding: 12px 0;
- cursor: pointer;
- position: relative;
- transition: all 0.3s ease;
- font-family: inherit;
- }
- .nav-tab:hover {
- color: rgba(255, 255, 255, 0.9);
- }
- .nav-tab.active {
- color: #ffffff;
- font-weight: 600;
- }
- .nav-tab.active::after {
- content: '';
- position: absolute;
- bottom: -22px;
- left: 0;
- right: 0;
- height: 3px;
- background: #3498db;
- border-radius: 2px;
- }
- .nav-info {
- display: flex;
- align-items: center;
- color: rgba(255, 255, 255, 0.9);
- font-size: 16px;
- flex-shrink: 0;
- }
- .time {
- font-weight: 500;
- letter-spacing: 0.5px;
- }
- .app-tile {
- position: relative;
- aspect-ratio: 1;
- border-radius: 12px;
- overflow: hidden;
- text-decoration: none;
- color: inherit;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- animation: tileFadeIn 0.4s ease-out both;
- cursor: pointer;
- }
- @keyframes tileFadeIn {
- from {
- opacity: 0;
- transform: scale(0.9);
- }
- to {
- opacity: 1;
- transform: scale(1);
- }
- }
- .app-tile:hover,
- .app-tile.focused {
- transform: scale(1.08) translateY(-5px);
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
- z-index: 10;
- }
- .tile-background {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- transition: all 0.3s ease;
- }
- .tile-background::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.4);
- transition: background 0.3s ease;
- }
- .app-tile:hover .tile-background,
- .app-tile.focused .tile-background {
- transform: scale(1.05);
- }
- .app-tile:hover .tile-background::after,
- .app-tile.focused .tile-background::after {
- background: rgba(0, 0, 0, 0.3);
- }
- .tile-content {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- width: 100%;
- padding: 20px;
- }
- .tile-name {
- color: #ffffff;
- font-weight: 700;
- text-align: center;
- line-height: 1.2;
- text-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
- word-break: break-word;
- letter-spacing: 1px;
- width: 100%;
- }
- /* 响应式设计 */
- @media (max-width: 768px) {
- .apps-grid {
- padding: 40px 20px 140px;
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 15px;
- }
- .bottom-nav-bar {
- flex-direction: column;
- gap: 15px;
- padding: 15px 20px;
- align-items: flex-start;
- }
- .nav-title {
- width: 100%;
- justify-content: center;
- }
- .nav-tabs {
- width: 100%;
- flex-wrap: wrap;
- gap: 15px;
- justify-content: flex-start;
- }
- .nav-tab {
- font-size: 18px;
- padding: 8px 0;
- }
- .nav-tab.active::after {
- bottom: -15px;
- }
- .nav-info {
- width: 100%;
- justify-content: flex-end;
- }
- .title {
- font-size: 24px;
- }
- .subtitle {
- font-size: 14px;
- }
- .logo-circle {
- width: 50px;
- height: 50px;
- }
- }
- @media (min-width: 1200px) {
- .apps-grid {
- grid-template-columns: repeat(6, 1fr);
- }
- }
- </style>
|