feat: 新用户首先弹出语言选择弹窗

This commit is contained in:
digua
2026-04-08 23:07:02 +08:00
committed by digua
parent 915efeb0be
commit d93ed0b389
7 changed files with 167 additions and 50 deletions
+5
View File
@@ -85,6 +85,11 @@
"nov": "Nov",
"dec": "Dec"
},
"languageSelect": {
"title": "Select Language",
"subtitle": "Choose your preferred interface language",
"next": "Next"
},
"agreement": {
"title": "Privacy Policy & User Agreement",
"subtitle": "Please read carefully before use",
+5
View File
@@ -85,6 +85,11 @@
"nov": "11月",
"dec": "12月"
},
"languageSelect": {
"title": "言語を選択",
"subtitle": "ご希望のインターフェース言語を選択してください",
"next": "次へ"
},
"agreement": {
"title": "プライバシーポリシーと利用規約",
"subtitle": "利用前に必ずお読みください",
+5
View File
@@ -85,6 +85,11 @@
"nov": "11月",
"dec": "12月"
},
"languageSelect": {
"title": "选择语言",
"subtitle": "请选择您偏好的界面语言",
"next": "下一步"
},
"agreement": {
"title": "隐私政策与用户协议",
"subtitle": "使用前请仔细阅读",
+5
View File
@@ -85,6 +85,11 @@
"nov": "11月",
"dec": "12月"
},
"languageSelect": {
"title": "選擇語言",
"subtitle": "請選擇您偏好的介面語言",
"next": "下一步"
},
"agreement": {
"title": "隱私權政策與使用條款",
"subtitle": "使用前請先詳閱",
+23 -49
View File
@@ -1,11 +1,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import MarkdownIt from 'markdown-it'
import { useSettingsStore } from '@/stores/settings'
import UITabs from '@/components/UI/Tabs.vue'
import { availableLocales, type LocaleType } from '@/i18n'
import agreementZh from '@/assets/docs/agreement_zh.md?raw'
import agreementEn from '@/assets/docs/agreement_en.md?raw'
import agreementZhTw from '@/assets/docs/agreement_zh_tw.md?raw'
@@ -15,42 +13,15 @@ const { t } = useI18n()
const settingsStore = useSettingsStore()
const { locale } = storeToRefs(settingsStore)
// 语言选项
const languageOptions = computed(() =>
availableLocales.map((l) => ({
label: l.nativeName,
value: l.code,
}))
)
// 语言切换
const currentLocale = computed({
get: () => locale.value,
set: (val: LocaleType) => settingsStore.setLocale(val),
})
// 协议版本号(更新协议时修改此版本号)
const AGREEMENT_VERSION = '2.0'
const AGREEMENT_KEY = 'chatlab_agreement_version'
// 弹窗显示状态(内部管理
// 弹窗显示状态(由父组件通过 open() 控制
const isOpen = ref(false)
// 是否为版本更新导致的重新阅读
const isVersionUpdated = ref(false)
// 组件挂载时检查是否需要显示
onMounted(() => {
const acceptedVersion = localStorage.getItem(AGREEMENT_KEY)
// 版本号不匹配时需要重新同意
if (acceptedVersion !== AGREEMENT_VERSION) {
isOpen.value = true
// 如果之前有同意过旧版本,则是版本更新
if (acceptedVersion) {
isVersionUpdated.value = true
}
}
})
// 创建 markdown-it 实例
const md = new MarkdownIt({
html: false,
@@ -83,8 +54,6 @@ const renderedContent = computed(() => md.render(agreementText.value))
// 同意协议
function handleAgree() {
localStorage.setItem(AGREEMENT_KEY, AGREEMENT_VERSION)
// 标记用户已确认语言设置(无论是否手动切换)
localStorage.setItem('chatlab_locale_set_by_user', 'true')
isOpen.value = false
}
@@ -95,12 +64,23 @@ function handleDisagree() {
window.api.send('window-close')
}
// 手动打开弹窗(供外部调用)
// 打开弹窗(供外部调用)
function open() {
const acceptedVersion = localStorage.getItem(AGREEMENT_KEY)
if (acceptedVersion && acceptedVersion !== AGREEMENT_VERSION) {
isVersionUpdated.value = true
}
isOpen.value = true
}
defineExpose({ open })
/**
* 检查是否需要同意协议(版本不匹配或从未同意)
*/
function needsAgreement(): boolean {
return localStorage.getItem(AGREEMENT_KEY) !== AGREEMENT_VERSION
}
defineExpose({ open, needsAgreement })
</script>
<template>
@@ -116,21 +96,15 @@ defineExpose({ open })
<!-- 弹窗区域禁止拖拽避免顶部点击被拖拽区域抢占 -->
<div class="agreement-modal flex max-h-[85vh] flex-col p-6">
<!-- Header -->
<div class="mb-4 flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-100 to-rose-100 dark:from-pink-900/30 dark:to-rose-900/30"
>
<UIcon name="i-heroicons-document-text" class="h-6 w-6 text-pink-600 dark:text-pink-400" />
</div>
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('common.agreement.title') }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('common.agreement.subtitle') }}</p>
</div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-100 to-rose-100 dark:from-pink-900/30 dark:to-rose-900/30"
>
<UIcon name="i-heroicons-document-text" class="h-6 w-6 text-pink-600 dark:text-pink-400" />
</div>
<!-- 语言切换 -->
<div class="w-36 shrink-0">
<UITabs v-model="currentLocale" size="sm" class="gap-0" :items="languageOptions" />
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('common.agreement.title') }}</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('common.agreement.subtitle') }}</p>
</div>
</div>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingsStore } from '@/stores/settings'
import UITabs from '@/components/UI/Tabs.vue'
import { availableLocales, type LocaleType } from '@/i18n'
const emit = defineEmits<{
(e: 'done'): void
}>()
const { t } = useI18n()
const settingsStore = useSettingsStore()
const LOCALE_SET_KEY = 'chatlab_locale_set_by_user'
const isOpen = ref(false)
const tabItems = computed(() =>
availableLocales.map((l) => ({
label: l.nativeName,
value: l.code,
}))
)
const currentLocale = computed({
get: () => settingsStore.locale,
set: (val: string | number) => settingsStore.setLocale(val as LocaleType),
})
onMounted(() => {
const hasUserSetLocale = localStorage.getItem(LOCALE_SET_KEY)
if (!hasUserSetLocale) {
isOpen.value = true
}
})
function handleNext() {
localStorage.setItem(LOCALE_SET_KEY, 'true')
isOpen.value = false
emit('done')
}
/**
* 是否因为已有语言设置而跳过了弹窗(供父组件判断流程)
*/
function wasSkipped(): boolean {
return !!localStorage.getItem(LOCALE_SET_KEY)
}
defineExpose({ wasSkipped })
</script>
<template>
<UModal
:open="isOpen"
prevent-close
:ui="{
content: 'sm:max-w-md',
overlay: 'backdrop-blur-sm',
}"
>
<template #content>
<div class="language-select-modal flex flex-col p-6">
<!-- Header -->
<div class="mb-6 flex flex-col items-center text-center">
<div
class="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-linear-to-br from-pink-100 to-rose-100 dark:from-pink-900/30 dark:to-rose-900/30"
>
<UIcon name="i-heroicons-language" class="h-7 w-7 text-pink-600 dark:text-pink-400" />
</div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
{{ t('common.languageSelect.title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t('common.languageSelect.subtitle') }}
</p>
</div>
<!-- Language Tabs -->
<div class="mb-6 flex justify-center">
<UITabs v-model="currentLocale" size="md" :items="tabItems" />
</div>
<!-- Next Button -->
<UButton
block
color="primary"
size="lg"
class="bg-pink-500 hover:bg-pink-600 dark:bg-pink-600 dark:hover:bg-pink-700"
@click="handleNext"
>
{{ t('common.languageSelect.next') }}
</UButton>
</div>
</template>
</UModal>
</template>
<style scoped>
.language-select-modal {
-webkit-app-region: no-drag;
}
</style>
+20 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import LanguageSelectModal from './components/LanguageSelectModal.vue'
import AgreementModal from './components/AgreementModal.vue'
import MigrationModal from './components/MigrationModal.vue'
import ImportArea from './components/ImportArea.vue'
@@ -10,9 +11,24 @@ import HomeFooter from './components/HomeFooter.vue'
const { t } = useI18n()
// 弹窗引用
const languageSelectRef = ref<InstanceType<typeof LanguageSelectModal> | null>(null)
const changelogModalRef = ref<InstanceType<typeof ChangelogModal> | null>(null)
const agreementModalRef = ref<InstanceType<typeof AgreementModal> | null>(null)
onMounted(() => {
// 已有语言设置(非首次用户)→ 直接检查协议
if (languageSelectRef.value?.wasSkipped() && agreementModalRef.value?.needsAgreement()) {
agreementModalRef.value.open()
}
})
// 语言选择完成后,检查是否需要显示协议弹窗
function onLanguageSelectDone() {
if (agreementModalRef.value?.needsAgreement()) {
agreementModalRef.value.open()
}
}
// 打开版本日志弹窗(手动点击时调用)
async function openChangelog() {
changelogModalRef.value?.open()
@@ -84,6 +100,9 @@ const features = computed(() => [
<HomeFooter @open-changelog="openChangelog" @open-terms="openTerms" />
</div>
<!-- 新用户语言选择弹窗 -->
<LanguageSelectModal ref="languageSelectRef" @done="onLanguageSelectDone" />
<!-- 用户协议弹窗 -->
<AgreementModal ref="agreementModalRef" />