feat: 优化设置页面

This commit is contained in:
alger
2025-12-19 00:22:22 +08:00
parent e2ebbe12e4
commit 70f1044dd9
4 changed files with 824 additions and 936 deletions

View File

@@ -0,0 +1,79 @@
<template>
<div
class="flex items-center justify-between p-4 rounded-lg transition-all bg-light dark:bg-dark text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 hover:dark:bg-gray-800"
:class="[
// 移动端垂直布局
{ 'max-md:flex-col max-md:items-start max-md:gap-3 max-md:p-3': !inline },
// 可点击样式
{
'cursor-pointer hover:text-green-500 hover:!bg-green-50 hover:dark:!bg-green-900/30':
clickable
},
customClass
]"
@click="handleClick"
>
<!-- 左侧标题和描述 -->
<div class="flex-1 min-w-0">
<div class="text-base font-medium mb-1">
<slot name="title">{{ title }}</slot>
</div>
<div
v-if="description || $slots.description"
class="text-sm text-gray-500 dark:text-gray-400"
>
<slot name="description">{{ description }}</slot>
</div>
<!-- 额外内容插槽 -->
<slot name="extra"></slot>
</div>
<!-- 右侧操作区 -->
<div
v-if="$slots.action || $slots.default"
class="flex items-center gap-2 flex-shrink-0"
:class="{ 'max-md:w-full max-md:justify-end': !inline }"
>
<slot name="action">
<slot></slot>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'SettingItem'
});
interface Props {
/** 设置项标题 */
title?: string;
/** 设置项描述 */
description?: string;
/** 是否可点击 */
clickable?: boolean;
/** 是否保持水平布局(不响应移动端) */
inline?: boolean;
/** 自定义类名 */
customClass?: string;
}
const props = withDefaults(defineProps<Props>(), {
title: '',
description: '',
clickable: false,
inline: false,
customClass: ''
});
const emit = defineEmits<{
click: [event: MouseEvent];
}>();
const handleClick = (event: MouseEvent) => {
if (props.clickable) {
emit('click', event);
}
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div
class="w-32 h-full flex-shrink-0 border-r border-gray-200 dark:border-gray-700 bg-light dark:bg-dark"
>
<div
v-for="section in sections"
:key="section.id"
class="px-4 py-2.5 cursor-pointer text-sm transition-colors duration-200 border-l-2"
:class="[
currentSection === section.id
? 'text-primary dark:text-white bg-gray-50 dark:bg-dark-100 !border-primary font-medium'
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-primary hover:dark:text-white hover:bg-gray-50 hover:dark:bg-dark-100 hover:border-gray-300'
]"
@click="handleClick(section.id)"
>
{{ section.title }}
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'SettingNav'
});
export interface NavSection {
id: string;
title: string;
}
interface Props {
/** 导航项列表 */
sections: NavSection[];
/** 当前激活的分组 ID */
currentSection: string;
}
defineProps<Props>();
const emit = defineEmits<{
navigate: [sectionId: string];
}>();
const handleClick = (sectionId: string) => {
emit('navigate', sectionId);
};
</script>

View File

@@ -0,0 +1,42 @@
<template>
<div :id="id" :ref="setRef" class="mb-6 scroll-mt-4">
<!-- 分组标题 -->
<div class="text-base font-medium mb-4 text-gray-600 dark:text-white">
<slot name="title">{{ title }}</slot>
</div>
<!-- 设置项列表 -->
<div class="space-y-4 max-md:space-y-3">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { type ComponentPublicInstance } from 'vue';
defineOptions({
name: 'SettingSection'
});
interface Props {
/** 分组 ID用于导航定位 */
id?: string;
/** 分组标题 */
title?: string;
}
withDefaults(defineProps<Props>(), {
id: '',
title: ''
});
const emit = defineEmits<{
ref: [el: Element | null];
}>();
// 暴露 ref 给父组件
const setRef = (el: Element | ComponentPublicInstance | null) => {
emit('ref', el as Element | null);
};
</script>

File diff suppressed because it is too large Load Diff