feat(island): 增加常驻内容并沿用 ClassIsland 尺寸与避让设置
This commit is contained in:
1 parent
c83b03d8e6
commit
7f7d3925b2
12 files changed
+1092
-127
No files matched your search
@@ -17,6 +17,11 @@ const props = defineProps<{
|
||||
parentHasWidth?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{ select: [id: string]; drag: [event: PointerEvent, id: string] }>();
|
||||
function select(event: MouseEvent) {
|
||||
if (!props.editing) return;
|
||||
event.stopPropagation();
|
||||
emit('select', props.node.id);
|
||||
}
|
||||
const container = computed(() => ['group', 'stack', 'slide', 'rolling'].includes(props.node.kind));
|
||||
const current = computed(() => {
|
||||
const d = new Date(props.now),
|
||||
@@ -101,7 +106,7 @@ const style = computed<CSSProperties>(() => {
|
||||
width: n.advanced.width ? `${n.advanced.width}px` : undefined,
|
||||
maxWidth: n.advanced.maxWidth ? `${n.advanced.maxWidth}px` : undefined,
|
||||
alignSelf: !props.parentHasWidth
|
||||
? 'stretch'
|
||||
? undefined
|
||||
: ({ stretch: 'stretch', start: 'flex-start', center: 'center', end: 'flex-end' } as const)[
|
||||
n.advanced.align
|
||||
],
|
||||
@@ -114,8 +119,8 @@ const style = computed<CSSProperties>(() => {
|
||||
'--emphasized': override ? `${a.emphasizedFontSize}px` : undefined,
|
||||
'--large': override ? `${a.largeFontSize}px` : undefined,
|
||||
'--rolling-duration': `${Math.max(1, 200 / n.settings.speed)}s`,
|
||||
flexDirection: n.settings.direction === 'vertical' ? 'column' : 'row',
|
||||
gap: `${n.settings.gap}px`,
|
||||
flexDirection: container.value ? (n.settings.direction === 'vertical' ? 'column' : 'row') : undefined,
|
||||
gap: container.value ? `${n.settings.gap}px` : undefined,
|
||||
display: container.value ? 'flex' : undefined,
|
||||
};
|
||||
});
|
||||
@@ -127,7 +132,7 @@ const style = computed<CSSProperties>(() => {
|
||||
:class="[{ selected: editing && selectedId === node.id, container, 'node-editing': editing }, node.kind]"
|
||||
:style="style"
|
||||
:data-node="node.id"
|
||||
@click.stop="editing && emit('select', node.id)"
|
||||
@click="select"
|
||||
>
|
||||
<button
|
||||
v-if="editing"
|
||||
@@ -168,7 +173,7 @@ const style = computed<CSSProperties>(() => {
|
||||
<slot v-if="node.id === alertNodeId" name="alerts" />
|
||||
<span v-else-if="editing">{{ ISLAND_COMPONENT_LABELS[node.kind] }}内容组件</span>
|
||||
</template>
|
||||
<span v-else :class="{ large: node.kind === 'clock', emphasized: node.kind === 'countdown' }">
|
||||
<span v-else :class="{ emphasized: node.kind === 'clock' || node.kind === 'countdown' }">
|
||||
{{ content }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -181,6 +186,34 @@ const style = computed<CSSProperties>(() => {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.component-node:not(.node-editing) {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.clock {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.date,
|
||||
.countdown {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.schedule > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.schedule > div + div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.separator > span {
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
background: currentColor;
|
||||
font-size: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
.container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type CSSProperties } from 'vue';
|
||||
import {
|
||||
ISLAND_COMPONENT_LABELS,
|
||||
CLASSISLAND_ISLAND_METRICS as CI,
|
||||
ISLAND_SHADOW_PAD,
|
||||
islandNode,
|
||||
normalizeIslandLayouts,
|
||||
@@ -37,13 +38,68 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
const scheme = computed(() => draft.value.schemes.find((s) => s.id === draft.value.activeSchemeId)!);
|
||||
const editingSurface = ref<'compact' | 'expanded'>('compact');
|
||||
const pendingCount = computed(
|
||||
() => new Set([props.state.active?.id, ...props.state.queued.map((item) => item.id)].filter(Boolean)).size,
|
||||
);
|
||||
const residentCompact = computed(
|
||||
() => draft.value.residentEnabled && !props.editing && props.state.mode !== 'expanded',
|
||||
);
|
||||
const idleExpanded = computed(
|
||||
() =>
|
||||
draft.value.residentEnabled &&
|
||||
!props.editing &&
|
||||
props.state.mode === 'expanded' &&
|
||||
pendingCount.value === 0,
|
||||
);
|
||||
const rows = computed(() =>
|
||||
(props.editing ? editingSurface.value === 'expanded' : idleExpanded.value)
|
||||
? scheme.value.expandedRows
|
||||
: scheme.value.rows,
|
||||
);
|
||||
function setSurface(surface: 'compact' | 'expanded') {
|
||||
editingSurface.value = surface;
|
||||
selectedId.value = '';
|
||||
selectedRowId.value = '';
|
||||
panel.value = 'appearance';
|
||||
}
|
||||
function openResident(event: Event) {
|
||||
if (!residentCompact.value || (event.target as HTMLElement).closest('button, a, input, select, textarea'))
|
||||
return;
|
||||
window.island?.sendAction('expand');
|
||||
}
|
||||
function residentKey(event: KeyboardEvent) {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
openResident(event);
|
||||
}
|
||||
}
|
||||
function expandResident() {
|
||||
window.island?.sendAction('expand');
|
||||
}
|
||||
function collapseResident() {
|
||||
window.island?.sendAction('collapse');
|
||||
}
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch' || event.pointerType === 'pen') window.island?.noteResidentTouch();
|
||||
}
|
||||
function restoreClassIslandAppearance() {
|
||||
Object.assign(scheme.value, {
|
||||
scale: CI.scale,
|
||||
color: '#000000',
|
||||
opacity: CI.opacity,
|
||||
radius: CI.radius,
|
||||
spacing: CI.rowSpacing,
|
||||
});
|
||||
for (const row of [...scheme.value.rows, ...scheme.value.expandedRows]) row.gap = 12;
|
||||
save();
|
||||
}
|
||||
const selectedId = ref(''),
|
||||
selectedKind = ref<IslandComponentKind>('clock');
|
||||
const panel = ref<'component' | 'row' | 'appearance'>('appearance');
|
||||
const selectedRowId = ref('');
|
||||
const activeRow = computed(
|
||||
() => scheme.value.rows.find((r) => r.id === selectedRowId.value) || scheme.value.rows[0]!,
|
||||
);
|
||||
const activeRow = computed(() => rows.value.find((r) => r.id === selectedRowId.value) || rows.value[0]!);
|
||||
const componentGroups = [
|
||||
{ name: '日常信息', kinds: ['clock', 'date', 'schedule', 'countdown', 'text', 'separator'] },
|
||||
{ name: '消息提醒', kinds: ['notification', 'homework', 'call'] },
|
||||
@@ -75,14 +131,14 @@ function allNodes(nodes: IslandComponentNode[]): IslandComponentNode[] {
|
||||
return nodes.flatMap((n) => [n, ...allNodes(n.children)]);
|
||||
}
|
||||
const selected = computed(() =>
|
||||
scheme.value.rows.flatMap((r) => allNodes(r.nodes)).find((n) => n.id === selectedId.value),
|
||||
rows.value.flatMap((r) => allNodes(r.nodes)).find((n) => n.id === selectedId.value),
|
||||
);
|
||||
const alertRow = computed(
|
||||
() =>
|
||||
scheme.value.rows.find((r) => r.main && r.allowNotifications) ||
|
||||
scheme.value.rows.find((r) => r.allowNotifications),
|
||||
rows.value.find((r) => r.main && r.allowNotifications) || rows.value.find((r) => r.allowNotifications),
|
||||
);
|
||||
const alertNodeId = computed(() => {
|
||||
if (residentCompact.value || idleExpanded.value) return '';
|
||||
const kind = props.state.active?.kind ?? 'notification';
|
||||
return (
|
||||
(alertRow.value &&
|
||||
@@ -90,7 +146,13 @@ const alertNodeId = computed(() => {
|
||||
''
|
||||
);
|
||||
});
|
||||
const hasMessage = computed(() => props.state.mode !== 'hidden' && Boolean(props.state.active));
|
||||
const hasMessage = computed(
|
||||
() =>
|
||||
props.state.mode !== 'hidden' &&
|
||||
Boolean(props.state.active) &&
|
||||
(!draft.value.residentEnabled || props.state.mode === 'expanded') &&
|
||||
!props.editing,
|
||||
);
|
||||
const shellStyle = computed<CSSProperties>(() => ({
|
||||
width: viewport.value.width ? `${viewport.value.width}px` : 'max-content',
|
||||
minHeight: !props.editing && viewport.value.height ? `${viewport.value.height}px` : undefined,
|
||||
@@ -103,8 +165,17 @@ const shellStyle = computed<CSSProperties>(() => ({
|
||||
Math.round(scheme.value.opacity * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0'),
|
||||
'--custom-radius': `${scheme.value.radius}px`,
|
||||
'--custom-radius': `${scheme.value.radius * (draft.value.residentEnabled ? scheme.value.scale : 1)}px`,
|
||||
'--custom-row-gap': `${scheme.value.spacing}px`,
|
||||
...(draft.value.residentEnabled
|
||||
? {
|
||||
'--secondary': `${CI.secondaryFontSize}px`,
|
||||
'--emphasized': `${CI.emphasizedFontSize}px`,
|
||||
'--large': `${CI.largeFontSize}px`,
|
||||
'--resident-height': `${CI.rowHeight}px`,
|
||||
'--resident-padding': `${CI.horizontalPadding * scheme.value.scale}px`,
|
||||
}
|
||||
: {}),
|
||||
gap: `${scheme.value.spacing}px`,
|
||||
}));
|
||||
function id() {
|
||||
@@ -117,7 +188,7 @@ function save() {
|
||||
savedAt.value = '已自动保存';
|
||||
}
|
||||
function addRow(after: number) {
|
||||
scheme.value.rows.splice(after + 1, 0, {
|
||||
rows.value.splice(after + 1, 0, {
|
||||
id: id(),
|
||||
name: '新主界面行',
|
||||
main: false,
|
||||
@@ -125,7 +196,7 @@ function addRow(after: number) {
|
||||
gap: 8,
|
||||
nodes: [],
|
||||
});
|
||||
selectRow(scheme.value.rows[after + 1]!.id);
|
||||
selectRow(rows.value[after + 1]!.id);
|
||||
save();
|
||||
}
|
||||
function add(kind = selectedKind.value, row = activeRow.value) {
|
||||
@@ -136,7 +207,7 @@ function add(kind = selectedKind.value, row = activeRow.value) {
|
||||
save();
|
||||
}
|
||||
function findList(nodeId: string, nodes?: IslandComponentNode[]): IslandComponentNode[] | null {
|
||||
for (const list of nodes ? [nodes] : scheme.value.rows.map((r) => r.nodes)) {
|
||||
for (const list of nodes ? [nodes] : rows.value.map((r) => r.nodes)) {
|
||||
if (list.some((n) => n.id === nodeId)) return list;
|
||||
for (const n of list) {
|
||||
const found = findList(nodeId, n.children);
|
||||
@@ -182,7 +253,7 @@ function newScheme() {
|
||||
const copy = JSON.parse(JSON.stringify(scheme.value));
|
||||
copy.id = id();
|
||||
copy.name = name;
|
||||
for (const r of copy.rows) {
|
||||
for (const r of [...copy.rows, ...copy.expandedRows]) {
|
||||
r.id = id();
|
||||
for (const n of allNodes(r.nodes)) n.id = id();
|
||||
}
|
||||
@@ -256,16 +327,16 @@ function drop() {
|
||||
if (!drag) return;
|
||||
const moving = drag;
|
||||
drag = null;
|
||||
const row = scheme.value.rows.find((r) => r.id === moving.rowId);
|
||||
const row = rows.value.find((r) => r.id === moving.rowId);
|
||||
if (!row) return;
|
||||
if (moving.type === 'library') {
|
||||
add(moving.kind, row);
|
||||
return;
|
||||
}
|
||||
if (moving.type === 'row') {
|
||||
const from = scheme.value.rows.findIndex((r) => r.id === moving.id),
|
||||
to = scheme.value.rows.indexOf(row);
|
||||
if (from >= 0) scheme.value.rows.splice(to, 0, scheme.value.rows.splice(from, 1)[0]!);
|
||||
const from = rows.value.findIndex((r) => r.id === moving.id),
|
||||
to = rows.value.indexOf(row);
|
||||
if (from >= 0) rows.value.splice(to, 0, rows.value.splice(from, 1)[0]!);
|
||||
save();
|
||||
return;
|
||||
}
|
||||
@@ -316,6 +387,10 @@ function measure() {
|
||||
parseFloat(padding?.paddingBottom || '0'),
|
||||
),
|
||||
});
|
||||
const control = shell.value?.querySelector('.resident-bell')?.getBoundingClientRect();
|
||||
window.island?.reportResidentControl(
|
||||
control ? { x: control.x, y: control.y, width: control.width, height: control.height } : null,
|
||||
);
|
||||
}
|
||||
}, 120);
|
||||
}
|
||||
@@ -336,6 +411,7 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div
|
||||
:class="editing ? 'layout-editor' : 'custom-window'"
|
||||
@pointerdown.capture="onPointerDown"
|
||||
@pointermove="panMove"
|
||||
@pointerup="
|
||||
drop();
|
||||
@@ -409,7 +485,7 @@ onUnmounted(() => {
|
||||
<label class="field">
|
||||
添加到
|
||||
<select :value="activeRow.id" @change="selectedRowId = ($event.target as HTMLSelectElement).value">
|
||||
<option v-for="row in scheme.rows" :key="row.id" :value="row.id">{{ row.name }}</option>
|
||||
<option v-for="row in rows" :key="row.id" :value="row.id">{{ row.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary-button wide-button" @click="add()">
|
||||
@@ -419,7 +495,15 @@ onUnmounted(() => {
|
||||
</aside>
|
||||
<main :class="{ canvas: editing }" @pointerdown="panStart">
|
||||
<div v-if="editing" class="canvas-toolbar">
|
||||
<span>岛内预览</span>
|
||||
<nav v-if="draft.residentEnabled" class="surface-tabs" aria-label="编辑常驻岛内容">
|
||||
<button :class="{ active: editingSurface === 'compact' }" @click="setSurface('compact')">
|
||||
收起时
|
||||
</button>
|
||||
<button :class="{ active: editingSurface === 'expanded' }" @click="setSurface('expanded')">
|
||||
无通知展开时
|
||||
</button>
|
||||
</nav>
|
||||
<span v-else>岛内预览</span>
|
||||
<div>
|
||||
<button
|
||||
:class="{ active: panel === 'appearance' }"
|
||||
@@ -447,36 +531,78 @@ onUnmounted(() => {
|
||||
<div
|
||||
ref="shell"
|
||||
class="custom-card"
|
||||
:class="{ separated: scheme.separated, 'has-alert': hasMessage }"
|
||||
:class="{ separated: scheme.separated, 'has-alert': hasMessage, 'resident-size': residentCompact }"
|
||||
:role="residentCompact ? 'button' : undefined"
|
||||
:tabindex="residentCompact ? 0 : undefined"
|
||||
:aria-label="
|
||||
residentCompact ? (pendingCount ? `查看 ${pendingCount} 条通知` : '展开常驻岛') : undefined
|
||||
"
|
||||
:aria-expanded="draft.residentEnabled && !editing ? state.mode === 'expanded' : undefined"
|
||||
@click="openResident"
|
||||
@keydown="residentKey"
|
||||
:data-priority="hasMessage ? state.active?.priority : undefined"
|
||||
:style="[shellStyle, editing ? { width: 'max-content', maxWidth: 'none', maxHeight: 'none' } : {}]"
|
||||
>
|
||||
<div ref="inner" class="custom-inner" :style="{ zoom: scheme.scale }">
|
||||
<section
|
||||
v-for="row in scheme.rows"
|
||||
:key="row.id"
|
||||
:data-row="row.id"
|
||||
class="layout-row"
|
||||
:class="{ 'row-selected': editing && selectedRowId === row.id, 'row-editing': editing }"
|
||||
:style="{ '--row-gap': `${row.gap}px` }"
|
||||
>
|
||||
<div v-if="editing" class="row-tools">
|
||||
<button class="row-drag" aria-label="拖动行" @pointerdown="startDrag($event, 'row', row.id)">
|
||||
⠿
|
||||
</button>
|
||||
<button class="row-name" @click="selectRow(row.id)">
|
||||
{{ row.name }}
|
||||
<span v-if="row.main">主要行</span>
|
||||
</button>
|
||||
<button class="row-settings" :aria-label="`${row.name}设置`" @click="selectRow(row.id)">
|
||||
行设置
|
||||
</button>
|
||||
</div>
|
||||
<div class="row-content" @click.self="editing && selectRow(row.id)">
|
||||
<div class="row-components">
|
||||
<div
|
||||
ref="inner"
|
||||
class="custom-inner"
|
||||
:class="{ 'resident-compact': residentCompact }"
|
||||
:style="{ zoom: scheme.scale }"
|
||||
>
|
||||
<header v-if="idleExpanded" class="resident-panel-heading">
|
||||
<span>常驻岛</span>
|
||||
<button aria-label="收起常驻岛" @click.stop="collapseResident">收起 ↑</button>
|
||||
</header>
|
||||
<div class="layout-rows">
|
||||
<section
|
||||
v-for="row in rows"
|
||||
:key="row.id"
|
||||
:data-row="row.id"
|
||||
class="layout-row"
|
||||
:class="{ 'row-selected': editing && selectedRowId === row.id, 'row-editing': editing }"
|
||||
:style="{ '--row-gap': `${row.gap}px` }"
|
||||
>
|
||||
<div v-if="editing" class="row-tools">
|
||||
<button
|
||||
class="row-drag"
|
||||
aria-label="拖动行"
|
||||
@pointerdown="startDrag($event, 'row', row.id)"
|
||||
>
|
||||
⠿
|
||||
</button>
|
||||
<button class="row-name" @click="selectRow(row.id)">
|
||||
{{ row.name }}
|
||||
<span v-if="row.main">主要行</span>
|
||||
</button>
|
||||
<button class="row-settings" :aria-label="`${row.name}设置`" @click="selectRow(row.id)">
|
||||
行设置
|
||||
</button>
|
||||
</div>
|
||||
<div class="row-content" @click.self="editing && selectRow(row.id)">
|
||||
<div class="row-components">
|
||||
<ComponentNode
|
||||
v-for="n in row.nodes.filter(
|
||||
(n) => !['notification', 'homework', 'call'].includes(n.kind),
|
||||
)"
|
||||
:key="n.id"
|
||||
:node="n"
|
||||
:state="state"
|
||||
:now="now"
|
||||
:schedules="schedules"
|
||||
:editing="editing"
|
||||
:selected-id="selectedId"
|
||||
:alert-node-id="alertNodeId"
|
||||
@select="selectNode"
|
||||
@drag="(e, id) => startDrag(e, 'node', id)"
|
||||
>
|
||||
<template #alerts>
|
||||
<div id="custom-alert-host" :ref="hostRef" v-show="hasMessage" />
|
||||
</template>
|
||||
</ComponentNode>
|
||||
</div>
|
||||
<ComponentNode
|
||||
v-for="n in row.nodes.filter(
|
||||
(n) => !['notification', 'homework', 'call'].includes(n.kind),
|
||||
v-for="n in row.nodes.filter((n) =>
|
||||
['notification', 'homework', 'call'].includes(n.kind),
|
||||
)"
|
||||
:key="n.id"
|
||||
:node="n"
|
||||
@@ -493,39 +619,44 @@ onUnmounted(() => {
|
||||
<div id="custom-alert-host" :ref="hostRef" v-show="hasMessage" />
|
||||
</template>
|
||||
</ComponentNode>
|
||||
<div
|
||||
v-if="row.id === alertRow?.id && !alertNodeId"
|
||||
id="custom-alert-host"
|
||||
:ref="hostRef"
|
||||
v-show="hasMessage"
|
||||
/>
|
||||
<span v-if="editing && !row.nodes.length" class="empty-row">拖入组件,或从左侧添加</span>
|
||||
</div>
|
||||
<ComponentNode
|
||||
v-for="n in row.nodes.filter((n) => ['notification', 'homework', 'call'].includes(n.kind))"
|
||||
:key="n.id"
|
||||
:node="n"
|
||||
:state="state"
|
||||
:now="now"
|
||||
:schedules="schedules"
|
||||
:editing="editing"
|
||||
:selected-id="selectedId"
|
||||
:alert-node-id="alertNodeId"
|
||||
@select="selectNode"
|
||||
@drag="(e, id) => startDrag(e, 'node', id)"
|
||||
>
|
||||
<template #alerts>
|
||||
<div id="custom-alert-host" :ref="hostRef" v-show="hasMessage" />
|
||||
</template>
|
||||
</ComponentNode>
|
||||
<div
|
||||
v-if="row.id === alertRow?.id && !alertNodeId"
|
||||
id="custom-alert-host"
|
||||
:ref="hostRef"
|
||||
v-show="hasMessage"
|
||||
/>
|
||||
<span v-if="editing && !row.nodes.length" class="empty-row">拖入组件,或从左侧添加</span>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
<button
|
||||
v-if="residentCompact"
|
||||
class="resident-bell"
|
||||
:class="{ 'bell-empty': !pendingCount }"
|
||||
:aria-label="pendingCount ? `查看 ${pendingCount} 条通知` : '展开常驻岛'"
|
||||
@click.stop="expandResident"
|
||||
>
|
||||
<ComponentIcon v-if="pendingCount" kind="notification" />
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m8 10 4 4 4-4" />
|
||||
</svg>
|
||||
<span v-if="pendingCount">{{ pendingCount > 99 ? '99+' : pendingCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="editing"
|
||||
class="add-row-button"
|
||||
@click="addRow(scheme.rows.findIndex((r) => r.id === activeRow.id))"
|
||||
@click="addRow(rows.findIndex((r) => r.id === activeRow.id))"
|
||||
>
|
||||
+ 在下方新建主界面行
|
||||
</button>
|
||||
@@ -584,7 +715,7 @@ onUnmounted(() => {
|
||||
class="wide-button"
|
||||
:disabled="activeRow.main"
|
||||
@click="
|
||||
scheme.rows.forEach((r) => (r.main = r.id === activeRow.id));
|
||||
rows.forEach((r) => (r.main = r.id === activeRow.id));
|
||||
save();
|
||||
"
|
||||
>
|
||||
@@ -593,14 +724,12 @@ onUnmounted(() => {
|
||||
<p class="field-hint">提醒优先显示在主要行内;若该行禁用提醒,则显示在下一条允许提醒的行内。</p>
|
||||
</section>
|
||||
<section class="setting-section">
|
||||
<button class="wide-button" @click="addRow(scheme.rows.indexOf(activeRow))">
|
||||
在此行下方新增行
|
||||
</button>
|
||||
<button class="wide-button" @click="addRow(rows.indexOf(activeRow))">在此行下方新增行</button>
|
||||
<button
|
||||
class="wide-button danger-button"
|
||||
:disabled="scheme.rows.length < 2"
|
||||
:disabled="rows.length < 2"
|
||||
@click="
|
||||
scheme.rows.splice(scheme.rows.indexOf(activeRow), 1);
|
||||
rows.splice(rows.indexOf(activeRow), 1);
|
||||
selectedRowId = '';
|
||||
save();
|
||||
"
|
||||
@@ -614,9 +743,43 @@ onUnmounted(() => {
|
||||
<div class="inspector-heading">
|
||||
<span class="eyebrow">方案设置</span>
|
||||
<h2>整体外观</h2>
|
||||
<p>所有组件共用一个灵动岛</p>
|
||||
<p>
|
||||
{{
|
||||
draft.residentEnabled
|
||||
? editingSurface === 'compact'
|
||||
? '正在编辑收起时的常驻内容'
|
||||
: '正在编辑没有通知时的展开内容'
|
||||
: '所有组件共用一个灵动岛'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="inspector-body" @change="save">
|
||||
<section class="setting-section">
|
||||
<label class="toggle-field">
|
||||
<span>常驻岛</span>
|
||||
<input type="checkbox" v-model="draft.residentEnabled" @change="setSurface('compact')" />
|
||||
</label>
|
||||
<p class="field-hint">
|
||||
开启后始终显示收起内容,收到消息时显示铃铛,点击查看通知;没有通知时展开另一套自定义内容。
|
||||
</p>
|
||||
</section>
|
||||
<section v-if="draft.residentEnabled" class="setting-section">
|
||||
<h3>ClassIsland 窗口设置</h3>
|
||||
<p class="field-hint">
|
||||
基准岛高 40 px,默认缩放 1×;正文 16、时钟 18 px。背景移入淡化并穿透,右侧展开按钮保持可用。
|
||||
</p>
|
||||
<label class="toggle-field">
|
||||
<span>指针移入淡化</span>
|
||||
<input type="checkbox" v-model="draft.pointerFadeEnabled" />
|
||||
</label>
|
||||
<label v-if="draft.pointerFadeEnabled" class="toggle-field">
|
||||
<span>反转指针移入淡化</span>
|
||||
<input type="checkbox" v-model="draft.pointerFadeReversed" />
|
||||
</label>
|
||||
<button class="wide-button" @click="restoreClassIslandAppearance">
|
||||
恢复 ClassIsland 默认外观
|
||||
</button>
|
||||
</section>
|
||||
<section class="setting-section">
|
||||
<h3>当前方案</h3>
|
||||
<label class="field">
|
||||
@@ -624,8 +787,14 @@ onUnmounted(() => {
|
||||
<input v-model="scheme.name" />
|
||||
</label>
|
||||
<label class="field inline-field">
|
||||
内容缩放
|
||||
<input type="number" v-model.number="scheme.scale" min="0.5" max="3" step="0.1" />
|
||||
界面缩放
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="scheme.scale"
|
||||
:min="draft.residentEnabled ? CI.scaleMin : 0.5"
|
||||
:max="draft.residentEnabled ? CI.scaleMax : 3"
|
||||
step="0.05"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="setting-section">
|
||||
@@ -641,20 +810,38 @@ onUnmounted(() => {
|
||||
<label class="field">
|
||||
背景透明度
|
||||
<div class="range-field">
|
||||
<input type="range" v-model.number="scheme.opacity" min="0.1" max="1" step="0.05" />
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="scheme.opacity"
|
||||
:min="draft.residentEnabled ? 0 : 0.1"
|
||||
max="1"
|
||||
step="0.05"
|
||||
/>
|
||||
<output>{{ Math.round(scheme.opacity * 100) }}%</output>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field inline-field">
|
||||
圆角
|
||||
<input type="number" v-model.number="scheme.radius" min="0" max="100" />
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="scheme.radius"
|
||||
min="0"
|
||||
:max="draft.residentEnabled ? CI.radiusMax : 100"
|
||||
step="0.5"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="setting-section">
|
||||
<h3>多行布局</h3>
|
||||
<label class="field inline-field">
|
||||
行间距
|
||||
<input type="number" v-model.number="scheme.spacing" min="0" max="80" />
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="scheme.spacing"
|
||||
min="0"
|
||||
:max="draft.residentEnabled ? CI.rowSpacingMax : 100"
|
||||
step="5"
|
||||
/>
|
||||
</label>
|
||||
<label class="toggle-field">
|
||||
<span>各行分离显示</span>
|
||||
|
||||
@@ -19,6 +19,129 @@
|
||||
flex-direction: column;
|
||||
gap: var(--custom-row-gap);
|
||||
}
|
||||
.custom-card .layout-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--custom-row-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
.custom-card .custom-inner.resident-compact {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.custom-card.resident-size {
|
||||
padding: 0 var(--resident-padding, 12px);
|
||||
}
|
||||
.custom-card.resident-size .row-content,
|
||||
.custom-card.resident-size .row-components {
|
||||
height: var(--resident-height, 40px);
|
||||
min-height: var(--resident-height, 40px);
|
||||
}
|
||||
.custom-card.resident-size .row-components {
|
||||
gap: 0;
|
||||
}
|
||||
.custom-card.resident-size .component-node:not(.node-editing) {
|
||||
height: var(--resident-height, 40px);
|
||||
max-height: var(--resident-height, 40px);
|
||||
padding: 0 calc(var(--row-gap, 12px) / 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.custom-card.resident-size .component-node.schedule {
|
||||
flex-direction: column;
|
||||
}
|
||||
.custom-card.resident-size .schedule small,
|
||||
.custom-card.resident-size .schedule strong {
|
||||
line-height: 1.05;
|
||||
}
|
||||
.custom-card.resident-size .separator > span {
|
||||
height: 20px;
|
||||
}
|
||||
.custom-card[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-card[role='button']:focus-visible {
|
||||
outline: 2px solid #8dafff;
|
||||
outline-offset: -3px;
|
||||
}
|
||||
.custom-card .resident-bell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 28px;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #ffda8b;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-card .resident-bell:hover {
|
||||
background: #ffffff1a;
|
||||
}
|
||||
.custom-card .resident-bell.bell-empty {
|
||||
color: #ffffff60;
|
||||
}
|
||||
.custom-card .resident-bell svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.custom-card .resident-bell span {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
padding: 0 3px;
|
||||
border-radius: 7px;
|
||||
background: #f45b58;
|
||||
color: white;
|
||||
font:
|
||||
600 9px/1 'Segoe UI',
|
||||
sans-serif;
|
||||
box-shadow: 0 0 0 2px var(--custom-color);
|
||||
}
|
||||
.custom-card .resident-panel-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 4px 6px 12px;
|
||||
border-bottom: 1px solid #ffffff1a;
|
||||
margin-bottom: 4px;
|
||||
color: #ffffff94;
|
||||
font-size: 12px;
|
||||
}
|
||||
.custom-card .resident-panel-heading button {
|
||||
min-height: 30px;
|
||||
padding: 4px 10px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff0d;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.layout-editor .surface-tabs {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.layout-editor .surface-tabs button {
|
||||
font-size: 12px;
|
||||
}
|
||||
.layout-editor .surface-tabs button.active {
|
||||
color: var(--editor-accent);
|
||||
background: #edf2ff;
|
||||
border-color: #b6c8f0;
|
||||
}
|
||||
.custom-card .layout-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in new issue
Block a user