1
This commit is contained in:
@ -8,6 +8,7 @@ import IconCard from '@/components/IconCard/index.vue';
|
|||||||
import WidgetCard from '@/components/WidgetCard/index.vue';
|
import WidgetCard from '@/components/WidgetCard/index.vue';
|
||||||
|
|
||||||
type GridItemType = 'icon' | 'widget';
|
type GridItemType = 'icon' | 'widget';
|
||||||
|
type GridItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
||||||
|
|
||||||
interface Icon {
|
interface Icon {
|
||||||
id: string;
|
id: string;
|
||||||
@ -15,9 +16,10 @@ interface Icon {
|
|||||||
url: string;
|
url: string;
|
||||||
img?: string;
|
img?: string;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
|
size?: GridItemSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WidgetSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
type WidgetSize = GridItemSize;
|
||||||
interface Widget {
|
interface Widget {
|
||||||
id: string;
|
id: string;
|
||||||
component: string;
|
component: string;
|
||||||
@ -217,7 +219,7 @@ onMounted(async () => {
|
|||||||
dragEnabled: true,
|
dragEnabled: true,
|
||||||
dragStartPredicate: {
|
dragStartPredicate: {
|
||||||
distance: 5,
|
distance: 5,
|
||||||
delay: 150,
|
delay: 0,
|
||||||
},
|
},
|
||||||
dragSort: true,
|
dragSort: true,
|
||||||
layout: {
|
layout: {
|
||||||
@ -255,8 +257,15 @@ watch(
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => widgetsStore.widgets.map(widget => widget.size).join('|'),
|
() => widgetsStore.widgets.map(widget => widget.size).join('|'),
|
||||||
() => {
|
async () => {
|
||||||
refreshLayout();
|
await refreshLayout();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => layoutStore.icons.map(icon => icon.size ?? '1x1').join('|'),
|
||||||
|
async () => {
|
||||||
|
await refreshLayout();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -277,7 +286,7 @@ onUnmounted(() => {
|
|||||||
v-for="item in orderedItems"
|
v-for="item in orderedItems"
|
||||||
:key="`${item.type}-${item.id}`"
|
:key="`${item.type}-${item.id}`"
|
||||||
class="grid-item"
|
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-id="item.id"
|
||||||
:data-type="item.type"
|
:data-type="item.type"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<a
|
<a
|
||||||
:href="props.icon.url"
|
:href="props.icon.url"
|
||||||
class="icon-card-wrapper"
|
class="icon-card-wrapper"
|
||||||
|
:class="`size-${props.icon.size ?? '1x1'}`"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
@ -14,10 +15,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
||||||
|
|
||||||
interface Icon {
|
interface Icon {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
size?: IconSize;
|
||||||
img?: string;
|
img?: string;
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
}
|
}
|
||||||
@ -34,27 +38,30 @@ const props = defineProps<{
|
|||||||
.icon-card-wrapper {
|
.icon-card-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-small;
|
||||||
padding: var(--icon-card-padding);
|
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; // 拖拽时避免选中文本
|
user-select: none; // 拖拽时避免选中文本
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
// 悬停效果由父级拖拽状态控制
|
// 悬停效果由父级拖拽状态控制
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
.icon-card {
|
||||||
|
box-shadow: $shadow-lg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-card {
|
.icon-card {
|
||||||
width: var(--icon-size);
|
width: 100%;
|
||||||
height: var(--icon-size);
|
flex: 1 1 auto;
|
||||||
border-radius: $border-radius-large;
|
min-height: 0;
|
||||||
|
border-radius: $border-radius-small;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -62,6 +69,7 @@ const props = defineProps<{
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: white;
|
color: white;
|
||||||
box-shadow: $shadow-md;
|
box-shadow: $shadow-md;
|
||||||
|
transition: box-shadow $motion-duration-sm $motion-easing-standard;
|
||||||
margin-bottom: var(--icon-label-margin-top);
|
margin-bottom: var(--icon-label-margin-top);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@ -70,11 +78,22 @@ const props = defineProps<{
|
|||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: $border-radius-large;
|
border-radius: $border-radius-small;
|
||||||
object-fit: cover;
|
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 {
|
.label {
|
||||||
font-size: var(--icon-label-font-size);
|
font-size: var(--icon-label-font-size);
|
||||||
color: $color-text-primary;
|
color: $color-text-primary;
|
||||||
|
|||||||
@ -2,6 +2,15 @@
|
|||||||
<div v-if="uiStore.contextMenu.isOpen" class="context-menu-overlay" @click="close" @contextmenu.prevent="close">
|
<div v-if="uiStore.contextMenu.isOpen" class="context-menu-overlay" @click="close" @contextmenu.prevent="close">
|
||||||
<div class="context-menu" :style="menuStyle">
|
<div class="context-menu" :style="menuStyle">
|
||||||
<ul v-if="uiStore.contextMenu.itemType === 'icon'">
|
<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="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="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>
|
<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 layoutStore = useLayoutStore();
|
||||||
const widgetsStore = useWidgetsStore();
|
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(() => ({
|
const menuStyle = computed(() => ({
|
||||||
left: `${uiStore.contextMenu.x}px`,
|
left: `${uiStore.contextMenu.x}px`,
|
||||||
@ -56,12 +68,19 @@ const deleteItem = () => {
|
|||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeWidgetSize = (newSize: '1x1' | '1x2' | '2x1' | '2x2' | '2x4') => {
|
const changeWidgetSize = (newSize: ItemSize) => {
|
||||||
if (uiStore.contextMenu.itemId) {
|
if (uiStore.contextMenu.itemId) {
|
||||||
widgetsStore.updateWidgetSize(uiStore.contextMenu.itemId, newSize);
|
widgetsStore.updateWidgetSize(uiStore.contextMenu.itemId, newSize);
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeIconSize = (newSize: ItemSize) => {
|
||||||
|
if (uiStore.contextMenu.itemId) {
|
||||||
|
layoutStore.updateIconSize(uiStore.contextMenu.itemId, newSize);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="widget-card" :class="`size-${props.widget.size}`">
|
<div class="widget-card-wrapper" :class="`size-${props.widget.size}`">
|
||||||
<div class="widget-content">
|
<div class="widget-card">
|
||||||
<component :is="widgetComponent" :data="props.widget.data" />
|
<div class="widget-content">
|
||||||
|
<component :is="widgetComponent" :data="props.widget.data" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">{{ label }}</div>
|
<div class="label label-below">{{ label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -37,24 +39,45 @@ const label = computed(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/styles/tokens.scss';
|
@import '@/styles/tokens.scss';
|
||||||
|
|
||||||
.widget-card {
|
.widget-card-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
padding: var(--icon-card-padding);
|
||||||
|
|
||||||
// 根据尺寸类名控制尺寸
|
// 鏍规嵁灏哄绫诲悕鎺у埗灏哄
|
||||||
&.size-1x1 { grid-column: span 1; grid-row: span 1; }
|
&.size-1x1 { grid-column: span 1; grid-row: span 1; }
|
||||||
&.size-1x2 { grid-column: span 1; grid-row: span 2; }
|
&.size-1x2 { grid-column: span 1; grid-row: span 2; }
|
||||||
&.size-2x1 { grid-column: span 2; grid-row: span 1; }
|
&.size-2x1 { grid-column: span 2; grid-row: span 1; }
|
||||||
&.size-2x2 { grid-column: span 2; grid-row: span 2; }
|
&.size-2x2 { grid-column: span 2; grid-row: span 2; }
|
||||||
&.size-2x4 { grid-column: span 2; grid-row: span 4; }
|
&.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 {
|
.widget-content {
|
||||||
@ -70,7 +93,10 @@ const label = computed(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding: var(--widget-label-padding-y) 0;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-below {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export const themeConfig = {
|
|||||||
// 圆角配置,统一控制卡片与胶囊样式。
|
// 圆角配置,统一控制卡片与胶囊样式。
|
||||||
radius: {
|
radius: {
|
||||||
// 小圆角(像素)。
|
// 小圆角(像素)。
|
||||||
sm: 8,
|
sm: 12,
|
||||||
// 中圆角(像素)。
|
// 中圆角(像素)。
|
||||||
md: 16,
|
md: 16,
|
||||||
// 大圆角(像素)。
|
// 大圆角(像素)。
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
type IconSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';
|
||||||
|
|
||||||
// 模拟数据(来自截图参考)
|
// 模拟数据(来自截图参考)
|
||||||
interface Icon {
|
interface Icon {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
size?: IconSize;
|
||||||
img?: string; // 可选:用于图片图标(如徽标)
|
img?: string; // 可选:用于图片图标(如徽标)
|
||||||
bgColor?: string; // 可选:纯色背景
|
bgColor?: string; // 可选:纯色背景
|
||||||
}
|
}
|
||||||
@ -45,9 +48,21 @@ const defaultIcons: Icon[] = [
|
|||||||
|
|
||||||
const savedIcons = localStorage.getItem('itab_icons');
|
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', {
|
export const useLayoutStore = defineStore('layout', {
|
||||||
state: (): LayoutState => ({
|
state: (): LayoutState => ({
|
||||||
icons: savedIcons ? JSON.parse(savedIcons) : defaultIcons,
|
icons: loadIcons(),
|
||||||
dragState: {
|
dragState: {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
itemId: null,
|
itemId: null,
|
||||||
@ -91,6 +106,13 @@ export const useLayoutStore = defineStore('layout', {
|
|||||||
this.icons = ordered;
|
this.icons = ordered;
|
||||||
localStorage.setItem('itab_icons', JSON.stringify(this.icons));
|
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) {
|
deleteIcon(itemId: string) {
|
||||||
const index = this.icons.findIndex(p => p.id === itemId);
|
const index = this.icons.findIndex(p => p.id === itemId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
|||||||
@ -26,7 +26,11 @@ body {
|
|||||||
#app {
|
#app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动条样式
|
// 滚动条样式
|
||||||
|
|||||||
Reference in New Issue
Block a user