feat(web): refine import homepage

This commit is contained in:
digua committed 2026-07-20 23:23:49 +08:00
1 parent 73c6de364e
commit 8e6cd5c6e6
7 files changed
+165 -27

No files matched your search

+3 -2
View File
@@ -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">
+135 -12
View File
@@ -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>
+7 -1
View File
@@ -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>
+5 -3
View File
@@ -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": {
+5 -3
View File
@@ -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": {
+5 -3
View File
@@ -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": {
+5 -3
View File
@@ -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": {