mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(web): refine import homepage
This commit is contained in:
1 parent
73c6de364e
commit
8e6cd5c6e6
7 files changed
+165
-27
No files matched your search
@@ -25,9 +25,10 @@ const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const sessionStore = useSessionStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { isInitialized } = storeToRefs(sessionStore)
|
||||
const { isInitialized, sessions } = storeToRefs(sessionStore)
|
||||
const initError = ref<string | null>(null)
|
||||
const pageTransitionKey = computed(() => route.fullPath)
|
||||
const shouldShowSidebar = computed(() => route.path !== '/' || sessions.value.length > 0)
|
||||
|
||||
useColorMode({
|
||||
emitAuto: true,
|
||||
@@ -112,7 +113,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Sidebar :backend-features="false" />
|
||||
<Sidebar v-if="shouldShowSidebar" :backend-features="false" />
|
||||
<main class="relative flex-1 overflow-hidden">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="page-fade" mode="out-in">
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import logoSvg from '@/assets/images/logo.svg'
|
||||
import AgreementModal from '@/components/home/AgreementModal.vue'
|
||||
import ChangelogModal from '@/components/home/ChangelogModal.vue'
|
||||
import HomeFooter from '@/components/home/HomeFooter.vue'
|
||||
import ImportArea from '@/components/import/ImportArea.vue'
|
||||
import LanguageSelectModal from '@/components/home/LanguageSelectModal.vue'
|
||||
import { availableLocales, type LocaleType } from '@/i18n'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { getChatlabSiteLocalePath } from '@/utils/chatlabSiteLocale'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const settingsStore = useSettingsStore()
|
||||
const sessionStore = useSessionStore()
|
||||
const { sessions } = storeToRefs(sessionStore)
|
||||
const isMounted = ref(false)
|
||||
const languageMenuOpen = ref(false)
|
||||
const changelogModalRef = ref<InstanceType<typeof ChangelogModal> | null>(null)
|
||||
const agreementModalRef = ref<InstanceType<typeof AgreementModal> | null>(null)
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
async function selectLocale(localeCode: LocaleType) {
|
||||
await settingsStore.setLocale(localeCode)
|
||||
languageMenuOpen.value = false
|
||||
}
|
||||
|
||||
const tutorialExportUrl = computed(() => {
|
||||
const localePath = getChatlabSiteLocalePath(locale.value)
|
||||
@@ -31,35 +47,128 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex h-full w-full overflow-hidden pt-4">
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div class="flex min-h-full w-full flex-col items-center justify-center px-4 py-12">
|
||||
<div class="relative flex h-full w-full overflow-hidden">
|
||||
<div class="relative flex h-full w-full flex-col overflow-y-auto">
|
||||
<header
|
||||
class="mx-auto flex w-full max-w-6xl flex-none items-center gap-3 px-4 py-4 sm:px-6 sm:py-5"
|
||||
:class="sessions.length > 0 ? 'justify-end' : 'justify-between'"
|
||||
>
|
||||
<div v-if="sessions.length === 0" class="flex select-none items-center gap-2.5">
|
||||
<img :src="logoSvg" alt="" class="pointer-events-none h-8 w-8 select-none" />
|
||||
<span class="text-base font-semibold tracking-tight text-gray-900 dark:text-white">ChatLab</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<UPopover v-model:open="languageMenuOpen" :ui="{ content: 'p-1.5' }">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
:aria-label="t('common.languageSelect.title')"
|
||||
:title="t('common.languageSelect.title')"
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<UIcon name="i-heroicons-language" class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<template #content>
|
||||
<div class="min-w-36" role="menu" :aria-label="t('common.languageSelect.title')">
|
||||
<button
|
||||
v-for="option in availableLocales"
|
||||
:key="option.code"
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
:aria-checked="settingsStore.locale === option.code"
|
||||
class="flex w-full items-center justify-between gap-4 rounded-md px-2.5 py-2 text-left text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
@click="selectLocale(option.code as LocaleType)"
|
||||
>
|
||||
<span>{{ option.nativeName }}</span>
|
||||
<UIcon
|
||||
v-if="settingsStore.locale === option.code"
|
||||
name="i-heroicons-check-20-solid"
|
||||
class="h-4 w-4 text-primary-500"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
:aria-label="t(isDark ? 'settings.basic.appearance.light' : 'settings.basic.appearance.dark')"
|
||||
:title="t(isDark ? 'settings.basic.appearance.light' : 'settings.basic.appearance.dark')"
|
||||
@click="toggleDark()"
|
||||
>
|
||||
<UIcon :name="isDark ? 'i-heroicons-sun' : 'i-heroicons-moon'" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main
|
||||
class="mx-auto flex w-full max-w-4xl flex-1 flex-col items-center justify-center px-4 pb-16 pt-6 sm:px-6 sm:pb-20 sm:pt-8"
|
||||
>
|
||||
<section class="w-full text-center" aria-labelledby="web-wasm-home-title">
|
||||
<div
|
||||
class="relative mb-4 flex select-none items-center justify-center gap-4 transition-all duration-700 ease-out xl:mb-6"
|
||||
class="landing-reveal transition-all duration-500 ease-out"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0'"
|
||||
>
|
||||
<img :src="logoSvg" alt="ChatLab" class="pointer-events-none h-10 w-10 select-none" />
|
||||
<h1 class="text-3xl font-bold leading-none tracking-tight text-gray-900 dark:text-white">
|
||||
{{ t('home.tagline') }}
|
||||
<h1
|
||||
id="web-wasm-home-title"
|
||||
class="landing-title mx-auto max-w-3xl text-[2.15rem] font-semibold leading-[1.08] tracking-[-0.035em] text-gray-950 sm:text-5xl lg:text-[3.25rem] dark:text-white"
|
||||
>
|
||||
{{ t('browser.title') }}
|
||||
</h1>
|
||||
<p class="mx-auto mt-4 max-w-xl text-sm leading-6 text-gray-500 dark:text-gray-400">
|
||||
{{ t('browser.subtitle') }}
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2" role="list">
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300"
|
||||
role="listitem"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>
|
||||
{{ t('browser.noSignIn') }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300"
|
||||
role="listitem"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>
|
||||
{{ t('browser.localOnly') }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300"
|
||||
role="listitem"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>
|
||||
{{ t('browser.noUpload') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full transition-all delay-200 duration-700 ease-out"
|
||||
class="landing-reveal mt-10 w-full transition-all delay-100 duration-500 ease-out"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0'"
|
||||
>
|
||||
<ImportArea :backend-features="false" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 flex items-center gap-3 transition-all delay-300 duration-700 ease-out"
|
||||
class="landing-reveal mt-4 flex justify-center transition-all delay-200 duration-500 ease-out"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0'"
|
||||
>
|
||||
<UButton :href="tutorialExportUrl" target="_blank" trailing-icon="i-heroicons-chevron-right-20-solid">
|
||||
{{ t('home.quickStart.export') }}
|
||||
<UButton
|
||||
:href="tutorialExportUrl"
|
||||
target="_blank"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
size="sm"
|
||||
trailing-icon="i-heroicons-arrow-up-right-20-solid"
|
||||
>
|
||||
{{ t('browser.exportGuide') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<HomeFooter
|
||||
:remote-config-enabled="false"
|
||||
@@ -77,3 +186,17 @@ onMounted(() => {
|
||||
<ChangelogModal ref="changelogModalRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.landing-title {
|
||||
font-family: 'Iowan Old Style', 'Songti SC', 'STSong', 'Noto Serif CJK SC', 'Noto Serif SC', serif;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.landing-reveal {
|
||||
transition-duration: 0.01ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1136,7 +1136,13 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ isDragOver ? t('home.import.dropHint') : t('home.import.clickHint') }}
|
||||
</p>
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<p
|
||||
class="mt-3 text-sm text-gray-500 dark:text-gray-400"
|
||||
:class="{
|
||||
'opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-within:opacity-100':
|
||||
!props.backendFeatures,
|
||||
}"
|
||||
>
|
||||
{{ t('home.import.multipleHint') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"eyebrow": "Local browser edition",
|
||||
"title": "Keep your chat history in your browser",
|
||||
"description": "Import ChatLab JSON / JSONL, WhatsApp TXT, LINE TXT, QQ TXT, or Telegram JSON into isolated local session databases. Files and chat content are never uploaded.",
|
||||
"title": "Your Chat History, Finally Yours.",
|
||||
"subtitle": "Local storage and privacy first, with AI agents and visual insights to help you revisit your chat memories.",
|
||||
"noSignIn": "No sign-in",
|
||||
"localOnly": "Processed on this device",
|
||||
"noUpload": "Nothing uploaded",
|
||||
"localOnlyDetail": "Data is stored in this browser's site storage. Clearing site data also removes imported sessions.",
|
||||
"exportGuide": "How to export chat history",
|
||||
"initializing": "Preparing your local workspace",
|
||||
"initializingDetail": "The first launch loads the SQLite runtime and opens browser storage.",
|
||||
"unsupported": {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"eyebrow": "ブラウザローカル版",
|
||||
"title": "チャット履歴を、ブラウザの中に",
|
||||
"description": "ChatLab JSON / JSONL、WhatsApp TXT、LINE TXT、QQ TXT、Telegram JSON を、端末内の独立したセッションデータベースへ取り込みます。ファイルや会話内容はアップロードされません。",
|
||||
"title": "一つひとつの会話を、たどれる形に",
|
||||
"subtitle": "ローカル保存とプライバシーを最優先に、AI エージェントと可視化グラフでチャットの記憶を振り返れます",
|
||||
"noSignIn": "ログイン不要",
|
||||
"localOnly": "この端末内だけで処理",
|
||||
"noUpload": "アップロードなし",
|
||||
"localOnlyDetail": "データはこのブラウザのサイトストレージに保存されます。サイトデータを消去すると、取り込んだセッションも削除されます。",
|
||||
"exportGuide": "チャット履歴のエクスポート方法",
|
||||
"initializing": "ローカルワークスペースを準備中",
|
||||
"initializingDetail": "初回起動では SQLite ランタイムを読み込み、ブラウザストレージを開きます。",
|
||||
"unsupported": {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"eyebrow": "浏览器本地版",
|
||||
"title": "把聊天记录,留在你的浏览器里",
|
||||
"description": "导入 ChatLab JSON / JSONL、WhatsApp / LINE / QQ TXT 或 Telegram JSON,在本机建立独立会话库。文件与聊天内容不会上传。",
|
||||
"title": "让每一段对话,都有迹可循",
|
||||
"subtitle": "本地存储、隐私优先,通过 AI Agent 和可视化图表,轻松回顾你的聊天记忆",
|
||||
"noSignIn": "无需登录",
|
||||
"localOnly": "仅在本机处理",
|
||||
"noUpload": "数据不上传",
|
||||
"localOnlyDetail": "数据保存在当前浏览器的站点存储中。清除站点数据会一并删除已导入会话。",
|
||||
"exportGuide": "如何导出聊天记录",
|
||||
"initializing": "正在准备本地工作区",
|
||||
"initializingDetail": "首次启动需要加载 SQLite 运行时并打开浏览器存储。",
|
||||
"unsupported": {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"eyebrow": "瀏覽器本機版",
|
||||
"title": "把聊天記錄,留在你的瀏覽器裡",
|
||||
"description": "匯入 ChatLab JSON / JSONL、WhatsApp / LINE / QQ TXT 或 Telegram JSON,在本機建立獨立對話資料庫。檔案與聊天內容不會上傳。",
|
||||
"title": "讓每一段對話,都有跡可循",
|
||||
"subtitle": "本機儲存、隱私優先,透過 AI Agent 和視覺化圖表,輕鬆回顧你的聊天記憶",
|
||||
"noSignIn": "無需登入",
|
||||
"localOnly": "僅在本機處理",
|
||||
"noUpload": "資料不上傳",
|
||||
"localOnlyDetail": "資料保存在目前瀏覽器的網站儲存空間。清除網站資料也會刪除已匯入的對話。",
|
||||
"exportGuide": "如何匯出聊天記錄",
|
||||
"initializing": "正在準備本機工作區",
|
||||
"initializingDetail": "首次啟動需要載入 SQLite 執行環境並開啟瀏覽器儲存空間。",
|
||||
"unsupported": {
|
||||
|
||||
Reference in new issue
Block a user