2026-10-02 11:18:47 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-10-17 03:06:46 +08:00
|
|
|
|
import { confirmAction } from '../../composables/confirm.js';
|
2026-10-17 14:29:25 +08:00
|
|
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
2026-10-17 03:06:46 +08:00
|
|
|
|
import { ElMessage } from 'element-plus';
|
2026-10-02 11:18:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
DEFAULT_ISLAND_APPEARANCE,
|
|
|
|
|
|
ISLAND_APPEARANCE_RANGES as RANGES,
|
|
|
|
|
|
ISLAND_POSITION_LABELS,
|
|
|
|
|
|
ISLAND_POSITIONS,
|
|
|
|
|
|
ISLAND_STYLE_LABELS,
|
|
|
|
|
|
ISLAND_STYLES,
|
|
|
|
|
|
type IslandAppearance,
|
|
|
|
|
|
type IslandPosition,
|
|
|
|
|
|
type IslandStyle,
|
|
|
|
|
|
} from '@classhelper/shared';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 灵动岛个性化(原设置页「灵动岛 · 个性化」卡整块迁移)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意:必须传**展开后的纯对象**。`island` 是 `ref`,它的 `.value` 在 Vue 里是响应式
|
|
|
|
|
|
* Proxy,Proxy 过不了 IPC 的结构化克隆(`An object could not be cloned.`)——
|
|
|
|
|
|
* 那正是"拖了滑块、数字在变、灵动岛却没反应"的原因。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const island = ref<IslandAppearance>({ ...DEFAULT_ISLAND_APPEARANCE });
|
|
|
|
|
|
const savingIsland = ref(false);
|
2026-10-17 12:55:39 +08:00
|
|
|
|
function editLayout() {
|
2026-10-17 13:45:26 +08:00
|
|
|
|
layoutMode.value = 'custom';
|
2026-10-17 12:55:39 +08:00
|
|
|
|
void window.desktop?.islandEditLayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
const layoutMode = ref<'fixed' | 'custom'>('fixed');
|
2026-10-17 14:29:25 +08:00
|
|
|
|
const residentEnabled = ref(false);
|
|
|
|
|
|
const pointerFadeEnabled = ref(true);
|
|
|
|
|
|
const pointerFadeReversed = ref(false);
|
2026-10-17 12:55:39 +08:00
|
|
|
|
async function changeLayout() {
|
|
|
|
|
|
const config = await window.desktop?.getConfig();
|
|
|
|
|
|
if (config)
|
2026-10-17 14:29:25 +08:00
|
|
|
|
await window.desktop?.saveConfig({
|
|
|
|
|
|
islandLayout: {
|
|
|
|
|
|
...config.islandLayout,
|
|
|
|
|
|
layout: layoutMode.value,
|
|
|
|
|
|
residentEnabled: residentEnabled.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-10-17 12:55:39 +08:00
|
|
|
|
}
|
2026-10-17 13:45:26 +08:00
|
|
|
|
async function chooseLayout(mode: 'fixed' | 'custom') {
|
|
|
|
|
|
layoutMode.value = mode;
|
2026-10-17 14:29:25 +08:00
|
|
|
|
if (mode === 'fixed') residentEnabled.value = false;
|
2026-10-17 13:45:26 +08:00
|
|
|
|
await changeLayout();
|
|
|
|
|
|
}
|
2026-10-17 14:29:25 +08:00
|
|
|
|
async function changeResident(value: boolean | string | number) {
|
|
|
|
|
|
residentEnabled.value = value === true;
|
|
|
|
|
|
if (residentEnabled.value) layoutMode.value = 'custom';
|
|
|
|
|
|
await changeLayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function changePointerFade(
|
|
|
|
|
|
key: 'pointerFadeEnabled' | 'pointerFadeReversed',
|
|
|
|
|
|
value: boolean | string | number,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const config = await window.desktop?.getConfig();
|
|
|
|
|
|
if (!config) return;
|
|
|
|
|
|
const saved = await window.desktop?.saveConfig({
|
|
|
|
|
|
islandLayout: { ...config.islandLayout, [key]: value === true },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
pointerFadeEnabled.value = saved.islandLayout.pointerFadeEnabled;
|
|
|
|
|
|
pointerFadeReversed.value = saved.islandLayout.pointerFadeReversed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadLayout() {
|
|
|
|
|
|
const settings = (await window.desktop?.getConfig())?.islandLayout;
|
|
|
|
|
|
layoutMode.value = settings?.layout ?? 'fixed';
|
|
|
|
|
|
residentEnabled.value = settings?.residentEnabled ?? false;
|
|
|
|
|
|
pointerFadeEnabled.value = settings?.pointerFadeEnabled ?? true;
|
|
|
|
|
|
pointerFadeReversed.value = settings?.pointerFadeReversed ?? false;
|
|
|
|
|
|
}
|
2026-10-17 12:55:39 +08:00
|
|
|
|
onMounted(async () => {
|
2026-10-17 14:29:25 +08:00
|
|
|
|
await loadLayout();
|
|
|
|
|
|
window.addEventListener('focus', loadLayout);
|
2026-10-17 12:55:39 +08:00
|
|
|
|
});
|
2026-10-17 14:29:25 +08:00
|
|
|
|
onUnmounted(() => window.removeEventListener('focus', loadLayout));
|
2026-10-02 11:18:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 左右边距是否有效:只在停靠左 / 右时生效。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 岛是"居中停靠"时(默认就是顶部居中),水平方向的锚点就是屏幕中线 ——
|
|
|
|
|
|
* 这时调左右边距不会有任何变化。之前没把这点说清楚,用户会以为"调了不生效"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const marginXApplies = computed(() => !island.value.position.endsWith('center'));
|
|
|
|
|
|
|
|
|
|
|
|
const positionOptions = ISLAND_POSITIONS.map((value) => ({
|
|
|
|
|
|
value,
|
|
|
|
|
|
label: ISLAND_POSITION_LABELS[value],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const styleOptions = ISLAND_STYLES.map((value) => ({
|
|
|
|
|
|
value,
|
|
|
|
|
|
label: ISLAND_STYLE_LABELS[value],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
/** 读取当前生效的外观(主进程为单一事实来源) */
|
|
|
|
|
|
async function loadIslandAppearance(): Promise<void> {
|
|
|
|
|
|
if (!window.desktop?.islandGetAppearance) return;
|
|
|
|
|
|
island.value = await window.desktop.islandGetAppearance();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 改动即生效:属性变化时先应用(实时预览),点"保存"再落盘 */
|
|
|
|
|
|
function applyIslandAppearance(patch: Partial<IslandAppearance> = {}): void {
|
|
|
|
|
|
island.value = { ...island.value, ...patch };
|
|
|
|
|
|
window.desktop?.islandSetAppearance({ ...island.value });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveIslandAppearance(): Promise<void> {
|
|
|
|
|
|
if (!window.desktop?.saveConfig) return;
|
|
|
|
|
|
savingIsland.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await window.desktop.saveConfig({ island: { ...island.value } });
|
|
|
|
|
|
ElMessage.success('个性化设置已保存');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
savingIsland.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function resetIslandAppearance(): Promise<void> {
|
2026-10-17 03:06:46 +08:00
|
|
|
|
if (!(await confirmAction('恢复灵动岛的默认外观?', '恢复默认', { type: 'warning' }))) return;
|
2026-10-02 11:18:47 +08:00
|
|
|
|
applyIslandAppearance({ ...DEFAULT_ISLAND_APPEARANCE });
|
|
|
|
|
|
await saveIslandAppearance();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 本地推一条测试通知,立刻确认外观效果(预览模式:直接展开、失焦不收起、30 秒后自动消失) */
|
|
|
|
|
|
function testIsland(): void {
|
|
|
|
|
|
window.desktop?.islandPush?.({
|
|
|
|
|
|
notification: {
|
|
|
|
|
|
id: `appearance-test-${Date.now()}`,
|
|
|
|
|
|
title: '灵动岛外观预览',
|
|
|
|
|
|
content: '拖动滑块即可实时预览:高度、宽度、圆角、透明度、字号、主题色、位置与动画。',
|
|
|
|
|
|
priority: 'NORMAL',
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
courseName: null,
|
|
|
|
|
|
teacherName: '本地预览',
|
|
|
|
|
|
},
|
|
|
|
|
|
context: { inClass: false, currentPeriodEnd: null, preview: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadIslandAppearance();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="page">
|
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="page-title">灵动岛</h2>
|
|
|
|
|
|
<p class="page-subtitle">屏幕上方的消息浮窗:外观、停靠位置与动画</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<el-card shadow="never" class="layout-mode-card">
|
|
|
|
|
|
<template #header><span>岛内布局</span></template>
|
|
|
|
|
|
<div class="layout-choices">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="layout-choice"
|
|
|
|
|
|
:class="{ active: layoutMode === 'fixed' }"
|
|
|
|
|
|
:aria-pressed="layoutMode === 'fixed'"
|
|
|
|
|
|
@click="chooseLayout('fixed')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>经典灵动岛</strong>
|
|
|
|
|
|
<span>有消息时展开,保持熟悉的提醒样式</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="layout-choice"
|
|
|
|
|
|
:class="{ active: layoutMode === 'custom' }"
|
|
|
|
|
|
:aria-pressed="layoutMode === 'custom'"
|
|
|
|
|
|
@click="chooseLayout('custom')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>自定义灵动岛</strong>
|
|
|
|
|
|
<span>在岛内排列时钟、课程和消息等组件</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-10-17 14:29:25 +08:00
|
|
|
|
<div class="resident-setting">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>常驻岛</strong>
|
|
|
|
|
|
<p>始终显示自定义内容;收到消息后显示铃铛,点击查看。没有通知时可展开另一套自定义内容。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-switch :model-value="residentEnabled" aria-label="常驻岛" @change="changeResident" />
|
|
|
|
|
|
</div>
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<div v-if="layoutMode === 'custom'" class="layout-edit-entry">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>按你的习惯布置岛内内容</strong>
|
2026-10-17 14:29:25 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
{{
|
|
|
|
|
|
residentEnabled
|
|
|
|
|
|
? '分别编辑「收起时」与「无通知展开时」的内容,点选组件即可设置。'
|
|
|
|
|
|
: '在预览中添加和排列组件,点选组件即可设置;更改自动保存到本机。'
|
|
|
|
|
|
}}
|
|
|
|
|
|
</p>
|
2026-10-17 13:45:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="primary" @click="editLayout">编辑岛内组件</el-button>
|
|
|
|
|
|
</div>
|
2026-10-17 14:29:25 +08:00
|
|
|
|
<div v-if="residentEnabled" class="resident-window-options">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>ClassIsland 窗口设置</strong>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
常驻行基准高度 40 px。指针移入背景后淡化至 5%
|
|
|
|
|
|
并穿透,右侧铃铛或展开按钮保持可用;编辑、展开和触摸交互期间暂停避让。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>指针移入淡化</span>
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="pointerFadeEnabled"
|
|
|
|
|
|
@change="(value: boolean | string | number) => changePointerFade('pointerFadeEnabled', value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label v-if="pointerFadeEnabled">
|
|
|
|
|
|
<span>反转指针移入淡化</span>
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="pointerFadeReversed"
|
|
|
|
|
|
@change="(value: boolean | string | number) => changePointerFade('pointerFadeReversed', value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-10-17 13:45:26 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-10-02 11:18:47 +08:00
|
|
|
|
<el-card shadow="never">
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<template #header>
|
|
|
|
|
|
<span>{{ layoutMode === 'fixed' ? '外观与位置' : '位置与提醒' }}</span>
|
|
|
|
|
|
</template>
|
2026-10-02 11:18:47 +08:00
|
|
|
|
<el-form label-width="96px" label-position="left">
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<template v-if="layoutMode === 'fixed'">
|
|
|
|
|
|
<el-form-item :label="`高度 ${island.height}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.height"
|
|
|
|
|
|
:min="RANGES.height.min"
|
|
|
|
|
|
:max="RANGES.height.max"
|
|
|
|
|
|
:step="1"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ height: value })"
|
2026-10-02 11:18:47 +08:00
|
|
|
|
/>
|
2026-10-17 13:45:26 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`宽度 ${island.width}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.width"
|
|
|
|
|
|
:min="RANGES.width.min"
|
|
|
|
|
|
:max="RANGES.width.max"
|
|
|
|
|
|
:step="2"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ width: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`圆角 ${island.radius}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.radius"
|
|
|
|
|
|
:min="RANGES.radius.min"
|
|
|
|
|
|
:max="RANGES.radius.max"
|
|
|
|
|
|
:step="1"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ radius: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`不透明度 ${Math.round(island.opacity * 100)}%`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.opacity"
|
|
|
|
|
|
:min="RANGES.opacity.min"
|
|
|
|
|
|
:max="RANGES.opacity.max"
|
|
|
|
|
|
:step="0.05"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ opacity: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`字号 ${island.fontSize}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.fontSize"
|
|
|
|
|
|
:min="RANGES.fontSize.min"
|
|
|
|
|
|
:max="RANGES.fontSize.max"
|
|
|
|
|
|
:step="1"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ fontSize: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`动画速度 ${island.speed.toFixed(1)}x`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.speed"
|
|
|
|
|
|
:min="RANGES.speed.min"
|
|
|
|
|
|
:max="RANGES.speed.max"
|
|
|
|
|
|
:step="0.1"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ speed: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="主题色">
|
|
|
|
|
|
<el-color-picker
|
|
|
|
|
|
:model-value="island.accent"
|
|
|
|
|
|
@change="(value: string | null) => applyIslandAppearance({ accent: value ?? '#0a84ff' })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted ml-8">用于高亮、按钮与进度条</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="视觉风格">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
:model-value="island.style"
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
@change="(value: IslandStyle) => applyIslandAppearance({ style: value })"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in styleOptions"
|
|
|
|
|
|
:key="item.value"
|
|
|
|
|
|
:label="item.label"
|
|
|
|
|
|
:value="item.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<span class="text-muted ml-8">纯黑 / 毛玻璃(半透明)/ 主题色渐变</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
2026-10-02 11:18:47 +08:00
|
|
|
|
<el-form-item label="显示位置">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
:model-value="island.position"
|
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
|
@change="(value: IslandPosition) => applyIslandAppearance({ position: value })"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="item in positionOptions"
|
|
|
|
|
|
:key="item.value"
|
|
|
|
|
|
:label="item.label"
|
|
|
|
|
|
:value="item.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`左右边距 ${island.marginX}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.marginX"
|
|
|
|
|
|
:min="RANGES.marginX.min"
|
|
|
|
|
|
:max="RANGES.marginX.max"
|
|
|
|
|
|
:step="2"
|
|
|
|
|
|
:disabled="!marginXApplies"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ marginX: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span v-if="marginXApplies" class="text-muted ml-8">停靠左/右时距屏幕边缘的距离</span>
|
|
|
|
|
|
<span v-else class="text-muted ml-8">
|
|
|
|
|
|
当前是「居中」停靠,左右边距不生效 —— 把上面的「显示位置」改成左 / 右停靠即可看到效果
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item :label="`上下边距 ${island.marginY}px`">
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
:model-value="island.marginY"
|
|
|
|
|
|
:min="RANGES.marginY.min"
|
|
|
|
|
|
:max="RANGES.marginY.max"
|
|
|
|
|
|
:step="2"
|
|
|
|
|
|
@input="(value: number) => applyIslandAppearance({ marginY: value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted ml-8">停靠顶/底时距屏幕边缘的距离(任务栏在侧面时调大)</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="跟随鼠标屏幕">
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="island.followCursorDisplay"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
(value: boolean | string | number) =>
|
|
|
|
|
|
applyIslandAppearance({ followCursorDisplay: Boolean(value) })
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted ml-8">多显示器教室电脑:岛出现在鼠标所在的那块屏幕</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="动画">
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="island.animations"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
(value: boolean | string | number) => applyIslandAppearance({ animations: Boolean(value) })
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted ml-8">关闭后展开/收起为瞬时生效</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="始终置顶">
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="island.alwaysOnTop"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
(value: boolean | string | number) => applyIslandAppearance({ alwaysOnTop: Boolean(value) })
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<el-form-item v-if="layoutMode === 'fixed'" label="空闲细缝">
|
2026-10-02 11:18:47 +08:00
|
|
|
|
<el-switch
|
|
|
|
|
|
:model-value="island.idleSliver"
|
|
|
|
|
|
@change="
|
|
|
|
|
|
(value: boolean | string | number) => applyIslandAppearance({ idleSliver: Boolean(value) })
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-muted ml-8">参考 WinIsland:没有消息时保留一条细缝(关闭则完全隐藏)</span>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<el-button type="primary" :loading="savingIsland" @click="saveIslandAppearance">保存设置</el-button>
|
|
|
|
|
|
<el-button @click="testIsland">预览效果</el-button>
|
|
|
|
|
|
<el-button @click="resetIslandAppearance">恢复默认</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
class="mt-12"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
title="拖动滑块即为实时预览;保存后写入本地配置,重启客户端仍然生效"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-10-17 13:45:26 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.layout-mode-card {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
2026-10-17 14:29:25 +08:00
|
|
|
|
.resident-setting {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-setting strong {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-setting p {
|
|
|
|
|
|
max-width: 620px;
|
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-window-options {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-window-options strong {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-window-options p {
|
|
|
|
|
|
margin: 8px 0 14px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resident-window-options label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
2026-10-17 13:45:26 +08:00
|
|
|
|
.layout-choices {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-choice {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: var(--el-fill-color-blank);
|
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-choice strong {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-choice span {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-choice.active {
|
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-choice:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--el-color-primary);
|
|
|
|
|
|
outline-offset: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-edit-entry {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-edit-entry strong {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-edit-entry p {
|
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 680px) {
|
|
|
|
|
|
.layout-choices {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layout-edit-entry {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
2026-10-02 11:18:47 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.ml-8 {
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|