feat(home): reorganize import options

This commit is contained in:
digua committed 2026-07-15 19:09:05 +08:00
1 parent 7067c3f1c8
commit fcf30853e9
12 files changed
+271 -134

No files matched your search

+19 -17
View File
@@ -18,6 +18,24 @@ ChatLab 可以作为 Codex、Claude Code、HermesAgent 等外部 Agent 的本地
安装:`npm i chatlab-cli -g`(需要 Node.js ≥ 20),并先在 ChatLab 中导入聊天记录。
## 使用 AI Agent
推荐通过通用 Agent Skills CLI 安装官方中文 `chatlab-analyze-cn` 技能:
```bash
npx skills add ChatLab/ChatLab --skill chatlab-analyze-cn -g
```
安装后,可以在 Codex、Claude Code、Cursor 等外部 Agent 中直接说:
```text
chatlab-analyze-cn 帮我分析我和小红的聊天记录
```
这个技能会引导 Agent 先执行 `chatlab manifest` 获取机读命令清单,再用安全的 `--format agent/json` 查询聊天记录。
`chatlab-analyze-cn` 始终只读。如果你希望 Agent 导入新的聊天导出文件,请改用独立的 `chatlab-import-cn` Skill;它会先预览导入计划,再自动新建或增量导入,详见[导入聊天记录指南](./how-to-import.md)。
## 命令总览
```bash
@@ -77,23 +95,7 @@ agent/json 模式下 stdout 只包含一个 JSON 响应信封,日志一律走
- `--raw`(绕过预处理)默认禁用,仅当你显式执行 `chatlab config set cli.allow_raw true` 或设置 `CHATLAB_CLI_ALLOW_RAW=1` 后可用。
- 不需要 SQL 兜底能力时,可用 `chatlab config set cli.allow_sql false` 关闭 `sql` 命令。
## 给 Agent 的使用指南
推荐通过通用 Agent Skills CLI 安装官方中文 `chatlab-analyze-cn` 技能:
```bash
npx skills add ChatLab/ChatLab --skill chatlab-analyze-cn -g
```
安装后,可以在 Codex、Claude Code、Cursor 等外部 Agent 中直接说:
```text
chatlab-analyze-cn 帮我分析我和小红的聊天记录
```
这个技能会引导 Agent 先执行 `chatlab manifest` 获取机读命令清单,再用安全的 `--format agent/json` 查询聊天记录。
`chatlab-analyze-cn` 始终只读。如果你希望 Agent 导入新的聊天导出文件,请改用独立的 `chatlab-import-cn` Skill;它会先预览导入计划,再自动新建或增量导入,详见[导入聊天记录指南](./how-to-import.md)。
## 查询示例
典型配方——"谁最早提到某个问题?查看现场":
+19 -17
View File
@@ -18,6 +18,24 @@ The `chatlab` CLI ships a set of query commands optimized for AI agents while st
Install with `npm i chatlab-cli -g` (Node.js ≥ 20) and import chat records in ChatLab first.
## Use it with an AI agent
Install the official `chatlab-analyze` skill through the general Agent Skills CLI:
```bash
npx skills add ChatLab/ChatLab --skill chatlab-analyze -g
```
After installation, ask Codex, Claude Code, Cursor, or another external agent:
```text
chatlab-analyze help me analyze my chat history with Alice
```
The skill tells the agent to run `chatlab manifest` first, then query chat records safely with explicit `--format agent/json` commands.
`chatlab-analyze` always remains read-only. To let an agent import a new chat export, use the separate `chatlab-import` skill; it previews the import, then automatically creates or incrementally updates a session. See the [Import Chat Records Guide](./how-to-import.md).
## Command overview
```bash
@@ -77,23 +95,7 @@ Failures return `{ "ok": false, "error": { "code", "message", "hint", "candidate
- `--raw` (bypassing preprocessing) is disabled by default; it only works after you explicitly run `chatlab config set cli.allow_raw true` or set `CHATLAB_CLI_ALLOW_RAW=1`.
- If you don't need the SQL fallback, disable it with `chatlab config set cli.allow_sql false`.
## Agent usage guide
Install the official `chatlab-analyze` skill through the general Agent Skills CLI:
```bash
npx skills add ChatLab/ChatLab --skill chatlab-analyze -g
```
After installation, ask Codex, Claude Code, Cursor, or another external agent:
```text
chatlab-analyze help me analyze my chat history with Alice
```
The skill tells the agent to run `chatlab manifest` first, then query chat records safely with explicit `--format agent/json` commands.
`chatlab-analyze` always remains read-only. To let an agent import a new chat export, use the separate `chatlab-import` skill; it previews the import, then automatically creates or incrementally updates a session. See the [Import Chat Records Guide](./how-to-import.md).
## Query example
A typical recipe — "who mentioned this first? inspect the surrounding context":
+14 -6
View File
@@ -2,12 +2,20 @@
"tagline": "Explore New Chat Memories",
"tabs": {
"file": "File Import",
"api": "Auto Sync",
"push": "API Push",
"apiDescription": "Connect external platform data sources to automatically sync chat records to ChatLab on a schedule.",
"apiButton": "Configure Auto Sync",
"pushDescription": "Start the local API server to let third-party tools, plugins, or scripts push chat records directly into ChatLab.",
"pushButton": "Configure API Server"
"api": "API Import",
"cli": "CLI Import",
"autoSyncTitle": "Automatic Sync",
"autoSyncDescription": "Let ChatLab periodically pull new chat records from an external data source.",
"autoSyncButton": "Configure Sync",
"apiPushTitle": "API Push",
"apiPushDescription": "Let third-party tools, plugins, or scripts write through the local API.",
"apiPushButton": "Configure API",
"agentImportTitle": "Import with an AI Agent",
"agentImportDescription": "Install the official Skill to import chat records automatically.",
"manualCliTitle": "Use the CLI Manually",
"manualCliDescription": "Ideal for terminal workflows, batch scripts, and repeatable automation.",
"copyCommand": "Copy command",
"viewDocs": "View documentation"
},
"import": {
"dropHint": "Drop to import files",
+14 -6
View File
@@ -2,12 +2,20 @@
"tagline": "新しいチャットの記憶を探索する",
"tabs": {
"file": "ファイルインポート",
"api": "自動同期",
"push": "APIプッシュ",
"apiDescription": "外部プラットフォームのデータソースを設定し、チャット履歴をChatLabへ定期的に自動同期します。",
"apiButton": "自動同期を設定する",
"pushDescription": "ローカルAPIサーバーを起動し、サードパーティのツール・プラグイン・スクリプトからChatLabへチャット履歴をプッシュできます。",
"pushButton": "APIサーバーを設定する"
"api": "API インポート",
"cli": "CLI インポート",
"autoSyncTitle": "自動同期",
"autoSyncDescription": "ChatLab が外部データソースから新しいチャット履歴を定期的に取得します。",
"autoSyncButton": "自動同期を設定",
"apiPushTitle": "API プッシュ",
"apiPushDescription": "外部ツール、プラグイン、スクリプトからローカル API 経由で書き込みます。",
"apiPushButton": "API を設定",
"agentImportTitle": "AI Agent でインポート",
"agentImportDescription": "公式 Skill をインストールして、チャット履歴を自動的にインポートします。",
"manualCliTitle": "CLI を手動で使用",
"manualCliDescription": "ターミナル操作、バッチスクリプト、繰り返し可能な自動化に適しています。",
"copyCommand": "コマンドをコピー",
"viewDocs": "ドキュメントを見る"
},
"import": {
"dropHint": "マウスを離してファイルをインポート",
+14 -6
View File
@@ -2,12 +2,20 @@
"tagline": "探索新的聊天记忆",
"tabs": {
"file": "文件导入",
"api": "自动同步",
"push": "API推送",
"apiDescription": "配置外部平台的数据源,让聊天记录定期自动同步到ChatLab。",
"apiButton": "去配置自动同步",
"pushDescription": "开启本地API服务,允许第三方工具/插件或脚本将聊天记录主动推送至ChatLab。",
"pushButton": "去开启 API 服务"
"api": "API 导入",
"cli": "CLI 导入",
"autoSyncTitle": "自动同步",
"autoSyncDescription": "由 ChatLab 定时从外部数据源拉取新增聊天记录。",
"autoSyncButton": "配置自动同步",
"apiPushTitle": "API 推送",
"apiPushDescription": "让第三方工具、插件或脚本通过本地 API 主动写入。",
"apiPushButton": "配置 API 服务",
"agentImportTitle": "使用 AI Agent 导入",
"agentImportDescription": "安装官方Skill,使用Skill自动导入聊天记录。",
"manualCliTitle": "手动使用 CLI",
"manualCliDescription": "适合终端操作、批处理脚本和可重复的自动化流程。",
"copyCommand": "复制命令",
"viewDocs": "查看文档"
},
"import": {
"dropHint": "松开鼠标导入文件",
+14 -6
View File
@@ -2,12 +2,20 @@
"tagline": "探索新的聊天記憶",
"tabs": {
"file": "檔案匯入",
"api": "自動同步",
"push": "API推送",
"apiDescription": "設定外部平台的資料來源,讓聊天記錄定期自動同步到ChatLab。",
"apiButton": "前往設定自動同步",
"pushDescription": "開啟本地API服務,允許第三方工具/掛件或腳本將聊天記錄主動推送至ChatLab。",
"pushButton": "前往設定 API 服務"
"api": "API 匯入",
"cli": "命令列匯入",
"autoSyncTitle": "自動同步",
"autoSyncDescription": "由 ChatLab 定期從外部資料來源拉取新增聊天記錄。",
"autoSyncButton": "設定自動同步",
"apiPushTitle": "API 推送",
"apiPushDescription": "讓第三方工具、外掛或腳本透過本機 API 主動寫入。",
"apiPushButton": "設定 API 服務",
"agentImportTitle": "使用 AI Agent 匯入",
"agentImportDescription": "安裝官方 Skill,使用 Skill 自動匯入聊天記錄。",
"manualCliTitle": "手動使用 CLI",
"manualCliDescription": "適合終端操作、批次腳本與可重複的自動化流程。",
"copyCommand": "複製命令",
"viewDocs": "查看文件"
},
"import": {
"dropHint": "鬆開滑鼠匯入檔案",
@@ -5,34 +5,67 @@ import { useLayoutStore } from '@/stores/layout'
const { t } = useI18n()
const layoutStore = useLayoutStore()
// 打开设置弹窗并定位到 API 服务子 Tab
// Pull 与 Push 共用“API 导入”入口,但继续进入各自独立的配置子页。
function openSyncSettings() {
layoutStore.openSettings('api', 'sync')
}
function openApiSettings() {
layoutStore.openSettings('api', 'api')
}
</script>
<template>
<div class="w-full flex flex-col items-center space-y-6">
<div
class="flex h-[190px] sm:h-[220px] w-full max-w-md flex-col items-center justify-center rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40 select-none text-center sm:px-12"
>
<div class="mb-2 flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center">
<UIcon name="i-heroicons-cpu-chip" class="h-6 w-6 sm:h-8 sm:w-8 text-pink-600 dark:text-pink-400" />
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
{{ t('home.tabs.pushDescription') }}
</p>
<UButton
class="mt-3 font-semibold cursor-pointer"
color="primary"
variant="solid"
icon="i-heroicons-cog-6-tooth"
@click="openApiSettings"
<div class="flex w-full flex-col items-center space-y-6">
<div class="grid w-full max-w-2xl gap-3 sm:h-[220px] sm:grid-cols-2">
<section
class="flex min-h-[210px] flex-col rounded-3xl bg-white/80 p-5 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
{{ t('home.tabs.pushButton') }}
</UButton>
<div class="flex items-start justify-between gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 dark:bg-primary-500/10">
<UIcon name="i-heroicons-cloud-arrow-down" class="h-5 w-5 text-primary-600 dark:text-primary-400" />
</div>
<span
class="rounded-full bg-primary-50 px-2 py-1 text-[10px] font-bold uppercase text-primary-600 dark:bg-primary-500/10 dark:text-primary-400"
>
Pull
</span>
</div>
<div class="mt-3">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.tabs.autoSyncTitle') }}</h2>
<p class="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
{{ t('home.tabs.autoSyncDescription') }}
</p>
</div>
<UButton class="mt-auto self-start" color="primary" variant="soft" size="sm" @click="openSyncSettings">
{{ t('home.tabs.autoSyncButton') }}
</UButton>
</section>
<section
class="flex min-h-[210px] flex-col rounded-3xl bg-white/80 p-5 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<div class="flex items-start justify-between gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 dark:bg-primary-500/10">
<UIcon name="i-heroicons-paper-airplane" class="h-5 w-5 text-primary-600 dark:text-primary-400" />
</div>
<span
class="rounded-full bg-primary-50 px-2 py-1 text-[10px] font-bold uppercase text-primary-600 dark:bg-primary-500/10 dark:text-primary-400"
>
Push
</span>
</div>
<div class="mt-3">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.tabs.apiPushTitle') }}</h2>
<p class="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
{{ t('home.tabs.apiPushDescription') }}
</p>
</div>
<UButton class="mt-auto self-start" color="primary" variant="soft" size="sm" @click="openApiSettings">
{{ t('home.tabs.apiPushButton') }}
</UButton>
</section>
</div>
<!-- 占位符以对齐文件导入底部的复选框高度,保持 Tab 切换时下方元素位置稳定 -->
<div class="h-6 w-full opacity-0 pointer-events-none" />
</div>
</template>
@@ -1,38 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useLayoutStore } from '@/stores/layout'
const { t } = useI18n()
const layoutStore = useLayoutStore()
// 打开设置弹窗并定位到自动同步子 Tab
function openSyncSettings() {
layoutStore.openSettings('api', 'sync')
}
</script>
<template>
<div class="w-full flex flex-col items-center space-y-6">
<div
class="flex h-[190px] sm:h-[220px] w-full max-w-md flex-col items-center justify-center rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40 select-none text-center sm:px-12"
>
<div class="mb-2 flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center">
<UIcon name="i-heroicons-cloud-arrow-down" class="h-6 w-6 sm:h-8 sm:w-8 text-pink-600 dark:text-pink-400" />
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
{{ t('home.tabs.apiDescription') }}
</p>
<UButton
class="mt-3 font-semibold cursor-pointer"
color="primary"
variant="solid"
icon="i-heroicons-cog-6-tooth"
@click="openSyncSettings"
>
{{ t('home.tabs.apiButton') }}
</UButton>
</div>
<!-- 占位符以对齐文件导入底部的复选框高度,保持 Tab 切换时下方元素位置稳定 -->
<div class="h-6 w-full opacity-0 pointer-events-none" />
</div>
</template>
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useClipboard } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const skillInstallCommand = computed(() => {
const skillName = locale.value === 'zh-CN' || locale.value === 'zh-TW' ? 'chatlab-import-cn' : 'chatlab-import'
return `npx skills add ChatLab/ChatLab --skill ${skillName} -g`
})
const cliImportCommand = 'chatlab import ~/Downloads/chat-export.json'
const { copy: copySkillCommand, copied: skillCommandCopied } = useClipboard({ copiedDuring: 2000 })
const { copy: copyCliCommand, copied: cliCommandCopied } = useClipboard({ copiedDuring: 2000 })
const importGuideUrl = computed(() => {
const prefix = locale.value === 'zh-CN' ? '/cn' : locale.value === 'zh-TW' ? '/tw' : ''
return `https://docs.chatlab.fun${prefix}/usage/how-to-import`
})
</script>
<template>
<div class="flex w-full flex-col items-center space-y-6">
<div class="grid w-full max-w-2xl gap-3 sm:h-[220px] sm:grid-cols-2">
<section
class="flex min-h-[210px] flex-col rounded-3xl bg-white/80 p-5 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 dark:bg-primary-500/10">
<UIcon name="i-heroicons-sparkles" class="h-5 w-5 text-primary-600 dark:text-primary-400" />
</div>
<div class="mt-3">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.tabs.agentImportTitle') }}</h2>
<p class="mt-1 text-xs leading-relaxed text-gray-600 dark:text-gray-300">
{{ t('home.tabs.agentImportDescription') }}
</p>
</div>
<div
class="mt-auto flex items-center gap-2 rounded-xl bg-gray-100 px-3 py-2 text-gray-700 ring-1 ring-gray-200/80 dark:bg-gray-900 dark:text-gray-100 dark:ring-white/10"
>
<code class="min-w-0 flex-1 truncate text-[10px] sm:text-[11px]">{{ skillInstallCommand }}</code>
<UButton
:icon="skillCommandCopied ? 'i-heroicons-check' : 'i-heroicons-clipboard'"
:color="skillCommandCopied ? 'success' : 'neutral'"
variant="ghost"
size="xs"
:aria-label="t('home.tabs.copyCommand')"
@click="copySkillCommand(skillInstallCommand)"
/>
</div>
<UButton
:href="importGuideUrl"
target="_blank"
color="primary"
variant="link"
size="xs"
icon="i-heroicons-document-text"
class="mt-1 self-start px-0"
>
{{ t('home.tabs.viewDocs') }}
</UButton>
</section>
<section
class="flex min-h-[210px] flex-col rounded-3xl bg-white/80 p-5 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 dark:bg-primary-500/10">
<UIcon name="i-heroicons-command-line" class="h-5 w-5 text-primary-600 dark:text-primary-400" />
</div>
<div class="mt-3">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.tabs.manualCliTitle') }}</h2>
<p class="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
{{ t('home.tabs.manualCliDescription') }}
</p>
</div>
<div
class="mt-auto flex items-center gap-2 rounded-xl bg-gray-100 px-3 py-2 text-gray-700 ring-1 ring-gray-200/80 dark:bg-gray-900 dark:text-gray-100 dark:ring-white/10"
>
<code class="min-w-0 flex-1 truncate text-[10px] sm:text-[11px]">{{ cliImportCommand }}</code>
<UButton
:icon="cliCommandCopied ? 'i-heroicons-check' : 'i-heroicons-clipboard'"
:color="cliCommandCopied ? 'success' : 'neutral'"
variant="ghost"
size="xs"
:aria-label="t('home.tabs.copyCommand')"
@click="copyCliCommand(cliImportCommand)"
/>
</div>
<UButton
:href="importGuideUrl"
target="_blank"
color="primary"
variant="link"
size="xs"
icon="i-heroicons-document-text"
class="mt-1 self-start px-0"
>
{{ t('home.tabs.viewDocs') }}
</UButton>
</section>
</div>
<div class="h-6 w-full opacity-0 pointer-events-none" />
</div>
</template>
@@ -878,7 +878,7 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
<!-- 批量导入进度(导入中) -->
<div
v-if="isBatchImporting && batchFiles.length > 0"
class="w-full max-w-4xl rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 py-6 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40"
class="w-full max-w-4xl rounded-3xl bg-white/80 px-8 py-6 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<!-- 标题和进度 -->
<div class="mb-4 flex items-center justify-between">
@@ -924,7 +924,7 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
<!-- 合并导入进度 -->
<div
v-else-if="isMergeImporting && mergeStage !== 'done'"
class="w-full max-w-4xl rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 py-6 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40"
class="w-full max-w-4xl rounded-3xl bg-white/80 px-8 py-6 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<!-- 标题 -->
<div class="mb-4 flex items-center justify-between">
@@ -977,7 +977,7 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
<!-- 合并导入完成 -->
<div
v-else-if="isMergeImporting && mergeStage === 'done' && mergeResult"
class="w-full max-w-4xl rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 py-6 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40"
class="w-full max-w-4xl rounded-3xl bg-white/80 px-8 py-6 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
@@ -1005,7 +1005,7 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
<!-- 批量导入结果摘要 -->
<div
v-else-if="batchImportResult"
class="w-full max-w-4xl rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 py-6 backdrop-blur-md dark:border-white/10 dark:bg-gray-800/40"
class="w-full max-w-4xl rounded-3xl bg-white/80 px-8 py-6 shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md dark:bg-card-dark dark:ring-white/[0.06]"
>
<!-- 标题 -->
<div class="mb-4 flex items-center justify-between">
@@ -1088,9 +1088,9 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
>
<template #default="{ isDragOver }">
<div
class="group relative flex w-full cursor-pointer flex-col items-center justify-center rounded-3xl border border-gray-200/50 bg-gray-100/50 px-8 h-[190px] sm:h-[220px] backdrop-blur-md transition-all duration-300 hover:border-pink-500/30 hover:bg-gray-100/80 hover:shadow-2xl hover:shadow-pink-500/10 focus:outline-none focus:ring-4 focus:ring-pink-500/20 sm:px-12 dark:border-white/10 dark:bg-gray-800/40 dark:hover:border-pink-500/30 dark:hover:bg-gray-800/60"
class="group relative flex w-full cursor-pointer flex-col items-center justify-center rounded-3xl bg-white/80 px-8 h-[190px] sm:h-[220px] shadow-elevated ring-1 ring-gray-900/[0.04] backdrop-blur-md transition-all duration-300 hover:bg-white hover:ring-primary-500/30 hover:shadow-2xl hover:shadow-primary-500/10 focus:outline-none focus:ring-4 focus:ring-primary-500/20 sm:px-12 dark:bg-card-dark dark:ring-white/[0.06] dark:hover:bg-card-dark dark:hover:ring-primary-500/30"
:class="{
'border-pink-500/50 bg-pink-50/50 dark:border-pink-400/50 dark:bg-pink-500/10':
'bg-primary-50/50 ring-primary-500/50 dark:bg-primary-500/10 dark:ring-primary-400/50':
isDragOver && !isAnyImporting,
'cursor-not-allowed opacity-70': isAnyImporting,
}"
@@ -1098,15 +1098,19 @@ const getMergeFileProgressText = (file: MergeFileInfo) =>
>
<!-- 上传图标容器 -->
<div
class="mb-5 flex h-16 w-16 items-center justify-center rounded-2xl transition-transform duration-300 group-hover:scale-105"
class="mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 transition-transform duration-300 group-hover:scale-105 dark:bg-primary-500/10"
:class="{ 'scale-105': isDragOver && !isAnyImporting, 'animate-pulse': isImporting }"
>
<UIcon
v-if="!isImporting"
name="i-heroicons-arrow-up-tray"
class="h-8 w-8 text-pink-600 transition-transform duration-200 group-hover:-translate-y-1 dark:text-pink-400"
class="h-5 w-5 text-primary-600 dark:text-primary-400"
/>
<UIcon
v-else
name="i-heroicons-arrow-path"
class="h-5 w-5 animate-spin text-primary-600 dark:text-primary-400"
/>
<UIcon v-else name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-600 dark:text-pink-400" />
</div>
<!-- Text -->
@@ -3,11 +3,11 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
modelValue: 'file' | 'api' | 'push'
modelValue: 'file' | 'api' | 'cli'
}>()
const emit = defineEmits<{
'update:modelValue': [value: 'file' | 'api' | 'push']
'update:modelValue': [value: 'file' | 'api' | 'cli']
}>()
const { t } = useI18n()
@@ -15,8 +15,8 @@ const { t } = useI18n()
// 定义切换栏的三个固定选项
const tabs = [
{ id: 'file', labelKey: 'home.tabs.file' },
{ id: 'cli', labelKey: 'home.tabs.cli' },
{ id: 'api', labelKey: 'home.tabs.api' },
{ id: 'push', labelKey: 'home.tabs.push' },
] as const
// 缓存按钮引用以计算滑动偏移
+6 -6
View File
@@ -9,8 +9,8 @@ import AgreementModal from './components/AgreementModal.vue'
import MigrationModal from './components/MigrationModal.vue'
import ImportArea from './components/import/ImportArea.vue'
import ImportTabSelector from './components/import/ImportTabSelector.vue'
import AutoSyncCard from './components/import/AutoSyncCard.vue'
import ApiImportCard from './components/import/ApiImportCard.vue'
import CliImportCard from './components/import/CliImportCard.vue'
import ChangelogModal from './components/ChangelogModal.vue'
import HomeFooter from './components/HomeFooter.vue'
import DemoImportButton from './components/DemoImportButton.vue'
@@ -19,7 +19,7 @@ const { t, locale } = useI18n()
const sessionStore = useSessionStore()
// 导入方式选中的 Tab 状态
const activeTab = ref<'file' | 'api' | 'push'>('file')
const activeTab = ref<'file' | 'api' | 'cli'>('file')
// 首页入场动效:挂载后通过 requestAnimationFrame 触发,确保初始透明态已完成渲染
const isMounted = ref(false)
@@ -94,11 +94,11 @@ const tutorialExportUrl = computed(() => {
<!-- 文件导入区域 -->
<ImportArea v-if="activeTab === 'file'" />
<!-- 自动同步区域 -->
<AutoSyncCard v-else-if="activeTab === 'api'" />
<!-- API 导入区域:统一承载自动拉取与 API 推送 -->
<ApiImportCard v-else-if="activeTab === 'api'" />
<!-- API 推送区域 -->
<ApiImportCard v-else-if="activeTab === 'push'" />
<!-- CLI 导入区域:Agent Skill 与手动 CLI -->
<CliImportCard v-else-if="activeTab === 'cli'" />
</div>
<div