Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 12s
Test / e2e-test (push) Failing after 23s
Navigating directly to /bugs, /oils etc. now correctly activates the matching tab and shows the right page content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
5.6 KiB
Vue
143 lines
5.6 KiB
Vue
<template>
|
||
<div v-if="isPreview" style="background:#e65100;color:white;text-align:center;padding:6px 16px;font-size:13px;font-weight:600;letter-spacing:0.5px;position:sticky;top:0;z-index:100">
|
||
⚠️ 预览环境 · PR #{{ prId }} · 数据为生产副本,修改不影响正式环境
|
||
</div>
|
||
<div class="app-header" style="position:relative">
|
||
<div class="header-inner" style="padding-right:80px">
|
||
<div class="header-icon">🌿</div>
|
||
<div class="header-title" style="text-align:left;flex:1">
|
||
<h1 style="display:flex;justify-content:space-between;align-items:center;gap:8px;white-space:nowrap">
|
||
<span style="flex-shrink:0">doTERRA 配方计算器
|
||
<span v-if="auth.isAdmin" style="font-size:10px;font-weight:400;opacity:0.5;vertical-align:top">v2.2.0</span>
|
||
</span>
|
||
<span
|
||
style="cursor:pointer;color:white;font-size:13px;font-weight:500;flex-shrink:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB',sans-serif;letter-spacing:0.3px;opacity:0.95"
|
||
@click="toggleUserMenu"
|
||
>
|
||
<template v-if="auth.isLoggedIn">
|
||
👤 {{ auth.user.display_name || auth.user.username }} ▾
|
||
</template>
|
||
<template v-else>
|
||
<span style="background:rgba(255,255,255,0.2);padding:4px 12px;border-radius:12px">登录</span>
|
||
</template>
|
||
</span>
|
||
</h1>
|
||
<p style="display:flex;flex-wrap:wrap;gap:4px 8px;margin:0">
|
||
<span style="white-space:nowrap">查询配方</span>
|
||
<span style="opacity:0.5">·</span>
|
||
<span style="white-space:nowrap">计算成本</span>
|
||
<span style="opacity:0.5">·</span>
|
||
<span style="white-space:nowrap">自制配方</span>
|
||
<span style="opacity:0.5">·</span>
|
||
<span style="white-space:nowrap">导出卡片</span>
|
||
<span style="opacity:0.5">·</span>
|
||
<span style="white-space:nowrap">精油知识</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- User Menu Popup -->
|
||
<UserMenu v-if="showUserMenu" @close="showUserMenu = false" />
|
||
|
||
<!-- Nav tabs -->
|
||
<div class="nav-tabs">
|
||
<div class="nav-tab" :class="{ active: ui.currentSection === 'search' }" @click="goSection('search')">🔍 配方查询</div>
|
||
<div class="nav-tab" :class="{ active: ui.currentSection === 'manage' }" @click="requireLogin('manage')">📋 管理配方</div>
|
||
<div class="nav-tab" :class="{ active: ui.currentSection === 'inventory' }" @click="requireLogin('inventory')">📦 个人库存</div>
|
||
<div class="nav-tab" :class="{ active: ui.currentSection === 'oils' }" @click="goSection('oils')">💧 精油价目</div>
|
||
<div v-if="auth.isBusiness" class="nav-tab" :class="{ active: ui.currentSection === 'projects' }" @click="goSection('projects')">💼 商业核算</div>
|
||
<div v-if="auth.isAdmin" class="nav-tab" :class="{ active: ui.currentSection === 'audit' }" @click="goSection('audit')">📜 操作日志</div>
|
||
<div v-if="auth.isAdmin" class="nav-tab" :class="{ active: ui.currentSection === 'bugs' }" @click="goSection('bugs')">🐛 Bug</div>
|
||
<div v-if="auth.isAdmin" class="nav-tab" :class="{ active: ui.currentSection === 'users' }" @click="goSection('users')">👥 用户管理</div>
|
||
</div>
|
||
|
||
<!-- Main content -->
|
||
<div class="main">
|
||
<router-view />
|
||
</div>
|
||
|
||
<!-- Login Modal -->
|
||
<LoginModal v-if="ui.showLoginModal" @close="ui.closeLogin()" />
|
||
|
||
<!-- Custom Dialog -->
|
||
<CustomDialog />
|
||
|
||
<!-- Toast messages -->
|
||
<div v-for="(toast, i) in ui.toasts" :key="i" class="toast">{{ toast }}</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useAuthStore } from './stores/auth'
|
||
import { useOilsStore } from './stores/oils'
|
||
import { useRecipesStore } from './stores/recipes'
|
||
import { useUiStore } from './stores/ui'
|
||
import LoginModal from './components/LoginModal.vue'
|
||
import CustomDialog from './components/CustomDialog.vue'
|
||
import UserMenu from './components/UserMenu.vue'
|
||
|
||
const auth = useAuthStore()
|
||
const oils = useOilsStore()
|
||
const recipeStore = useRecipesStore()
|
||
const ui = useUiStore()
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const showUserMenu = ref(false)
|
||
|
||
// Sync ui.currentSection from route on load and navigation
|
||
const routeToSection = { '/': 'search', '/manage': 'manage', '/inventory': 'inventory', '/oils': 'oils', '/projects': 'projects', '/mydiary': 'mydiary', '/audit': 'audit', '/bugs': 'bugs', '/users': 'users' }
|
||
watch(() => route.path, (path) => {
|
||
const section = routeToSection[path] || 'search'
|
||
ui.showSection(section)
|
||
}, { immediate: true })
|
||
|
||
// Preview environment detection: pr-{id}.oil.oci.euphon.net
|
||
const hostname = window.location.hostname
|
||
const prMatch = hostname.match(/^pr-(\d+)\./)
|
||
const isPreview = !!prMatch
|
||
const prId = prMatch ? prMatch[1] : ''
|
||
|
||
function goSection(name) {
|
||
ui.showSection(name)
|
||
router.push('/' + (name === 'search' ? '' : name))
|
||
}
|
||
|
||
function requireLogin(name) {
|
||
if (!auth.isLoggedIn) {
|
||
ui.openLogin()
|
||
return
|
||
}
|
||
goSection(name)
|
||
}
|
||
|
||
function toggleUserMenu() {
|
||
if (!auth.isLoggedIn) {
|
||
ui.openLogin()
|
||
return
|
||
}
|
||
showUserMenu.value = !showUserMenu.value
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await auth.initToken()
|
||
await Promise.all([
|
||
oils.loadOils(),
|
||
recipeStore.loadRecipes(),
|
||
recipeStore.loadTags(),
|
||
])
|
||
if (auth.isLoggedIn) {
|
||
await recipeStore.loadFavorites()
|
||
}
|
||
|
||
// Periodic refresh
|
||
setInterval(async () => {
|
||
if (document.visibilityState !== 'visible') return
|
||
try {
|
||
await auth.loadMe()
|
||
} catch {}
|
||
}, 15000)
|
||
})
|
||
</script>
|