1
This commit is contained in:
BIN
ScreenShot_2026-01-29_094743_454.png
Normal file
BIN
ScreenShot_2026-01-29_094743_454.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 669 KiB |
BIN
ScreenShot_2026-01-29_101729_394.png
Normal file
BIN
ScreenShot_2026-01-29_101729_394.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
@ -4,12 +4,13 @@
|
|||||||
:groups="sidebarGroups"
|
:groups="sidebarGroups"
|
||||||
:base-overrides="sidebarOverrides"
|
:base-overrides="sidebarOverrides"
|
||||||
:hidden-base-ids="hiddenBaseIds"
|
:hidden-base-ids="hiddenBaseIds"
|
||||||
|
@select="handleSidebarSelect"
|
||||||
@add="openAddGroup"
|
@add="openAddGroup"
|
||||||
@edit-group="openEditGroup"
|
@edit-group="openEditGroup"
|
||||||
@remove-group="handleRemoveGroup"
|
@remove-group="handleRemoveGroup"
|
||||||
@settings="toggleSettings"
|
@settings="toggleSettings"
|
||||||
/>
|
/>
|
||||||
<MainContent />
|
<MainContent :active-group-id="activeGroupId" />
|
||||||
<TheContextMenu
|
<TheContextMenu
|
||||||
@add-icon="openAddIcon"
|
@add-icon="openAddIcon"
|
||||||
@edit-icon="openEditIcon"
|
@edit-icon="openEditIcon"
|
||||||
@ -99,6 +100,8 @@ const isSettingsOpen = ref(false);
|
|||||||
const addIconMode = ref<'add' | 'replace-widget'>('add');
|
const addIconMode = ref<'add' | 'replace-widget'>('add');
|
||||||
const addIconInitialTab = ref<'widgets' | 'sites' | 'custom'>('widgets');
|
const addIconInitialTab = ref<'widgets' | 'sites' | 'custom'>('widgets');
|
||||||
const replaceWidgetId = ref<string | null>(null);
|
const replaceWidgetId = ref<string | null>(null);
|
||||||
|
const defaultGroupId = 'home';
|
||||||
|
const activeGroupId = ref<string | null>(defaultGroupId);
|
||||||
const editTarget = ref<{ id: string; source: 'base' | 'group' } | null>(null);
|
const editTarget = ref<{ id: string; source: 'base' | 'group' } | null>(null);
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const widgetsStore = useWidgetsStore();
|
const widgetsStore = useWidgetsStore();
|
||||||
@ -154,6 +157,10 @@ const toggleSettings = () => {
|
|||||||
isSettingsOpen.value = !isSettingsOpen.value;
|
isSettingsOpen.value = !isSettingsOpen.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSidebarSelect = (id: string) => {
|
||||||
|
activeGroupId.value = id;
|
||||||
|
};
|
||||||
|
|
||||||
const openAddIcon = () => {
|
const openAddIcon = () => {
|
||||||
addIconMode.value = 'add';
|
addIconMode.value = 'add';
|
||||||
addIconInitialTab.value = 'widgets';
|
addIconInitialTab.value = 'widgets';
|
||||||
@ -252,6 +259,7 @@ const handleAddIcon = (payload: { name: string; url: string; bgColor: string; im
|
|||||||
bgColor: payload.bgColor,
|
bgColor: payload.bgColor,
|
||||||
img: payload.img,
|
img: payload.img,
|
||||||
text: payload.text,
|
text: payload.text,
|
||||||
|
groupId: activeGroupId.value ?? defaultGroupId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -270,6 +278,7 @@ const handleAddWidget = (payload: { component: string; size?: '1x1' | '1x2' | '2
|
|||||||
component: payload.component,
|
component: payload.component,
|
||||||
size: payload.size,
|
size: payload.size,
|
||||||
data: payload.data,
|
data: payload.data,
|
||||||
|
groupId: activeGroupId.value ?? defaultGroupId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ interface Icon {
|
|||||||
text?: string;
|
text?: string;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
size?: GridItemSize;
|
size?: GridItemSize;
|
||||||
|
groupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WidgetSize = GridItemSize;
|
type WidgetSize = GridItemSize;
|
||||||
@ -27,6 +28,7 @@ interface Widget {
|
|||||||
component: string;
|
component: string;
|
||||||
size: WidgetSize;
|
size: WidgetSize;
|
||||||
data?: any;
|
data?: any;
|
||||||
|
groupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GridOrderEntry {
|
interface GridOrderEntry {
|
||||||
@ -39,6 +41,11 @@ type OrderedGridItem =
|
|||||||
| { type: 'widget'; id: string; widget: Widget };
|
| { type: 'widget'; id: string; widget: Widget };
|
||||||
|
|
||||||
const GRID_ORDER_STORAGE_KEY = 'itab_grid_order';
|
const GRID_ORDER_STORAGE_KEY = 'itab_grid_order';
|
||||||
|
const DEFAULT_GROUP_ID = 'home';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeGroupId?: string | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const widgetsStore = useWidgetsStore();
|
const widgetsStore = useWidgetsStore();
|
||||||
@ -59,6 +66,22 @@ const previousIconIds = ref(new Set<string>());
|
|||||||
const previousWidgetIds = ref(new Set<string>());
|
const previousWidgetIds = ref(new Set<string>());
|
||||||
const layoutAnimationMs = 240;
|
const layoutAnimationMs = 240;
|
||||||
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
|
||||||
|
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));
|
||||||
|
|
||||||
|
const normalizeGridOrder = (order: GridOrderEntry[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: GridOrderEntry[] = [];
|
||||||
|
for (const entry of order) {
|
||||||
|
if (!entry || typeof entry.id !== 'string') continue;
|
||||||
|
if (entry.type !== 'icon' && entry.type !== 'widget') continue;
|
||||||
|
const key = `${entry.type}:${entry.id}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push(entry);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
const buildDefaultOrder = (): GridOrderEntry[] => [
|
const buildDefaultOrder = (): GridOrderEntry[] => [
|
||||||
...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })),
|
...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })),
|
||||||
@ -85,7 +108,7 @@ const loadGridOrder = () => {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(saved) as GridOrderEntry[];
|
const parsed = JSON.parse(saved) as GridOrderEntry[];
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
gridOrder.value = parsed.filter(item => item?.id && (item.type === 'icon' || item.type === 'widget'));
|
gridOrder.value = normalizeGridOrder(parsed);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
gridOrder.value = [];
|
gridOrder.value = [];
|
||||||
@ -100,26 +123,36 @@ const loadGridOrder = () => {
|
|||||||
const ensureOrderConsistency = () => {
|
const ensureOrderConsistency = () => {
|
||||||
const iconIds = new Set(layoutStore.icons.map(icon => icon.id));
|
const iconIds = new Set(layoutStore.icons.map(icon => icon.id));
|
||||||
const widgetIds = new Set(widgetsStore.widgets.map(widget => widget.id));
|
const widgetIds = new Set(widgetsStore.widgets.map(widget => widget.id));
|
||||||
|
const seen = new Set<string>();
|
||||||
const nextOrder: GridOrderEntry[] = [];
|
const nextOrder: GridOrderEntry[] = [];
|
||||||
|
|
||||||
|
const pushEntry = (entry: GridOrderEntry) => {
|
||||||
|
const key = `${entry.type}:${entry.id}`;
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
nextOrder.push(entry);
|
||||||
|
};
|
||||||
|
|
||||||
for (const entry of gridOrder.value) {
|
for (const entry of gridOrder.value) {
|
||||||
if (entry.type === 'icon' && iconIds.has(entry.id)) {
|
if (entry.type === 'icon' && iconIds.has(entry.id)) {
|
||||||
nextOrder.push(entry);
|
pushEntry(entry);
|
||||||
}
|
}
|
||||||
if (entry.type === 'widget' && widgetIds.has(entry.id)) {
|
if (entry.type === 'widget' && widgetIds.has(entry.id)) {
|
||||||
nextOrder.push(entry);
|
pushEntry(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const widget of widgetsStore.widgets) {
|
for (const widget of widgetsStore.widgets) {
|
||||||
if (!nextOrder.some(item => item.type === 'widget' && item.id === widget.id)) {
|
const key = `widget:${widget.id}`;
|
||||||
nextOrder.push({ id: widget.id, type: 'widget' });
|
if (!seen.has(key)) {
|
||||||
|
pushEntry({ id: widget.id, type: 'widget' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const icon of layoutStore.icons) {
|
for (const icon of layoutStore.icons) {
|
||||||
if (!nextOrder.some(item => item.type === 'icon' && item.id === icon.id)) {
|
const key = `icon:${icon.id}`;
|
||||||
nextOrder.push({ id: icon.id, type: 'icon' });
|
if (!seen.has(key)) {
|
||||||
|
pushEntry({ id: icon.id, type: 'icon' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +170,9 @@ const ensureOrderConsistency = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isInActiveGroup = (groupId?: string | null) =>
|
||||||
|
normalizeGroupId(groupId) === activeGroupId.value;
|
||||||
|
|
||||||
const orderedItems = computed<OrderedGridItem[]>(() => {
|
const orderedItems = computed<OrderedGridItem[]>(() => {
|
||||||
const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon]));
|
const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon]));
|
||||||
const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget]));
|
const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget]));
|
||||||
@ -146,13 +182,13 @@ const orderedItems = computed<OrderedGridItem[]>(() => {
|
|||||||
for (const entry of gridOrder.value) {
|
for (const entry of gridOrder.value) {
|
||||||
if (entry.type === 'icon') {
|
if (entry.type === 'icon') {
|
||||||
const icon = iconsById.get(entry.id);
|
const icon = iconsById.get(entry.id);
|
||||||
if (icon) {
|
if (icon && isInActiveGroup(icon.groupId)) {
|
||||||
items.push({ type: 'icon', id: icon.id, icon });
|
items.push({ type: 'icon', id: icon.id, icon });
|
||||||
used.add(`icon:${icon.id}`);
|
used.add(`icon:${icon.id}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const widget = widgetsById.get(entry.id);
|
const widget = widgetsById.get(entry.id);
|
||||||
if (widget) {
|
if (widget && isInActiveGroup(widget.groupId)) {
|
||||||
items.push({ type: 'widget', id: widget.id, widget });
|
items.push({ type: 'widget', id: widget.id, widget });
|
||||||
used.add(`widget:${widget.id}`);
|
used.add(`widget:${widget.id}`);
|
||||||
}
|
}
|
||||||
@ -161,14 +197,14 @@ const orderedItems = computed<OrderedGridItem[]>(() => {
|
|||||||
|
|
||||||
for (const widget of widgetsStore.widgets) {
|
for (const widget of widgetsStore.widgets) {
|
||||||
const key = `widget:${widget.id}`;
|
const key = `widget:${widget.id}`;
|
||||||
if (!used.has(key)) {
|
if (!used.has(key) && isInActiveGroup(widget.groupId)) {
|
||||||
items.push({ type: 'widget', id: widget.id, widget });
|
items.push({ type: 'widget', id: widget.id, widget });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const icon of layoutStore.icons) {
|
for (const icon of layoutStore.icons) {
|
||||||
const key = `icon:${icon.id}`;
|
const key = `icon:${icon.id}`;
|
||||||
if (!used.has(key)) {
|
if (!used.has(key) && isInActiveGroup(icon.groupId)) {
|
||||||
items.push({ type: 'icon', id: icon.id, icon });
|
items.push({ type: 'icon', id: icon.id, icon });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,7 +258,36 @@ const persistOrderFromGrid = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nextOrder.length) {
|
if (nextOrder.length) {
|
||||||
applyGridOrder(nextOrder, true);
|
if (!gridOrder.value.length) {
|
||||||
|
applyGridOrder(nextOrder, true);
|
||||||
|
} else {
|
||||||
|
const entryKey = (entry: GridOrderEntry) => `${entry.type}:${entry.id}`;
|
||||||
|
const visibleKeys = new Set(nextOrder.map(entryKey));
|
||||||
|
const visibleQueue = [...nextOrder];
|
||||||
|
const merged: GridOrderEntry[] = [];
|
||||||
|
|
||||||
|
for (const entry of gridOrder.value) {
|
||||||
|
if (visibleKeys.has(entryKey(entry))) {
|
||||||
|
const nextEntry = visibleQueue.shift();
|
||||||
|
if (nextEntry) {
|
||||||
|
merged.push(nextEntry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedKeys = new Set(merged.map(entryKey));
|
||||||
|
for (const entry of visibleQueue) {
|
||||||
|
const key = entryKey(entry);
|
||||||
|
if (!mergedKeys.has(key)) {
|
||||||
|
merged.push(entry);
|
||||||
|
mergedKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGridOrder(merged, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await nextTick();
|
await nextTick();
|
||||||
grid.value?.synchronize();
|
grid.value?.synchronize();
|
||||||
@ -247,7 +312,11 @@ const syncGridItems = () => {
|
|||||||
const refreshLayout = async (instant = false) => {
|
const refreshLayout = async (instant = false) => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
syncGridItems();
|
syncGridItems();
|
||||||
|
grid.value?.synchronize();
|
||||||
grid.value?.refreshItems();
|
grid.value?.refreshItems();
|
||||||
|
if (grid.value?._settings?.layout) {
|
||||||
|
grid.value._settings.layout.fillGaps = true;
|
||||||
|
}
|
||||||
if (instant) {
|
if (instant) {
|
||||||
grid.value?.layout(true);
|
grid.value?.layout(true);
|
||||||
} else {
|
} else {
|
||||||
@ -283,6 +352,7 @@ const syncSizeMap = (
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loadGridOrder();
|
loadGridOrder();
|
||||||
|
ensureOrderConsistency();
|
||||||
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
|
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
|
||||||
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
|
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@ -296,7 +366,7 @@ onMounted(async () => {
|
|||||||
},
|
},
|
||||||
dragSort: true,
|
dragSort: true,
|
||||||
layout: {
|
layout: {
|
||||||
fillGaps: settingsStore.autoAlign,
|
fillGaps: true,
|
||||||
rounding: true,
|
rounding: true,
|
||||||
},
|
},
|
||||||
layoutDuration: layoutAnimationMs,
|
layoutDuration: layoutAnimationMs,
|
||||||
@ -388,12 +458,10 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => settingsStore.autoAlign,
|
() => props.activeGroupId,
|
||||||
value => {
|
() => {
|
||||||
if (!grid.value) return;
|
if (!grid.value) return;
|
||||||
// Muuri has no public setter for fillGaps; update internal setting and relayout.
|
refreshLayout(true);
|
||||||
grid.value._settings.layout.fillGaps = value;
|
|
||||||
grid.value.layout(true);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -475,8 +543,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-item.is-resized {
|
.grid-item.is-resized {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
border-radius: $border-radius-small;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item.is-entering {
|
.grid-item.is-entering {
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
<img v-if="props.icon.img" :src="props.icon.img" :alt="props.icon.name" />
|
<img v-if="props.icon.img" :src="props.icon.img" :alt="props.icon.name" />
|
||||||
<span v-else>{{ props.icon.text?.trim() || props.icon.name.charAt(0) }}</span>
|
<span v-else>{{ props.icon.text?.trim() || props.icon.name.charAt(0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-spacer"></div>
|
|
||||||
<div class="label">{{ props.icon.name }}</div>
|
<div class="label">{{ props.icon.name }}</div>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
@ -38,9 +37,8 @@ const props = defineProps<{
|
|||||||
@import '@/styles/tokens.scss';
|
@import '@/styles/tokens.scss';
|
||||||
|
|
||||||
.icon-card-wrapper {
|
.icon-card-wrapper {
|
||||||
display: grid;
|
position: relative;
|
||||||
grid-template-rows: minmax(0, 1fr) var(--icon-label-margin-top) auto;
|
display: block;
|
||||||
align-items: stretch;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -83,27 +81,21 @@ const props = defineProps<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-spacer {
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-card-wrapper.size-1x1 .icon-card {
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: var(--icon-label-margin-top);
|
||||||
|
transform: translateX(-50%);
|
||||||
font-size: var(--icon-label-font-size);
|
font-size: var(--icon-label-font-size);
|
||||||
color: $color-text-primary;
|
color: $color-text-primary;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: var(--icon-label-display, block);
|
max-width: 100%;
|
||||||
white-space: normal;
|
visibility: var(--icon-label-visibility, visible);
|
||||||
word-break: break-word;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="main-content" @contextmenu="handleDesktopContextMenu">
|
<main class="main-content" @contextmenu="handleDesktopContextMenu">
|
||||||
<div class="main-content-container">
|
<div class="main-content-container">
|
||||||
<TheClock
|
<div v-if="showClock || showSearch" class="content-header">
|
||||||
v-if="showClock"
|
<TheClock
|
||||||
:show-date="showDate"
|
v-if="showClock"
|
||||||
:show-seconds="showSeconds"
|
:show-date="showDate"
|
||||||
:use-24-hour="use24Hour"
|
:show-seconds="showSeconds"
|
||||||
/>
|
:use-24-hour="use24Hour"
|
||||||
<TheSearchBar v-if="showSearch" />
|
/>
|
||||||
<GridCanvas />
|
<TheSearchBar v-if="showSearch" />
|
||||||
|
</div>
|
||||||
|
<GridCanvas :active-group-id="activeGroupId" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
@ -24,6 +26,10 @@ import GridCanvas from '@/components/GridCanvas/index.vue';
|
|||||||
import { useSettingsStore } from '@/store/useSettingsStore';
|
import { useSettingsStore } from '@/store/useSettingsStore';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
activeGroupId?: string | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { showSearch, showClock, showDate, showSeconds, use24Hour } = storeToRefs(settingsStore);
|
const { showSearch, showClock, showDate, showSeconds, use24Hour } = storeToRefs(settingsStore);
|
||||||
@ -49,6 +55,13 @@ const handleDesktopContextMenu = (event: MouseEvent) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start; // 内容顶部对齐
|
align-items: flex-start; // 内容顶部对齐
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content-container {
|
.main-content-container {
|
||||||
@ -59,4 +72,15 @@ const handleDesktopContextMenu = (event: MouseEvent) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--content-padding);
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -179,12 +179,12 @@
|
|||||||
<div class="row-desc">用网格密度统一控制图标与组件大小。</div>
|
<div class="row-desc">用网格密度统一控制图标与组件大小。</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model.number="layoutDensity"
|
v-model.number="iconSizeDensity"
|
||||||
class="settings-range"
|
class="settings-range"
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
:min="layoutDensityRange.min"
|
||||||
max="4"
|
:max="layoutDensityRange.max"
|
||||||
step="1"
|
:step="layoutDensityRange.step"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
@ -362,6 +362,7 @@ import { useSettingsStore } from '@/store/useSettingsStore';
|
|||||||
import {
|
import {
|
||||||
iconDensityRange,
|
iconDensityRange,
|
||||||
iconRadiusRange,
|
iconRadiusRange,
|
||||||
|
layoutDensityRange,
|
||||||
searchProviders,
|
searchProviders,
|
||||||
themePresets,
|
themePresets,
|
||||||
} from '@/config/settingsPresets';
|
} from '@/config/settingsPresets';
|
||||||
@ -441,9 +442,13 @@ const themeId = computed({
|
|||||||
set: value => settingsStore.setSetting('themeId', value as typeof settingsStore.themeId),
|
set: value => settingsStore.setSetting('themeId', value as typeof settingsStore.themeId),
|
||||||
});
|
});
|
||||||
|
|
||||||
const layoutDensity = computed({
|
const invertLayoutDensity = (value: number) =>
|
||||||
get: () => settingsStore.layoutDensity,
|
layoutDensityRange.max + layoutDensityRange.min -
|
||||||
set: value => settingsStore.setSetting('layoutDensity', value),
|
Math.min(Math.max(value, layoutDensityRange.min), layoutDensityRange.max);
|
||||||
|
|
||||||
|
const iconSizeDensity = computed({
|
||||||
|
get: () => invertLayoutDensity(settingsStore.layoutDensity),
|
||||||
|
set: value => settingsStore.setSetting('layoutDensity', invertLayoutDensity(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compactSidebar = computed({
|
const compactSidebar = computed({
|
||||||
@ -656,7 +661,6 @@ const clampRange = (value: number, min: number, max: number) =>
|
|||||||
const applyImportedSettings = (settings: Partial<typeof settingsStore.$state>) => {
|
const applyImportedSettings = (settings: Partial<typeof settingsStore.$state>) => {
|
||||||
const providerIds = new Set(searchProviders.map(item => item.id));
|
const providerIds = new Set(searchProviders.map(item => item.id));
|
||||||
const themeIds = new Set(themePresets.map(item => item.id));
|
const themeIds = new Set(themePresets.map(item => item.id));
|
||||||
const layoutDensityValues = new Set([1, 2, 3, 4]);
|
|
||||||
|
|
||||||
if (typeof settings.showSearch === 'boolean') {
|
if (typeof settings.showSearch === 'boolean') {
|
||||||
settingsStore.setSetting('showSearch', settings.showSearch);
|
settingsStore.setSetting('showSearch', settings.showSearch);
|
||||||
@ -709,8 +713,11 @@ const applyImportedSettings = (settings: Partial<typeof settingsStore.$state>) =
|
|||||||
clampRange(settings.iconDensity, iconDensityRange.min, iconDensityRange.max)
|
clampRange(settings.iconDensity, iconDensityRange.min, iconDensityRange.max)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof settings.layoutDensity === 'number' && layoutDensityValues.has(settings.layoutDensity)) {
|
if (typeof settings.layoutDensity === 'number') {
|
||||||
settingsStore.setSetting('layoutDensity', settings.layoutDensity as typeof settingsStore.layoutDensity);
|
settingsStore.setSetting(
|
||||||
|
'layoutDensity',
|
||||||
|
clampRange(settings.layoutDensity, layoutDensityRange.min, layoutDensityRange.max)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -169,6 +169,16 @@ const setActive = (id: string) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, id);
|
localStorage.setItem(STORAGE_KEY, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => activeId.value,
|
||||||
|
id => {
|
||||||
|
if (selectableItems.value.some(item => item.id === id)) {
|
||||||
|
emit('select', id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
const handleItemClick = (item: NavItem) => {
|
const handleItemClick = (item: NavItem) => {
|
||||||
if (item.action === 'add') {
|
if (item.action === 'add') {
|
||||||
emit('add');
|
emit('add');
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
<component :is="widgetComponent" :data="props.widget.data" :size="props.widget.size" />
|
<component :is="widgetComponent" :data="props.widget.data" :size="props.widget.size" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-spacer"></div>
|
|
||||||
<div class="label label-below">{{ label }}</div>
|
<div class="label label-below">{{ label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -41,29 +40,22 @@ const label = computed(() => {
|
|||||||
@import '@/styles/tokens.scss';
|
@import '@/styles/tokens.scss';
|
||||||
|
|
||||||
.widget-card-wrapper {
|
.widget-card-wrapper {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
|
||||||
grid-template-rows: minmax(0, 1fr) var(--icon-label-margin-top) auto;
|
|
||||||
padding: var(--widget-card-padding, var(--icon-card-padding));
|
padding: var(--widget-card-padding, var(--icon-card-padding));
|
||||||
|
box-sizing: border-box;
|
||||||
// 鏍规嵁灏哄绫诲悕鎺у埗灏哄
|
|
||||||
&.size-1x1 { grid-column: span 1; grid-row: span 1; }
|
|
||||||
&.size-1x2 { grid-column: span 1; grid-row: span 2; }
|
|
||||||
&.size-2x1 { grid-column: span 2; grid-row: span 1; }
|
|
||||||
&.size-2x2 { grid-column: span 2; grid-row: span 2; }
|
|
||||||
&.size-2x4 { grid-column: span 2; grid-row: span 4; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-card {
|
.widget-card {
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background-color: $color-surface-1;
|
background-color: $color-surface-1;
|
||||||
backdrop-filter: blur($backdrop-filter-blur) saturate($backdrop-filter-saturation);
|
backdrop-filter: blur($backdrop-filter-blur) saturate($backdrop-filter-saturation);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: $border-radius-small;
|
border-radius: var(--icon-radius, #{$border-radius-small});
|
||||||
box-shadow: $shadow-md;
|
box-shadow: $shadow-md;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -72,31 +64,26 @@ const label = computed(() => {
|
|||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-spacer {
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-card-wrapper.size-1x1 .widget-card {
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-content {
|
.widget-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: var(--widget-content-padding);
|
padding: var(--widget-content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: var(--icon-label-margin-top);
|
||||||
|
transform: translateX(-50%);
|
||||||
font-size: var(--widget-label-font-size);
|
font-size: var(--widget-label-font-size);
|
||||||
color: $color-text-primary;
|
color: $color-text-primary;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: var(--widget-label-display, block);
|
max-width: 100%;
|
||||||
white-space: normal;
|
visibility: var(--widget-label-visibility, visible);
|
||||||
word-break: break-word;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,8 +53,9 @@ export const useSettingsSync = () => {
|
|||||||
const baseGrid = layoutConfig.grid;
|
const baseGrid = layoutConfig.grid;
|
||||||
const sizeScale = layoutPreset.cellSize / baseGrid.cellSize;
|
const sizeScale = layoutPreset.cellSize / baseGrid.cellSize;
|
||||||
const labelScale = clampRange(sizeScale, 0.7, 1);
|
const labelScale = clampRange(sizeScale, 0.7, 1);
|
||||||
|
const densityScale = getIconDensityScale(settingsStore.iconDensity);
|
||||||
const iconPadding = Math.max(0, baseIcon.padding * sizeScale);
|
const iconPadding = Math.max(0, baseIcon.padding * sizeScale);
|
||||||
const widgetScale = sizeScale;
|
const widgetScale = 1;
|
||||||
const widgetPadding = Math.max(0, baseIcon.padding * sizeScale);
|
const widgetPadding = Math.max(0, baseIcon.padding * sizeScale);
|
||||||
const iconFontSize = Math.max(14, Math.round(baseIcon.fontSize * sizeScale));
|
const iconFontSize = Math.max(14, Math.round(baseIcon.fontSize * sizeScale));
|
||||||
const iconLabelFontSize = Math.max(10, Math.round(baseIcon.labelFontSize * labelScale));
|
const iconLabelFontSize = Math.max(10, Math.round(baseIcon.labelFontSize * labelScale));
|
||||||
@ -67,11 +68,7 @@ export const useSettingsSync = () => {
|
|||||||
4,
|
4,
|
||||||
Math.round(layoutConfig.widget.labelPaddingY * labelScale)
|
Math.round(layoutConfig.widget.labelPaddingY * labelScale)
|
||||||
);
|
);
|
||||||
const densityScale = getIconDensityScale(settingsStore.iconDensity);
|
const gridGap = Math.max(0, Math.round(baseGrid.gap * densityScale));
|
||||||
const baseVisualGap = (layoutPreset.gap + iconPadding * 2) * densityScale;
|
|
||||||
// Keep density purely controlling spacing; allow deeper negative gap for extra-tight layouts.
|
|
||||||
const minGap = -layoutPreset.cellSize * 0.75;
|
|
||||||
const gridGap = Math.max(minGap, baseVisualGap - iconPadding * 2);
|
|
||||||
|
|
||||||
setPx(root, '--grid-cell-size', layoutPreset.cellSize);
|
setPx(root, '--grid-cell-size', layoutPreset.cellSize);
|
||||||
setPx(root, '--grid-gap', gridGap);
|
setPx(root, '--grid-gap', gridGap);
|
||||||
@ -82,16 +79,13 @@ export const useSettingsSync = () => {
|
|||||||
setPx(root, '--icon-font-size', iconFontSize);
|
setPx(root, '--icon-font-size', iconFontSize);
|
||||||
setPx(root, '--icon-label-font-size', iconLabelFontSize);
|
setPx(root, '--icon-label-font-size', iconLabelFontSize);
|
||||||
setPx(root, '--icon-radius', iconRadius);
|
setPx(root, '--icon-radius', iconRadius);
|
||||||
setPx(
|
setPx(root, '--icon-label-margin-top', iconLabelMarginTop);
|
||||||
root,
|
|
||||||
'--icon-label-margin-top',
|
|
||||||
settingsStore.showIconLabels ? iconLabelMarginTop : 0
|
|
||||||
);
|
|
||||||
setPx(root, '--widget-label-font-size', widgetLabelFontSize);
|
setPx(root, '--widget-label-font-size', widgetLabelFontSize);
|
||||||
setPx(root, '--widget-label-padding-y', widgetLabelPaddingY);
|
setPx(root, '--widget-label-padding-y', widgetLabelPaddingY);
|
||||||
|
|
||||||
setVar(root, '--icon-label-display', settingsStore.showIconLabels ? 'block' : 'none');
|
const labelVisibility = settingsStore.showIconLabels ? 'visible' : 'hidden';
|
||||||
setVar(root, '--widget-label-display', settingsStore.showIconLabels ? 'block' : 'none');
|
setVar(root, '--icon-label-visibility', labelVisibility);
|
||||||
|
setVar(root, '--widget-label-visibility', labelVisibility);
|
||||||
|
|
||||||
const sidebarVars = getSidebarVars(settingsStore.compactSidebar);
|
const sidebarVars = getSidebarVars(settingsStore.compactSidebar);
|
||||||
setPx(root, '--sidebar-width', sidebarVars.width);
|
setPx(root, '--sidebar-width', sidebarVars.width);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export const layoutConfig = {
|
|||||||
// 图标首字母字体大小(像素)。
|
// 图标首字母字体大小(像素)。
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
// 卡片内边距(像素)。
|
// 卡片内边距(像素)。
|
||||||
padding: 8,
|
padding: 0,
|
||||||
// 图标卡片圆角(像素)。
|
// 图标卡片圆角(像素)。
|
||||||
radius: 12,
|
radius: 12,
|
||||||
// 标签字体大小(像素)。
|
// 标签字体大小(像素)。
|
||||||
|
|||||||
@ -83,7 +83,7 @@ const clampRange = (value: number, min: number, max: number) =>
|
|||||||
Math.min(Math.max(value, min), max);
|
Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
export type LayoutDensityPreset = {
|
export type LayoutDensityPreset = {
|
||||||
value: 1 | 2 | 3 | 4;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
cellSize: number;
|
cellSize: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
@ -119,19 +119,46 @@ export const layoutDensityPresets: LayoutDensityPreset[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getLayoutDensityPreset = (value?: number) =>
|
export const layoutDensityRange = {
|
||||||
layoutDensityPresets.find(item => item.value === value) ?? layoutDensityPresets[1];
|
min: 1,
|
||||||
|
max: 4,
|
||||||
|
step: 0.1,
|
||||||
|
default: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLayoutDensityPreset = (value?: number) => {
|
||||||
|
const normalized = clampRange(
|
||||||
|
typeof value === 'number' ? value : layoutDensityRange.default,
|
||||||
|
layoutDensityRange.min,
|
||||||
|
layoutDensityRange.max
|
||||||
|
);
|
||||||
|
const lower = Math.floor(normalized);
|
||||||
|
const upper = Math.ceil(normalized);
|
||||||
|
if (lower === upper) {
|
||||||
|
return layoutDensityPresets[lower - 1] ?? layoutDensityPresets[1];
|
||||||
|
}
|
||||||
|
const lowerPreset = layoutDensityPresets[lower - 1] ?? layoutDensityPresets[0];
|
||||||
|
const upperPreset = layoutDensityPresets[upper - 1] ??
|
||||||
|
layoutDensityPresets[layoutDensityPresets.length - 1];
|
||||||
|
const t = (normalized - lower) / (upper - lower);
|
||||||
|
return {
|
||||||
|
value: normalized,
|
||||||
|
label: t < 0.5 ? lowerPreset.label : upperPreset.label,
|
||||||
|
cellSize: Math.round(lowerPreset.cellSize + (upperPreset.cellSize - lowerPreset.cellSize) * t),
|
||||||
|
gap: Math.round(lowerPreset.gap + (upperPreset.gap - lowerPreset.gap) * t),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const iconDensityRange = {
|
export const iconDensityRange = {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 10,
|
max: 50,
|
||||||
step: 1,
|
step: 1,
|
||||||
default: 5,
|
default: 25,
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconDensityScaleRange = {
|
const iconDensityScaleRange = {
|
||||||
loose: 1.3,
|
loose: 2,
|
||||||
dense: 0.05,
|
dense: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIconDensityScale = (value?: number) => {
|
export const getIconDensityScale = (value?: number) => {
|
||||||
@ -140,6 +167,7 @@ export const getIconDensityScale = (value?: number) => {
|
|||||||
iconDensityRange.min,
|
iconDensityRange.min,
|
||||||
iconDensityRange.max
|
iconDensityRange.max
|
||||||
);
|
);
|
||||||
|
const ease = (t: number) => Math.pow(t, 0.5);
|
||||||
if (normalized === iconDensityRange.default) {
|
if (normalized === iconDensityRange.default) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -147,11 +175,12 @@ export const getIconDensityScale = (value?: number) => {
|
|||||||
// Smaller slider value = denser布局
|
// Smaller slider value = denser布局
|
||||||
const t = (iconDensityRange.default - normalized) /
|
const t = (iconDensityRange.default - normalized) /
|
||||||
(iconDensityRange.default - iconDensityRange.min);
|
(iconDensityRange.default - iconDensityRange.min);
|
||||||
return 1 - t * (1 - iconDensityScaleRange.dense);
|
return 1 - ease(t) * (1 - iconDensityScaleRange.dense);
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = (normalized - iconDensityRange.default) /
|
const t = (normalized - iconDensityRange.default) /
|
||||||
(iconDensityRange.max - iconDensityRange.default);
|
(iconDensityRange.max - iconDensityRange.default);
|
||||||
return 1 + t * (iconDensityScaleRange.loose - 1);
|
return 1 + ease(t) * (iconDensityScaleRange.loose - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const iconSizeRange = {
|
export const iconSizeRange = {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||||
|
|
||||||
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
||||||
|
const DEFAULT_GROUP_ID = 'home';
|
||||||
|
|
||||||
// 模拟数据(来自截图参考)
|
// 模拟数据(来自截图参考)
|
||||||
interface Icon {
|
interface Icon {
|
||||||
@ -11,6 +12,7 @@ interface Icon {
|
|||||||
img?: string; // 可选:用于图片图标(如徽标)
|
img?: string; // 可选:用于图片图标(如徽标)
|
||||||
text?: string; // 可选:用于文字图标
|
text?: string; // 可选:用于文字图标
|
||||||
bgColor?: string; // 可选:纯色背景
|
bgColor?: string; // 可选:纯色背景
|
||||||
|
groupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
@ -47,10 +49,33 @@ const defaultIcons: Icon[] = [
|
|||||||
{ id: '16', name: '书签管理', url: '#', bgColor: '#f1c40f' },
|
{ id: '16', name: '书签管理', url: '#', bgColor: '#f1c40f' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const defaultGroupById: Record<string, string> = {
|
||||||
|
'1': 'product',
|
||||||
|
'2': 'product',
|
||||||
|
'3': 'home',
|
||||||
|
'4': 'product',
|
||||||
|
'5': 'fun',
|
||||||
|
'6': 'home',
|
||||||
|
'7': 'fun',
|
||||||
|
'8': 'fun',
|
||||||
|
'9': 'ai',
|
||||||
|
'10': 'fun',
|
||||||
|
'11': 'design',
|
||||||
|
'12': 'design',
|
||||||
|
'13': 'ai',
|
||||||
|
'14': 'code',
|
||||||
|
'15': 'home',
|
||||||
|
'16': 'home',
|
||||||
|
};
|
||||||
|
|
||||||
const savedIcons = localStorage.getItem('itab_icons');
|
const savedIcons = localStorage.getItem('itab_icons');
|
||||||
|
|
||||||
const normalizeIcons = (icons: Icon[]): Icon[] =>
|
const normalizeIcons = (icons: Icon[]): Icon[] =>
|
||||||
icons.map(icon => ({ ...icon, size: icon.size ?? '1x1' }));
|
icons.map(icon => ({
|
||||||
|
...icon,
|
||||||
|
size: icon.size ?? '1x1',
|
||||||
|
groupId: icon.groupId ?? defaultGroupById[icon.id] ?? DEFAULT_GROUP_ID,
|
||||||
|
}));
|
||||||
|
|
||||||
const loadIcons = (): Icon[] => {
|
const loadIcons = (): Icon[] => {
|
||||||
if (!savedIcons) return normalizeIcons(defaultIcons);
|
if (!savedIcons) return normalizeIcons(defaultIcons);
|
||||||
@ -77,7 +102,7 @@ export const useLayoutStore = defineStore('layout', {
|
|||||||
actions: {
|
actions: {
|
||||||
addIcon(icon: Omit<Icon, 'id'>) {
|
addIcon(icon: Omit<Icon, 'id'>) {
|
||||||
const nextId = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
const nextId = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
this.icons.push({ ...icon, id: nextId });
|
this.icons.push({ ...icon, id: nextId, groupId: icon.groupId ?? DEFAULT_GROUP_ID });
|
||||||
localStorage.setItem('itab_icons', JSON.stringify(this.icons));
|
localStorage.setItem('itab_icons', JSON.stringify(this.icons));
|
||||||
},
|
},
|
||||||
updateIcon(iconId: string, updates: Partial<Omit<Icon, 'id'>>) {
|
updateIcon(iconId: string, updates: Partial<Omit<Icon, 'id'>>) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
getThemePreset,
|
getThemePreset,
|
||||||
iconDensityRange,
|
iconDensityRange,
|
||||||
iconRadiusRange,
|
iconRadiusRange,
|
||||||
layoutDensityPresets,
|
layoutDensityRange,
|
||||||
searchProviders,
|
searchProviders,
|
||||||
themePresets,
|
themePresets,
|
||||||
type SearchProviderId,
|
type SearchProviderId,
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
} from '@/config/settingsPresets';
|
} from '@/config/settingsPresets';
|
||||||
|
|
||||||
type IconDensity = number;
|
type IconDensity = number;
|
||||||
type LayoutDensity = 1 | 2 | 3 | 4;
|
type LayoutDensity = number;
|
||||||
|
|
||||||
type SettingsState = {
|
type SettingsState = {
|
||||||
showSearch: boolean;
|
showSearch: boolean;
|
||||||
@ -54,7 +54,6 @@ const defaultSettings: SettingsState = {
|
|||||||
|
|
||||||
const searchProviderIds = new Set(searchProviders.map(item => item.id));
|
const searchProviderIds = new Set(searchProviders.map(item => item.id));
|
||||||
const themePresetIds = new Set(themePresets.map(item => item.id));
|
const themePresetIds = new Set(themePresets.map(item => item.id));
|
||||||
const layoutDensityValues = new Set(layoutDensityPresets.map(item => item.value));
|
|
||||||
const clampRange = (value: number, min: number, max: number) =>
|
const clampRange = (value: number, min: number, max: number) =>
|
||||||
Math.min(Math.max(value, min), max);
|
Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
@ -92,10 +91,10 @@ const loadSettings = (): SettingsState => {
|
|||||||
? parsed.themeId
|
? parsed.themeId
|
||||||
: defaultSettings.themeId,
|
: defaultSettings.themeId,
|
||||||
layoutDensity:
|
layoutDensity:
|
||||||
typeof parsed.layoutDensity === 'number' && layoutDensityValues.has(parsed.layoutDensity)
|
typeof parsed.layoutDensity === 'number'
|
||||||
? parsed.layoutDensity
|
? clampRange(parsed.layoutDensity, layoutDensityRange.min, layoutDensityRange.max)
|
||||||
: defaultSettings.layoutDensity,
|
: defaultSettings.layoutDensity,
|
||||||
autoAlign: typeof parsed.autoAlign === 'boolean' ? parsed.autoAlign : defaultSettings.autoAlign,
|
autoAlign: defaultSettings.autoAlign,
|
||||||
compactSidebar:
|
compactSidebar:
|
||||||
typeof parsed.compactSidebar === 'boolean' ? parsed.compactSidebar : defaultSettings.compactSidebar,
|
typeof parsed.compactSidebar === 'boolean' ? parsed.compactSidebar : defaultSettings.compactSidebar,
|
||||||
showGroupLabels:
|
showGroupLabels:
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { defineStore, acceptHMRUpdate } from 'pinia';
|
|||||||
|
|
||||||
// 组件数据结构
|
// 组件数据结构
|
||||||
type WidgetSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
type WidgetSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
||||||
|
const DEFAULT_GROUP_ID = 'home';
|
||||||
interface Widget {
|
interface Widget {
|
||||||
id: string;
|
id: string;
|
||||||
component: string; // 渲染的组件名
|
component: string; // 渲染的组件名
|
||||||
size: WidgetSize; // 组件尺寸
|
size: WidgetSize; // 组件尺寸
|
||||||
gridPosition: { x: number; y: number }; // 网格位置
|
gridPosition: { x: number; y: number }; // 网格位置
|
||||||
data?: any; // 组件数据
|
data?: any; // 组件数据
|
||||||
|
groupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WidgetsState {
|
interface WidgetsState {
|
||||||
@ -46,13 +48,34 @@ const defaultWidgets: Widget[] = [
|
|||||||
|
|
||||||
const savedWidgets = localStorage.getItem('itab_widgets');
|
const savedWidgets = localStorage.getItem('itab_widgets');
|
||||||
|
|
||||||
|
const defaultGroupById: Record<string, string> = {
|
||||||
|
'widget-1': 'home',
|
||||||
|
'widget-2': 'fun',
|
||||||
|
'widget-3': 'product',
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeWidgets = (widgets: Widget[]): Widget[] =>
|
||||||
|
widgets.map(widget => ({
|
||||||
|
...widget,
|
||||||
|
groupId: widget.groupId ?? defaultGroupById[widget.id] ?? DEFAULT_GROUP_ID,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const loadWidgets = (): Widget[] => {
|
||||||
|
if (!savedWidgets) return normalizeWidgets(defaultWidgets);
|
||||||
|
try {
|
||||||
|
return normalizeWidgets(JSON.parse(savedWidgets) as Widget[]);
|
||||||
|
} catch {
|
||||||
|
return normalizeWidgets(defaultWidgets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 模拟数据(来自截图参考)
|
// 模拟数据(来自截图参考)
|
||||||
export const useWidgetsStore = defineStore('widgets', {
|
export const useWidgetsStore = defineStore('widgets', {
|
||||||
state: (): WidgetsState => ({
|
state: (): WidgetsState => ({
|
||||||
widgets: savedWidgets ? JSON.parse(savedWidgets) : defaultWidgets,
|
widgets: loadWidgets(),
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
addWidget(payload: { component: string; size?: WidgetSize; data?: any }) {
|
addWidget(payload: { component: string; size?: WidgetSize; data?: any; groupId?: string }) {
|
||||||
const nextId = `widget-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
const nextId = `widget-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
const nextWidget: Widget = {
|
const nextWidget: Widget = {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
@ -60,6 +83,7 @@ export const useWidgetsStore = defineStore('widgets', {
|
|||||||
size: payload.size ?? '1x1',
|
size: payload.size ?? '1x1',
|
||||||
gridPosition: { x: 0, y: 0 },
|
gridPosition: { x: 0, y: 0 },
|
||||||
data: payload.data,
|
data: payload.data,
|
||||||
|
groupId: payload.groupId ?? DEFAULT_GROUP_ID,
|
||||||
};
|
};
|
||||||
this.widgets.push(nextWidget);
|
this.widgets.push(nextWidget);
|
||||||
localStorage.setItem('itab_widgets', JSON.stringify(this.widgets));
|
localStorage.setItem('itab_widgets', JSON.stringify(this.widgets));
|
||||||
|
|||||||
Reference in New Issue
Block a user