mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-19 04:49:36 +08:00
feat: 新用户首先弹出语言选择弹窗
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -85,6 +85,11 @@
|
||||
"nov": "11月",
|
||||
"dec": "12月"
|
||||
},
|
||||
"languageSelect": {
|
||||
"title": "言語を選択",
|
||||
"subtitle": "ご希望のインターフェース言語を選択してください",
|
||||
"next": "次へ"
|
||||
},
|
||||
"agreement": {
|
||||
"title": "プライバシーポリシーと利用規約",
|
||||
"subtitle": "利用前に必ずお読みください",
|
||||
|
||||
@@ -85,6 +85,11 @@
|
||||
"nov": "11月",
|
||||
"dec": "12月"
|
||||
},
|
||||
"languageSelect": {
|
||||
"title": "选择语言",
|
||||
"subtitle": "请选择您偏好的界面语言",
|
||||
"next": "下一步"
|
||||
},
|
||||
"agreement": {
|
||||
"title": "隐私政策与用户协议",
|
||||
"subtitle": "使用前请仔细阅读",
|
||||
|
||||
@@ -85,6 +85,11 @@
|
||||
"nov": "11月",
|
||||
"dec": "12月"
|
||||
},
|
||||
"languageSelect": {
|
||||
"title": "選擇語言",
|
||||
"subtitle": "請選擇您偏好的介面語言",
|
||||
"next": "下一步"
|
||||
},
|
||||
"agreement": {
|
||||
"title": "隱私權政策與使用條款",
|
||||
"subtitle": "使用前請先詳閱",
|
||||
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user