feat: richer copyright and sidebar preferences

pull/48/MERGE
vben 2024-07-06 17:25:38 +08:00
parent 13f3af96b7
commit 5976e255fb
23 changed files with 422 additions and 19 deletions

View File

@ -9,6 +9,7 @@
"taze",
"acmr",
"antd",
"lucide",
"brotli",
"defu",
"iconify",

View File

@ -49,6 +49,7 @@
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"class-variance-authority": "^0.7.0",
"lucide-vue-next": "^0.400.0",
"radix-vue": "^1.9.0",
"vue": "^3.4.31",
"vue-sonner": "^1.1.3"

View File

@ -34,6 +34,8 @@ export * from './ui/checkbox';
export * from './ui/dialog';
export * from './ui/dropdown-menu';
export * from './ui/hover-card';
export * from './ui/input';
export * from './ui/number-field';
export * from './ui/pin-input';
export * from './ui/popover';
export * from './ui/scroll-area';

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
import { useVModel } from '@vueuse/core';
const props = defineProps<{
class?: HTMLAttributes['class'];
defaultValue?: number | string;
modelValue?: number | string;
}>();
const emits = defineEmits<{
(e: 'update:modelValue', payload: number | string): void;
}>();
const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
passive: true,
});
</script>
<template>
<input
v-model="modelValue"
:class="
cn(
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
/>
</template>

View File

@ -0,0 +1 @@
export { default as Input } from './Input.vue';

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { NumberFieldRootEmits, NumberFieldRootProps } from 'radix-vue';
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import { NumberFieldRoot, useForwardPropsEmits } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & NumberFieldRootProps
>();
const emits = defineEmits<NumberFieldRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<NumberFieldRoot v-bind="forwarded" :class="cn('grid gap-1.5', props.class)">
<slot></slot>
</NumberFieldRoot>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/toolkit';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="
cn(
'relative [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5 [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { NumberFieldDecrementProps } from 'radix-vue';
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import { Minus } from 'lucide-vue-next';
import { NumberFieldDecrement, useForwardProps } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & NumberFieldDecrementProps
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldDecrement
data-slot="decrement"
v-bind="forwarded"
:class="
cn(
'absolute left-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',
props.class,
)
"
>
<slot>
<Minus class="h-4 w-4" />
</slot>
</NumberFieldDecrement>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { NumberFieldIncrementProps } from 'radix-vue';
import { type HTMLAttributes, computed } from 'vue';
import { cn } from '@vben-core/toolkit';
import { Plus } from 'lucide-vue-next';
import { NumberFieldIncrement, useForwardProps } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & NumberFieldIncrementProps
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<NumberFieldIncrement
data-slot="increment"
v-bind="forwarded"
:class="
cn(
'absolute right-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20',
props.class,
)
"
>
<slot>
<Plus class="h-4 w-4" />
</slot>
</NumberFieldIncrement>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import { cn } from '@vben-core/toolkit';
import { NumberFieldInput } from 'radix-vue';
</script>
<template>
<NumberFieldInput
:class="
cn(
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent py-1 text-center text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
)
"
data-slot="input"
/>
</template>

View File

@ -0,0 +1,5 @@
export { default as NumberField } from './NumberField.vue';
export { default as NumberFieldContent } from './NumberFieldContent.vue';
export { default as NumberFieldDecrement } from './NumberFieldDecrement.vue';
export { default as NumberFieldIncrement } from './NumberFieldIncrement.vue';
export { default as NumberFieldInput } from './NumberFieldInput.vue';

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { useSlots } from 'vue';
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceSelectItem',
});
withDefaults(
defineProps<{
disabled?: boolean;
items?: SelectListItem[];
placeholder?: string;
}>(),
{
disabled: false,
placeholder: '',
items: () => [],
},
);
const inputValue = defineModel<string>();
const slots = useSlots();
</script>
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
>
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<template #trigger>
<MdiQuestionMarkCircleOutline class="ml-1 cursor-help" />
</template>
<slot name="tip"></slot>
</VbenTooltip>
</span>
<Input v-model="inputValue" class="h-8 w-[160px]" />
</div>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import InputItem from '../input-item.vue';
import SwitchItem from '../switch-item.vue';
defineOptions({
@ -8,10 +9,34 @@ defineOptions({
});
const copyrightEnable = defineModel<boolean>('copyrightEnable');
const copyrightDate = defineModel<string>('copyrightDate');
const copyrightIcp = defineModel<string>('copyrightIcp');
const copyrightIcpLink = defineModel<string>('copyrightIcpLink');
const copyrightCompanyName = defineModel<string>('copyrightCompanyName');
const copyrightCompanySiteLink = defineModel<string>(
'copyrightCompanySiteLink',
);
</script>
<template>
<SwitchItem v-model="copyrightEnable">
{{ $t('preferences.copyright.enable') }}
</SwitchItem>
<InputItem v-model="copyrightCompanyName" :disabled="!copyrightEnable">
{{ $t('preferences.copyright.company-name') }}
</InputItem>
<InputItem v-model="copyrightCompanySiteLink" :disabled="!copyrightEnable">
{{ $t('preferences.copyright.company-site-link') }}
</InputItem>
<InputItem v-model="copyrightDate" :disabled="!copyrightEnable">
{{ $t('preferences.copyright.date') }}
</InputItem>
<InputItem v-model="copyrightIcp" :disabled="!copyrightEnable">
{{ $t('preferences.copyright.icp') }}
</InputItem>
<InputItem v-model="copyrightIcpLink" :disabled="!copyrightEnable">
{{ $t('preferences.copyright.icp-link') }}
</InputItem>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import NumberFieldItem from '../number-field-item.vue';
import SwitchItem from '../switch-item.vue';
defineOptions({
@ -10,6 +11,7 @@ defineOptions({
defineProps<{ disabled: boolean }>();
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarWidth = defineModel<number>('sidebarWidth');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
@ -18,15 +20,24 @@ const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
<template>
<SwitchItem v-model="sidebarEnable" :disabled="disabled">
{{ $t('preferences.side-visible') }}
{{ $t('preferences.sidebar.visible') }}
</SwitchItem>
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
{{ $t('preferences.collapse') }}
{{ $t('preferences.sidebar.collapsed') }}
</SwitchItem>
<SwitchItem
v-model="sidebarCollapsedShowTitle"
:disabled="!sidebarEnable || disabled"
>
{{ $t('preferences.collapse-show-title') }}
{{ $t('preferences.sidebar.collapsed-show-title') }}
</SwitchItem>
<NumberFieldItem
v-model="sidebarWidth"
:disabled="!sidebarEnable || disabled"
:max="320"
:min="160"
:step="10"
>
{{ $t('preferences.sidebar.width') }}
</NumberFieldItem>
</template>

View File

@ -20,7 +20,4 @@ const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }}
</SwitchItem>
<!-- <SwitchItem v-model="sideCollapseShowTitle" :disabled="!tabsVisible">
{{ $t('preferences.collapse-show-title') }}
</SwitchItem> -->
</template>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { useSlots } from 'vue';
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
import {
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
VbenTooltip,
} from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceSelectItem',
});
withDefaults(
defineProps<{
disabled?: boolean;
items?: SelectListItem[];
placeholder?: string;
}>(),
{
disabled: false,
placeholder: '',
items: () => [],
},
);
const inputValue = defineModel<number>();
const slots = useSlots();
</script>
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
>
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<template #trigger>
<MdiQuestionMarkCircleOutline class="ml-1 cursor-help" />
</template>
<slot name="tip"></slot>
</VbenTooltip>
</span>
<NumberField v-model="inputValue" v-bind="$attrs" class="w-[160px]">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</template>

View File

@ -54,7 +54,7 @@ const slots = useSlots();
</VbenTooltip>
</span>
<Select v-model="selectValue">
<SelectTrigger class="h-7 w-[140px]">
<SelectTrigger class="h-8 w-[160px]">
<SelectValue :placeholder="placeholder" />
</SelectTrigger>
<SelectContent>

View File

@ -26,7 +26,7 @@ function handleClick() {
:class="{
'pointer-events-none opacity-50': disabled,
}"
class="hover:bg-accent my-1 flex w-full items-center justify-between rounded-md px-2 py-2"
class="hover:bg-accent my-1 flex w-full items-center justify-between rounded-md px-2 py-2.5"
@click="handleClick"
>
<span class="flex items-center text-sm">

View File

@ -19,7 +19,12 @@ import Preferences from './preferences.vue';
:breadcrumb-show-home="preferences.breadcrumb.showHome"
:breadcrumb-show-icon="preferences.breadcrumb.showIcon"
:breadcrumb-style-type="preferences.breadcrumb.styleType"
:copyright-company-name="preferences.copyright.companyName"
:copyright-company-site-link="preferences.copyright.companySiteLink"
:copyright-date="preferences.copyright.date"
:copyright-enable="preferences.copyright.enable"
:copyright-icp="preferences.copyright.icp"
:copyright-icp-link="preferences.copyright.icpLink"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
:header-enable="preferences.header.enable"
@ -36,6 +41,7 @@ import Preferences from './preferences.vue';
:sidebar-collapsed="preferences.sidebar.collapsed"
:sidebar-collapsed-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="preferences.sidebar.enable"
:sidebar-width="preferences.sidebar.width"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-show-icon="preferences.tabbar.showIcon"
:theme-builtin-type="preferences.theme.builtinType"
@ -86,9 +92,24 @@ import Preferences from './preferences.vue';
@update:breadcrumb-style-type="
(val) => updatePreferences({ breadcrumb: { styleType: val } })
"
@update:copyright-company-name="
(val) => updatePreferences({ copyright: { companyName: val } })
"
@update:copyright-company-site-link="
(val) => updatePreferences({ copyright: { companySiteLink: val } })
"
@update:copyright-date="
(val) => updatePreferences({ copyright: { date: val } })
"
@update:copyright-enable="
(val) => updatePreferences({ copyright: { enable: val } })
"
@update:copyright-icp="
(val) => updatePreferences({ copyright: { icp: val } })
"
@update:copyright-icp-link="
(val) => updatePreferences({ copyright: { icpLink: val } })
"
@update:footer-enable="
(val) => updatePreferences({ footer: { enable: val } })
"
@ -129,6 +150,9 @@ import Preferences from './preferences.vue';
@update:sidebar-enable="
(val) => updatePreferences({ sidebar: { enable: val } })
"
@update:sidebar-width="
(val) => updatePreferences({ sidebar: { width: val } })
"
@update:tabbar-enable="
(val) => updatePreferences({ tabbar: { enable: val } })
"

View File

@ -77,6 +77,7 @@ const themeMode = defineModel<ThemeModeType>('themeMode');
const themeRadius = defineModel<string>('themeRadius');
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarWidth = defineModel<number>('sidebarWidth');
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
@ -108,6 +109,13 @@ const footerEnable = defineModel<boolean>('footerEnable');
const footerFixed = defineModel<boolean>('footerFixed');
const copyrightEnable = defineModel<boolean>('copyrightEnable');
const copyrightCompanyName = defineModel<string>('copyrightCompanyName');
const copyrightCompanySiteLink = defineModel<string>(
'copyrightCompanySiteLink',
);
const copyrightDate = defineModel<string>('copyrightDate');
const copyrightIcp = defineModel<string>('copyrightIcp');
const copyrightIcpLink = defineModel<string>('copyrightIcpLink');
const shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');
const shortcutKeysGlobalSearch = defineModel<boolean>(
@ -272,11 +280,12 @@ async function handleReset() {
<Content v-model="appContentCompact" />
</Block>
<Block :title="$t('preferences.sidebar')">
<Block :title="$t('preferences.sidebar.title')">
<Sidebar
v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable"
v-model:sidebar-width="sidebarWidth"
:disabled="!isSideMode"
/>
</Block>
@ -325,7 +334,14 @@ async function handleReset() {
/>
</Block>
<Block :title="$t('preferences.copyright.title')">
<Copyright v-model:copyright-enable="copyrightEnable" />
<Copyright
v-model:copyright-company-name="copyrightCompanyName"
v-model:copyright-company-site-link="copyrightCompanySiteLink"
v-model:copyright-date="copyrightDate"
v-model:copyright-enable="copyrightEnable"
v-model:copyright-icp="copyrightIcp"
v-model:copyright-icp-link="copyrightIcpLink"
/>
</Block>
</template>

View File

@ -160,15 +160,11 @@ preferences:
normal: Normal
plain: Plain
rounded: Rounded
collapse: Collpase Menu
collapse-show-title: Display menu name
interface-control: Interface Layout Control
copy: Copy Preferences
copy-success: Copy successful. Please replace in `src/preferences.ts` of the app
clear-and-logout: Clear Cache & Logout
reset-success: Preferences reset successfully
sidebar: Sidebar
side-visible: Display Sidebar
mode: Mode
logo-visible: Display Logo
# general
@ -176,6 +172,12 @@ preferences:
language: Language
dynamic-title: Dynamic Title
ai-assistant: Ai Assistant
sidebar:
title: Sidebar
width: Width
visible: Display Sidebar
collapsed: Collpase Menu
collapsed-show-title: Display menu name
tabbar:
title: Tabbar
enable: Enable Tab Bar
@ -246,7 +248,12 @@ preferences:
fixed: Display Footer
copyright:
title: Copyright
enable: Enable CopyRight
enable: Enable copyright
company-name: Company name
company-site-link: Company homepage
date: Date
icp: ICP number
icp-link: ICP Site Link
shortcut-keys:
title: Shortcut Keys
global: Global

View File

@ -145,8 +145,6 @@ preferences:
wide: 流式
compact: 定宽
follow-system: 跟随系统
collapse: 折叠菜单
collapse-show-title: 显示菜单名
vertical: 垂直
vertical-tip: 侧边垂直菜单模式
horizontal: 水平
@ -166,8 +164,6 @@ preferences:
copy-success: 拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖
clear-and-logout: 清空缓存 & 退出登录
reset-success: 重置偏好设置成功
sidebar: 侧边栏
side-visible: 显示侧边栏
mode: 模式
logo-visible: 显示 Logo
# general
@ -175,6 +171,12 @@ preferences:
language: 语言
dynamic-title: 动态标题
ai-assistant: Ai 助手
sidebar:
title: 侧边栏
width: 宽度
visible: 显示侧边栏
collapsed: 折叠菜单
collapsed-show-title: 显示菜单名
tabbar:
title: 标签栏
enable: 启用标签栏
@ -246,6 +248,11 @@ preferences:
copyright:
title: 版权
enable: 启用版权
company-name: 公司名
company-site-link: 公司主页
date: 日期
icp: ICP 备案号
icp-link: ICP 网站链接
shortcut-keys:
title: 快捷键
global: 全局

View File

@ -760,6 +760,9 @@ importers:
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
lucide-vue-next:
specifier: ^0.400.0
version: 0.400.0(vue@3.4.31(typescript@5.5.3))
radix-vue:
specifier: ^1.9.0
version: 1.9.0(vue@3.4.31(typescript@5.5.3))
@ -6552,6 +6555,11 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-vue-next@0.400.0:
resolution: {integrity: sha512-JQMby6HuSr/ALLL3IAjpca/hP499vWy4+zqzCrTsAzdg0BHM0Lge84bMMxvpqqXnU24uRQkmOZCi5ksecTogfw==}
peerDependencies:
vue: ^3.4.31
magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@ -15771,6 +15779,10 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-vue-next@0.400.0(vue@3.4.31(typescript@5.5.3)):
dependencies:
vue: 3.4.31(typescript@5.5.3)
magic-string@0.25.9:
dependencies:
sourcemap-codec: 1.4.8