mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-03 19:51:17 +08:00
feat: 设置页面重构
This commit is contained in:
@@ -5,12 +5,10 @@ import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import TitleBar from '@/components/common/TitleBar.vue'
|
||||
import Sidebar from '@/components/common/Sidebar.vue'
|
||||
import SettingModal from '@/components/common/SettingModal.vue'
|
||||
import ScreenCaptureModal from '@/components/common/ScreenCaptureModal.vue'
|
||||
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useLLMStore } from '@/stores/llm'
|
||||
|
||||
@@ -18,7 +16,6 @@ const { t } = useI18n()
|
||||
|
||||
const sessionStore = useSessionStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const promptStore = usePromptStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const llmStore = useLLMStore()
|
||||
const { isInitialized } = storeToRefs(sessionStore)
|
||||
@@ -72,7 +69,6 @@ onMounted(async () => {
|
||||
</main>
|
||||
</template>
|
||||
</div>
|
||||
<SettingModal v-model:open="layoutStore.showSettingModal" @ai-config-saved="promptStore.notifyAIConfigChanged" />
|
||||
<ScreenCaptureModal
|
||||
:open="layoutStore.showScreenCaptureModal"
|
||||
:image-data="layoutStore.screenCaptureImage"
|
||||
|
||||
@@ -3,14 +3,15 @@ import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useLLMStore } from '@/stores/llm'
|
||||
import { exportConversation, type ExportFormat } from '@/utils/conversationExport'
|
||||
import type { AgentRuntimeStatus } from '@electron/shared/types'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -24,7 +25,6 @@ const props = defineProps<{
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const llmStore = useLLMStore()
|
||||
const { aiPromptSettings, activePreset, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { configs, activeConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
|
||||
@@ -112,15 +112,13 @@ function setActivePreset(presetId: string) {
|
||||
isPresetPopoverOpen.value = false
|
||||
}
|
||||
|
||||
// 打开设置弹窗并跳转到预设配置
|
||||
function openPresetSettings() {
|
||||
isPresetPopoverOpen.value = false
|
||||
layoutStore.openSettingAt('ai', 'preset')
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'preset' } })
|
||||
}
|
||||
|
||||
// 打开设置弹窗并跳转到对话配置(消息条数限制)
|
||||
function openChatSettings() {
|
||||
layoutStore.openSettingAt('ai', 'chat')
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'chat' } })
|
||||
}
|
||||
|
||||
// 切换 AI 模型配置
|
||||
@@ -138,10 +136,9 @@ async function switchModelConfig(configId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开设置弹窗并跳转到模型配置
|
||||
function openModelSettings() {
|
||||
isModelPopoverOpen.value = false
|
||||
layoutStore.openSettingAt('ai', 'model')
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'model' } })
|
||||
}
|
||||
|
||||
// 导出当前对话
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import AISettingsTab from './settings/AISettingsTab.vue'
|
||||
import BasicSettingsTab from './settings/BasicSettingsTab.vue'
|
||||
import StorageTab from './settings/StorageTab.vue'
|
||||
import AboutTab from './settings/AboutTab.vue'
|
||||
import SubTabs from '@/components/UI/SubTabs.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// 可滚动 Tab 的通用接口(支持 section 跳转的 Tab 需实现此接口)
|
||||
interface ScrollableTab {
|
||||
scrollToSection?: (sectionId: string) => void
|
||||
refresh?: () => void
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
'ai-config-saved': []
|
||||
}>()
|
||||
|
||||
// Tab 配置(使用 computed 以便语言切换时自动更新)
|
||||
const tabs = computed(() => [
|
||||
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
|
||||
{ id: 'ai', label: t('settings.tabs.ai'), icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'storage', label: t('settings.tabs.storage'), icon: 'i-heroicons-folder-open' },
|
||||
{ id: 'about', label: t('settings.tabs.about'), icon: 'i-heroicons-information-circle' },
|
||||
])
|
||||
|
||||
const activeTab = ref('settings')
|
||||
|
||||
// 统一的 Tab 引用管理(通过 setTabRef 动态设置)
|
||||
const tabRefs = ref<Record<string, ScrollableTab | null>>({})
|
||||
|
||||
/**
|
||||
* 设置 Tab 引用(在模板中通过 :ref 调用)
|
||||
*/
|
||||
function setTabRef(tabId: string, el: unknown) {
|
||||
tabRefs.value[tabId] = el as ScrollableTab | null
|
||||
}
|
||||
|
||||
// AI 配置变更回调
|
||||
function handleAIConfigChanged() {
|
||||
emit('ai-config-saved')
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeModal() {
|
||||
emit('update:open', false)
|
||||
layoutStore.clearSettingTarget()
|
||||
}
|
||||
|
||||
// 监听打开状态
|
||||
watch(
|
||||
() => props.open,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
// 检查是否有指定的跳转目标
|
||||
const target = layoutStore.settingTarget
|
||||
if (target) {
|
||||
activeTab.value = target.tab
|
||||
// 如果有指定 section,等待渲染后滚动(通用逻辑)
|
||||
if (target.section) {
|
||||
await nextTick()
|
||||
// 延迟一下确保目标 Tab 已渲染
|
||||
setTimeout(() => {
|
||||
const tabRef = tabRefs.value[target.tab]
|
||||
tabRef?.scrollToSection?.(target.section!)
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
activeTab.value = 'settings' // 默认打开基础设置 Tab
|
||||
}
|
||||
// 刷新存储管理
|
||||
tabRefs.value['storage']?.refresh?.()
|
||||
} else {
|
||||
// 弹窗关闭时清空 target
|
||||
layoutStore.clearSettingTarget()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 Tab 切换,刷新对应数据
|
||||
watch(
|
||||
() => activeTab.value,
|
||||
(newTab) => {
|
||||
// 通用刷新逻辑
|
||||
tabRefs.value[newTab]?.refresh?.()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal
|
||||
:open="open"
|
||||
:ui="{ overlay: 'app-region-no-drag', content: 'md:w-full max-w-2xl app-region-no-drag' }"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('settings.title') }}</h2>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<div class="mb-6 -mx-6">
|
||||
<SubTabs v-model="activeTab" :items="tabs" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 内容 -->
|
||||
<div class="h-[500px] overflow-y-auto">
|
||||
<!-- 基础设置 -->
|
||||
<div v-show="activeTab === 'settings'">
|
||||
<BasicSettingsTab />
|
||||
</div>
|
||||
|
||||
<!-- AI 设置 -->
|
||||
<div v-show="activeTab === 'ai'" class="h-full">
|
||||
<AISettingsTab :ref="(el) => setTabRef('ai', el)" @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- 存储管理 -->
|
||||
<div v-show="activeTab === 'storage'" class="h-full">
|
||||
<StorageTab :ref="(el) => setTabRef('storage', el)" />
|
||||
</div>
|
||||
|
||||
<!-- 关于 -->
|
||||
<div v-show="activeTab === 'about'">
|
||||
<AboutTab />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -2,35 +2,23 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import SidebarButton from './SidebarButton.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// 是否在管理页面
|
||||
const isManagePage = computed(() => route.name === 'manage')
|
||||
|
||||
// 注意:帮助和反馈功能已迁移到首页 Footer (HomeFooter.vue)
|
||||
// 如需恢复,请参考 git 历史记录
|
||||
const isSettingsPage = computed(() => route.name === 'settings')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-4 py-2 dark:border-gray-800 space-y-2 mb-2">
|
||||
<!-- 管理 -->
|
||||
<SidebarButton
|
||||
icon="i-heroicons-rectangle-stack"
|
||||
:title="t('tools.title')"
|
||||
:active="isManagePage"
|
||||
@click="router.push({ name: 'manage' })"
|
||||
/>
|
||||
<!-- 设置 -->
|
||||
<SidebarButton
|
||||
icon="i-heroicons-cog-6-tooth"
|
||||
:title="t('layout.footer.settings')"
|
||||
@click="layoutStore.showSettingModal = true"
|
||||
:active="isSettingsPage"
|
||||
@click="router.push({ name: 'settings' })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"aiPrompt": "Chat Config",
|
||||
"aiPreset": "Prompts",
|
||||
"aiPreprocess": "Preprocess",
|
||||
"storage": "Data & Storage",
|
||||
"dataManage": "Data Management",
|
||||
"storage": "Storage",
|
||||
"storageManage": "Storage",
|
||||
"sessionManage": "Sessions",
|
||||
"about": "About"
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"aiPrompt": "チャット設定",
|
||||
"aiPreset": "プロンプト設定",
|
||||
"aiPreprocess": "前処理",
|
||||
"storage": "データとストレージ",
|
||||
"dataManage": "データ管理",
|
||||
"storage": "ストレージ管理",
|
||||
"storageManage": "ストレージ管理",
|
||||
"sessionManage": "セッション管理",
|
||||
"about": "ChatLabについて"
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"aiPrompt": "对话配置",
|
||||
"aiPreset": "提示词配置",
|
||||
"aiPreprocess": "预处理",
|
||||
"storage": "数据和存储",
|
||||
"dataManage": "数据管理",
|
||||
"storage": "存储管理",
|
||||
"storageManage": "存储管理",
|
||||
"sessionManage": "会话管理",
|
||||
"about": "关于"
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"aiPrompt": "聊天設定",
|
||||
"aiPreset": "提示詞設定",
|
||||
"aiPreprocess": "前處理",
|
||||
"storage": "資料與儲存",
|
||||
"dataManage": "資料管理",
|
||||
"storage": "儲存管理",
|
||||
"storageManage": "儲存管理",
|
||||
"sessionManage": "會話管理",
|
||||
"about": "關於 ChatLab"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BatchManageTab from './components/BatchManageTab.vue'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative z-0 flex h-full flex-col bg-white pt-4 dark:bg-[var(--color-page-dark)]">
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
:title="t('tools.title')"
|
||||
:description="t('tools.description')"
|
||||
icon="i-heroicons-rectangle-stack"
|
||||
icon-class="bg-primary-600 dark:bg-primary-500"
|
||||
/>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<BatchManageTab />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
146
src/pages/settings/index.vue
Normal file
146
src/pages/settings/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import AISettingsTab from './components/AISettingsTab.vue'
|
||||
import BasicSettingsTab from './components/BasicSettingsTab.vue'
|
||||
import BatchManageTab from './components/BatchManageTab.vue'
|
||||
import StorageTab from './components/StorageTab.vue'
|
||||
import AboutTab from './components/AboutTab.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
interface ScrollableTab {
|
||||
scrollToSection?: (sectionId: string) => void
|
||||
refresh?: () => void
|
||||
}
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
|
||||
{ id: 'ai', label: t('settings.tabs.ai'), icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'data', label: t('settings.tabs.dataManage'), icon: 'i-heroicons-rectangle-stack' },
|
||||
{ id: 'storage', label: t('settings.tabs.storage'), icon: 'i-heroicons-folder-open' },
|
||||
{ id: 'about', label: t('settings.tabs.about'), icon: 'i-heroicons-information-circle' },
|
||||
])
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'settings')
|
||||
|
||||
const tabRefs = ref<Record<string, ScrollableTab | null>>({})
|
||||
|
||||
function setTabRef(tabId: string, el: unknown) {
|
||||
tabRefs.value[tabId] = el as ScrollableTab | null
|
||||
}
|
||||
|
||||
function handleAIConfigChanged() {
|
||||
promptStore.notifyAIConfigChanged()
|
||||
}
|
||||
|
||||
function switchTab(tabId: string) {
|
||||
activeTab.value = tabId
|
||||
const query: Record<string, string> = { tab: tabId }
|
||||
router.replace({ query })
|
||||
|
||||
nextTick(() => {
|
||||
tabRefs.value[tabId]?.refresh?.()
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToSubTab(subTab: string) {
|
||||
const tabRef = tabRefs.value[activeTab.value]
|
||||
if (tabRef?.scrollToSection) {
|
||||
tabRef.scrollToSection(subTab)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
async (query) => {
|
||||
const tab = (query.tab as string) || 'settings'
|
||||
if (tab !== activeTab.value) {
|
||||
activeTab.value = tab
|
||||
}
|
||||
const subTab = query.subTab as string
|
||||
if (subTab) {
|
||||
await nextTick()
|
||||
setTimeout(() => scrollToSubTab(subTab), 100)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
const subTab = route.query.subTab as string
|
||||
if (subTab) {
|
||||
await nextTick()
|
||||
setTimeout(() => scrollToSubTab(subTab), 100)
|
||||
}
|
||||
tabRefs.value[activeTab.value]?.refresh?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col bg-white dark:bg-gray-900" style="padding-top: var(--titlebar-area-height)">
|
||||
<PageHeader
|
||||
:title="t('settings.title')"
|
||||
icon="i-heroicons-cog-6-tooth"
|
||||
icon-class="bg-primary-600 text-white dark:bg-primary-500 dark:text-white"
|
||||
>
|
||||
<div class="mt-4 flex items-center gap-1 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
|
||||
]"
|
||||
@click="switchTab(tab.id)"
|
||||
>
|
||||
<UIcon :name="tab.icon" class="h-4 w-4" />
|
||||
<span class="whitespace-nowrap">{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div class="relative flex-1 overflow-y-auto">
|
||||
<div class="h-full p-6">
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
<BasicSettingsTab v-if="activeTab === 'settings'" key="settings" />
|
||||
<AISettingsTab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
key="ai"
|
||||
:ref="(el) => setTabRef('ai', el)"
|
||||
@config-changed="handleAIConfigChanged"
|
||||
/>
|
||||
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
|
||||
<StorageTab v-else-if="activeTab === 'storage'" key="storage" :ref="(el) => setTabRef('storage', el)" />
|
||||
<AboutTab v-else-if="activeTab === 'about'" key="about" />
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-slide-enter-active,
|
||||
.tab-slide-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.tab-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
@@ -18,9 +18,9 @@ export const router = createRouter({
|
||||
component: () => import('@/pages/private-chat/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/manage',
|
||||
name: 'manage',
|
||||
component: () => import('@/pages/manage/index.vue'),
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/pages/settings/index.vue'),
|
||||
},
|
||||
],
|
||||
history: createWebHashHistory(),
|
||||
|
||||
@@ -9,18 +9,11 @@ export const useLayoutStore = defineStore(
|
||||
'layout',
|
||||
() => {
|
||||
const isSidebarCollapsed = ref(false)
|
||||
const showSettingModal = ref(false)
|
||||
const showScreenCaptureModal = ref(false)
|
||||
const screenCaptureImage = ref<string | null>(null)
|
||||
const showChatRecordDrawer = ref(false)
|
||||
const chatRecordQuery = ref<ChatRecordQuery | null>(null)
|
||||
|
||||
// 设置弹窗定位目标(用于从外部跳转到设置的特定位置)
|
||||
const settingTarget = ref<{
|
||||
tab: 'settings' | 'ai' | 'storage' | 'about'
|
||||
section?: string // AI tab 下的子锚点,如 'model', 'chat', 'preset'
|
||||
} | null>(null)
|
||||
|
||||
// 截图设置
|
||||
const screenshotMobileAdapt = ref(true) // 截图时开启移动端适配,默认开启
|
||||
|
||||
@@ -67,39 +60,18 @@ export const useLayoutStore = defineStore(
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置弹窗并定位到指定位置
|
||||
* @param tab 要跳转的 Tab(settings, ai, storage, about)
|
||||
* @param section 子锚点(仅 ai tab 支持:model, chat, preset)
|
||||
*/
|
||||
function openSettingAt(tab: 'settings' | 'ai' | 'storage' | 'about', section?: string) {
|
||||
settingTarget.value = { tab, section }
|
||||
showSettingModal.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空设置目标(弹窗关闭后调用)
|
||||
*/
|
||||
function clearSettingTarget() {
|
||||
settingTarget.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
isSidebarCollapsed,
|
||||
showSettingModal,
|
||||
showScreenCaptureModal,
|
||||
screenCaptureImage,
|
||||
showChatRecordDrawer,
|
||||
chatRecordQuery,
|
||||
settingTarget,
|
||||
screenshotMobileAdapt,
|
||||
toggleSidebar,
|
||||
openScreenCaptureModal,
|
||||
closeScreenCaptureModal,
|
||||
openChatRecordDrawer,
|
||||
closeChatRecordDrawer,
|
||||
openSettingAt,
|
||||
clearSettingTarget,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user