This commit is contained in:
yinsx
2026-01-23 16:56:28 +08:00
parent 5674ce116e
commit ce796e2fd7
7 changed files with 131 additions and 32 deletions

View File

@ -8,6 +8,7 @@ import IconCard from '@/components/IconCard/index.vue';
import WidgetCard from '@/components/WidgetCard/index.vue';
type GridItemType = 'icon' | 'widget';
type GridItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
interface Icon {
id: string;
@ -15,9 +16,10 @@ interface Icon {
url: string;
img?: string;
bgColor?: string;
size?: GridItemSize;
}
type WidgetSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
type WidgetSize = GridItemSize;
interface Widget {
id: string;
component: string;
@ -217,7 +219,7 @@ onMounted(async () => {
dragEnabled: true,
dragStartPredicate: {
distance: 5,
delay: 150,
delay: 0,
},
dragSort: true,
layout: {
@ -255,8 +257,15 @@ watch(
watch(
() => widgetsStore.widgets.map(widget => widget.size).join('|'),
() => {
refreshLayout();
async () => {
await refreshLayout();
}
);
watch(
() => layoutStore.icons.map(icon => icon.size ?? '1x1').join('|'),
async () => {
await refreshLayout();
}
);
@ -277,7 +286,7 @@ onUnmounted(() => {
v-for="item in orderedItems"
:key="`${item.type}-${item.id}`"
class="grid-item"
:class="item.type === 'widget' ? `size-${item.widget.size}` : 'size-1x1'"
:class="`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`"
:data-id="item.id"
:data-type="item.type"
>

View File

@ -2,6 +2,7 @@
<a
:href="props.icon.url"
class="icon-card-wrapper"
:class="`size-${props.icon.size ?? '1x1'}`"
@click.prevent
target="_blank"
>
@ -14,10 +15,13 @@
</template>
<script setup lang="ts">
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
interface Icon {
id: string;
name: string;
url: string;
size?: IconSize;
img?: string;
bgColor?: string;
}
@ -34,27 +38,30 @@ const props = defineProps<{
.icon-card-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-items: stretch;
justify-content: flex-start;
width: 100%;
height: 100%;
text-decoration: none;
border-radius: $border-radius-medium;
border-radius: $border-radius-small;
padding: var(--icon-card-padding);
transition: transform $motion-duration-sm $motion-easing-standard, background-color $motion-duration-sm $motion-easing-standard;
transition: transform $motion-duration-sm $motion-easing-standard;
user-select: none; // 拖拽时避免选中文本
box-sizing: border-box;
// 悬停效果由父级拖拽状态控制
&:hover {
background-color: rgba(255, 255, 255, 0.05);
.icon-card {
box-shadow: $shadow-lg;
}
}
}
.icon-card {
width: var(--icon-size);
height: var(--icon-size);
border-radius: $border-radius-large;
width: 100%;
flex: 1 1 auto;
min-height: 0;
border-radius: $border-radius-small;
display: flex;
align-items: center;
justify-content: center;
@ -62,6 +69,7 @@ const props = defineProps<{
font-weight: 500;
color: white;
box-shadow: $shadow-md;
transition: box-shadow $motion-duration-sm $motion-easing-standard;
margin-bottom: var(--icon-label-margin-top);
background-size: cover;
background-position: center;
@ -70,11 +78,22 @@ const props = defineProps<{
img {
width: 100%;
height: 100%;
border-radius: $border-radius-large;
border-radius: $border-radius-small;
object-fit: cover;
}
}
.icon-card-wrapper.size-1x1 {
align-items: center;
justify-content: center;
}
.icon-card-wrapper.size-1x1 .icon-card {
width: var(--icon-size);
height: var(--icon-size);
flex: 0 0 auto;
}
.label {
font-size: var(--icon-label-font-size);
color: $color-text-primary;

View File

@ -2,6 +2,15 @@
<div v-if="uiStore.contextMenu.isOpen" class="context-menu-overlay" @click="close" @contextmenu.prevent="close">
<div class="context-menu" :style="menuStyle">
<ul v-if="uiStore.contextMenu.itemType === 'icon'">
<li class="layout-section">
<div class="layout-title">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
<span>布局</span>
</div>
<div class="widget-size-options">
<span v-for="size in iconSizes" :key="size" @click="changeIconSize(size)" class="size-option">{{ size }}</span>
</div>
</li>
<li><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>在新标签页打开</li>
<li><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>编辑图标</li>
<li><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="menu-icon"><path d="m12 19-7-7 7-7 7 7-7 7Z"></path><path d="M19 5v14"></path><path d="M5 5v14"></path></svg>编辑主页</li>
@ -34,7 +43,10 @@ const uiStore = useUIStore();
const layoutStore = useLayoutStore();
const widgetsStore = useWidgetsStore();
const widgetSizes = ['1x1', '1x2', '2x1', '2x2', '2x4'];
type ItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
const widgetSizes: ItemSize[] = ['1x1', '1x2', '2x1', '2x2', '2x4'];
const iconSizes: ItemSize[] = ['1x1', '1x2', '2x1', '2x2', '2x4'];
const menuStyle = computed(() => ({
left: `${uiStore.contextMenu.x}px`,
@ -56,12 +68,19 @@ const deleteItem = () => {
close();
};
const changeWidgetSize = (newSize: '1x1' | '1x2' | '2x1' | '2x2' | '2x4') => {
const changeWidgetSize = (newSize: ItemSize) => {
if (uiStore.contextMenu.itemId) {
widgetsStore.updateWidgetSize(uiStore.contextMenu.itemId, newSize);
}
close();
};
const changeIconSize = (newSize: ItemSize) => {
if (uiStore.contextMenu.itemId) {
layoutStore.updateIconSize(uiStore.contextMenu.itemId, newSize);
}
close();
};
</script>
<style lang="scss" scoped>

View File

@ -1,9 +1,11 @@
<template>
<div class="widget-card" :class="`size-${props.widget.size}`">
<div class="widget-card-wrapper" :class="`size-${props.widget.size}`">
<div class="widget-card">
<div class="widget-content">
<component :is="widgetComponent" :data="props.widget.data" />
</div>
<div class="label">{{ label }}</div>
</div>
<div class="label label-below">{{ label }}</div>
</div>
</template>
@ -37,24 +39,45 @@ const label = computed(() => {
<style lang="scss" scoped>
@import '@/styles/tokens.scss';
.widget-card {
.widget-card-wrapper {
width: 100%;
height: 100%;
background-color: $color-surface-1;
backdrop-filter: blur($backdrop-filter-blur) saturate($backdrop-filter-saturation);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: $border-radius-large;
box-shadow: $shadow-md;
display: flex;
flex-direction: column;
overflow: hidden;
padding: var(--icon-card-padding);
// 根据尺寸类名控制尺寸
// 鏍规嵁灏哄绫诲悕鎺у埗灏哄
&.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; }
&.size-1x1 {
align-items: center;
justify-content: center;
}
}
.widget-card {
width: 100%;
flex: 1 1 auto;
min-height: 0;
background-color: $color-surface-1;
backdrop-filter: blur($backdrop-filter-blur) saturate($backdrop-filter-saturation);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: $border-radius-small;
box-shadow: $shadow-md;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: var(--icon-label-margin-top);
}
.widget-card-wrapper.size-1x1 .widget-card {
width: var(--icon-size);
height: var(--icon-size);
flex: 0 0 auto;
}
.widget-content {
@ -70,7 +93,10 @@ const label = computed(() => {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: var(--widget-label-padding-y) 0;
flex-shrink: 0;
}
.label-below {
padding: 0;
}
</style>

View File

@ -2,7 +2,7 @@ export const themeConfig = {
// 圆角配置,统一控制卡片与胶囊样式。
radius: {
// 小圆角(像素)。
sm: 8,
sm: 12,
// 中圆角(像素)。
md: 16,
// 大圆角(像素)。

View File

@ -1,10 +1,13 @@
import { defineStore } from 'pinia';
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
// 模拟数据(来自截图参考)
interface Icon {
id: string;
name: string;
url: string;
size?: IconSize;
img?: string; // 可选:用于图片图标(如徽标)
bgColor?: string; // 可选:纯色背景
}
@ -45,9 +48,21 @@ const defaultIcons: Icon[] = [
const savedIcons = localStorage.getItem('itab_icons');
const normalizeIcons = (icons: Icon[]): Icon[] =>
icons.map(icon => ({ ...icon, size: icon.size ?? '1x1' }));
const loadIcons = (): Icon[] => {
if (!savedIcons) return normalizeIcons(defaultIcons);
try {
return normalizeIcons(JSON.parse(savedIcons) as Icon[]);
} catch {
return normalizeIcons(defaultIcons);
}
};
export const useLayoutStore = defineStore('layout', {
state: (): LayoutState => ({
icons: savedIcons ? JSON.parse(savedIcons) : defaultIcons,
icons: loadIcons(),
dragState: {
isDragging: false,
itemId: null,
@ -91,6 +106,13 @@ export const useLayoutStore = defineStore('layout', {
this.icons = ordered;
localStorage.setItem('itab_icons', JSON.stringify(this.icons));
},
updateIconSize(iconId: string, newSize: IconSize) {
const icon = this.icons.find(item => item.id === iconId);
if (icon) {
icon.size = newSize;
localStorage.setItem('itab_icons', JSON.stringify(this.icons));
}
},
deleteIcon(itemId: string) {
const index = this.icons.findIndex(p => p.id === itemId);
if (index !== -1) {

View File

@ -26,7 +26,11 @@ body {
#app {
width: 100%;
height: 100%;
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
background-color: $color-background;
background-image: url('/bg.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
// 滚动条样式