Compare commits
2
Commits
4d31219368
...
f6e916c985
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6e916c985 | ||
|
|
509b0d41c6 |
@@ -223,14 +223,14 @@ defineExpose({ startEdit, commitNote })
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.cv-node {
|
.cv-node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid rgb(24 24 27 / 0.08);
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
background: var(--surface-solid);
|
background: var(--surface-solid);
|
||||||
box-shadow: var(--glass-shadow);
|
box-shadow: 0 1px 2px rgb(24 24 27 / 0.05), 0 8px 24px -8px rgb(24 24 27 / 0.12);
|
||||||
transition: box-shadow 0.15s ease, border-color 0.15s ease;
|
transition: box-shadow 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
.cv-node__clip {
|
.cv-node__clip {
|
||||||
@@ -240,8 +240,8 @@ defineExpose({ startEdit, commitNote })
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.cv-node.is-selected {
|
.cv-node.is-selected {
|
||||||
border-color: rgb(24 24 27 / 0.7);
|
border-color: #18181b;
|
||||||
box-shadow: var(--glass-shadow-lg), 0 0 0 3px rgb(24 24 27 / 0.25), 0 10px 30px var(--accent-glow);
|
box-shadow: 0 0 0 2px #18181b, 0 14px 34px -10px rgb(24 24 27 / 0.3);
|
||||||
}
|
}
|
||||||
.cv-node:active {
|
.cv-node:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
@@ -286,10 +286,10 @@ defineExpose({ startEdit, commitNote })
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 笔记 = 淡黄色贴纸 */
|
/* 笔记 = 白色卡片 */
|
||||||
.cv-node--note {
|
.cv-node--note {
|
||||||
background: #fdf6c9;
|
background: #fff;
|
||||||
border-color: rgb(180 140 20 / 0.35);
|
border-color: rgb(24 24 27 / 0.1);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
html.dark .cv-node--note {
|
html.dark .cv-node--note {
|
||||||
@@ -305,7 +305,7 @@ html.dark .cv-node--note {
|
|||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
color: #111;
|
color: #27272a;
|
||||||
outline: none;
|
outline: none;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -390,7 +390,7 @@ html.dark .cv-node--note {
|
|||||||
max-height: 5em;
|
max-height: 5em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,15 +404,21 @@ html.dark .cv-node--note {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
.cv-node:hover .cv-port,
|
||||||
|
.cv-node.is-selected .cv-port,
|
||||||
|
.cv-port.is-active,
|
||||||
|
.cv-port:focus-visible { opacity: 1; }
|
||||||
.cv-port::after {
|
.cv-port::after {
|
||||||
content: '';
|
content: '';
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 3px solid var(--m-orange);
|
border: 2px solid #18181b;
|
||||||
box-shadow: 0 1px 4px rgb(0 0 0 / 0.35), 0 0 0 2px rgb(255 255 255 / 0.8);
|
box-shadow: 0 1px 4px rgb(0 0 0 / 0.25);
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.cv-port:hover::after, .cv-port:focus-visible::after, .cv-port.is-active::after {
|
.cv-port:hover::after, .cv-port:focus-visible::after, .cv-port.is-active::after {
|
||||||
|
|||||||
@@ -781,8 +781,8 @@ defineExpose({
|
|||||||
background-color: var(--app-bg);
|
background-color: var(--app-bg);
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
color-mix(in srgb, var(--fg) 16%, transparent) 1.4px,
|
color-mix(in srgb, var(--fg) 12%, transparent) 1.3px,
|
||||||
transparent 1.4px
|
transparent 1.3px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.cv-stage.is-panning {
|
.cv-stage.is-panning {
|
||||||
@@ -808,21 +808,18 @@ defineExpose({
|
|||||||
/* 常规连线:蓝色虚线;选中时改紫色实线加粗 */
|
/* 常规连线:蓝色虚线;选中时改紫色实线加粗 */
|
||||||
.cv-edge {
|
.cv-edge {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgb(95 108 248 / 0.85);
|
stroke: rgb(113 113 122 / 0.75);
|
||||||
stroke-width: 2.4;
|
stroke-width: 2;
|
||||||
stroke-dasharray: 10 6;
|
stroke-dasharray: 8 6;
|
||||||
filter: drop-shadow(0 0 5px rgb(24 24 27 / 0.3));
|
|
||||||
}
|
}
|
||||||
.cv-edge--hot {
|
.cv-edge--hot {
|
||||||
stroke: var(--m-orange);
|
stroke: var(--m-orange);
|
||||||
stroke-width: 2.8;
|
stroke-width: 2.6;
|
||||||
filter: drop-shadow(0 0 7px var(--accent-glow));
|
|
||||||
}
|
}
|
||||||
.cv-edge--sel {
|
.cv-edge--sel {
|
||||||
stroke: var(--m-orange);
|
stroke: var(--m-orange);
|
||||||
stroke-width: 3.5;
|
stroke-width: 3.5;
|
||||||
stroke-dasharray: none;
|
stroke-dasharray: none;
|
||||||
filter: drop-shadow(0 0 8px var(--accent-glow));
|
|
||||||
}
|
}
|
||||||
.cv-edge-hit {
|
.cv-edge-hit {
|
||||||
fill: none;
|
fill: none;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { api, jsonBody, loadImageBlob, loadImageData } from '../api'
|
|||||||
import { auth } from '../auth'
|
import { auth } from '../auth'
|
||||||
import { normalizeDoc, newId, ratioToSize, serializeDoc, screenToWorld, rectsIntersect, DEFAULT_SCALE } from '../utils/canvas'
|
import { normalizeDoc, newId, ratioToSize, serializeDoc, screenToWorld, rectsIntersect, DEFAULT_SCALE } from '../utils/canvas'
|
||||||
import CanvasStage from '../components/canvas/CanvasStage.vue'
|
import CanvasStage from '../components/canvas/CanvasStage.vue'
|
||||||
|
import Icon from '../components/Icon.vue'
|
||||||
import SelectMenu from '../components/SelectMenu.vue'
|
import SelectMenu from '../components/SelectMenu.vue'
|
||||||
|
|
||||||
const stage = ref(null)
|
const stage = ref(null)
|
||||||
@@ -916,10 +917,11 @@ const saveLabel = computed(() => ({
|
|||||||
站点固定顶栏(z-40)盖住,画布工具栏无法点击。 -->
|
站点固定顶栏(z-40)盖住,画布工具栏无法点击。 -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="cv-page">
|
<div class="cv-page">
|
||||||
<!-- 顶栏 -->
|
<!-- 悬浮岛:左上画布切换 / 右上撤销缩放 -->
|
||||||
<div class="cv-top">
|
<div class="cv-top">
|
||||||
<div class="cv-top__left">
|
<div class="cv-island cv-top__left">
|
||||||
<button class="cv-btn" title="返回" @click="$router.push('/user')">←</button>
|
<button class="cv-btn cv-btn--ghost" title="返回" @click="$router.push('/user')"><Icon name="arrow" :size="15" style="transform: rotate(180deg)" /></button>
|
||||||
|
<span class="cv-sep" />
|
||||||
<SelectMenu
|
<SelectMenu
|
||||||
class="cv-dd cv-dd--canvas"
|
class="cv-dd cv-dd--canvas"
|
||||||
:model-value="currentId"
|
:model-value="currentId"
|
||||||
@@ -927,26 +929,30 @@ const saveLabel = computed(() => ({
|
|||||||
placeholder="选择画布"
|
placeholder="选择画布"
|
||||||
@update:model-value="switchCanvas"
|
@update:model-value="switchCanvas"
|
||||||
/>
|
/>
|
||||||
<button class="cv-btn" title="重命名" @click="renameCanvas">✎</button>
|
<button class="cv-btn cv-btn--ghost" title="重命名" @click="renameCanvas">✎</button>
|
||||||
<button class="cv-btn" title="新建画布" @click="newCanvas">+</button>
|
<button class="cv-btn cv-btn--ghost" title="新建画布" @click="newCanvas"><Icon name="plus" :size="15" /></button>
|
||||||
<button class="cv-btn cv-btn--danger" title="删除画布" @click="deleteCanvas">🗑</button>
|
<button class="cv-btn cv-btn--ghost cv-btn--danger" title="删除画布" @click="deleteCanvas"><Icon name="trash" :size="15" /></button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cv-top__right">
|
|
||||||
<button class="cv-btn" :disabled="!undoCount" title="撤销 (Ctrl+Z)" @click="undo">↩</button>
|
|
||||||
<button class="cv-btn" :disabled="!redoCount" title="重做 (Ctrl+Shift+Z)" @click="redo">↪</button>
|
|
||||||
<span class="cv-sep" />
|
|
||||||
<button class="cv-btn" title="缩小 10%" @click="stage?.zoomStep(-1)">−</button>
|
|
||||||
<span class="cv-zoom">{{ scalePct }}%</span>
|
|
||||||
<button class="cv-btn" title="放大 10%" @click="stage?.zoomStep(1)">+</button>
|
|
||||||
<button class="cv-btn" title="重置视图 (0)" @click="stage?.setViewport({ x: 0, y: 0, scale: DEFAULT_SCALE })">⌂</button>
|
|
||||||
<button class="cv-btn" title="适应内容 (1)" @click="stage?.fitToContent?.()">⤢</button>
|
|
||||||
<span class="cv-sep" />
|
|
||||||
<button class="cv-btn cv-btn--accent" @click="addNote">+ 笔记</button>
|
|
||||||
<button class="cv-btn cv-btn--accent" @click="openPicker">🗂 图片库</button>
|
|
||||||
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onFilePicked">
|
|
||||||
<span v-if="saveLabel" class="cv-save" :data-state="saveState">{{ saveLabel }}</span>
|
<span v-if="saveLabel" class="cv-save" :data-state="saveState">{{ saveLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cv-island cv-top__right">
|
||||||
|
<button class="cv-btn cv-btn--ghost" :disabled="!undoCount" title="撤销 (Ctrl+Z)" @click="undo">↩</button>
|
||||||
|
<button class="cv-btn cv-btn--ghost" :disabled="!redoCount" title="重做 (Ctrl+Shift+Z)" @click="redo">↪</button>
|
||||||
|
<span class="cv-sep" />
|
||||||
|
<button class="cv-btn cv-btn--ghost" title="缩小 10%" @click="stage?.zoomStep(-1)">−</button>
|
||||||
|
<span class="cv-zoom">{{ scalePct }}%</span>
|
||||||
|
<button class="cv-btn cv-btn--ghost" title="放大 10%" @click="stage?.zoomStep(1)"><Icon name="plus" :size="15" /></button>
|
||||||
|
<span class="cv-sep" />
|
||||||
|
<button class="cv-btn cv-btn--ghost" title="重置视图 (0)" @click="stage?.setViewport({ x: 0, y: 0, scale: DEFAULT_SCALE })">⌂</button>
|
||||||
|
<button class="cv-btn cv-btn--ghost" title="适应内容 (1)" @click="stage?.fitToContent?.()">⤢</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧工具坞 -->
|
||||||
|
<div class="cv-tools">
|
||||||
|
<button class="cv-tool" title="添加笔记" @click="addNote"><Icon name="plus" :size="18" /><span>笔记</span></button>
|
||||||
|
<button class="cv-tool" title="从图片库添加" @click="openPicker"><Icon name="image" :size="18" /><span>图片库</span></button>
|
||||||
|
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onFilePicked">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 舞台 + 生成面板 -->
|
<!-- 舞台 + 生成面板 -->
|
||||||
@@ -1136,14 +1142,14 @@ const saveLabel = computed(() => ({
|
|||||||
box-shadow: 0 4px 14px rgb(229 72 77 / 0.25);
|
box-shadow: 0 4px 14px rgb(229 72 77 / 0.25);
|
||||||
}
|
}
|
||||||
.cv-btn--accent {
|
.cv-btn--accent {
|
||||||
background: linear-gradient(120deg, rgb(24 24 27 / 0.16), rgb(168 85 247 / 0.14));
|
background: #fff;
|
||||||
border-color: rgb(24 24 27 / 0.42);
|
border-color: rgb(24 24 27 / 0.28);
|
||||||
color: var(--m-orange);
|
color: #18181b;
|
||||||
}
|
}
|
||||||
.cv-btn--accent:hover:not(:disabled) {
|
.cv-btn--accent:hover:not(:disabled) {
|
||||||
background: linear-gradient(120deg, rgb(24 24 27 / 0.26), rgb(168 85 247 / 0.22));
|
background: rgb(24 24 27 / 0.05);
|
||||||
border-color: rgb(24 24 27 / 0.62);
|
border-color: #18181b;
|
||||||
box-shadow: 0 4px 16px var(--accent-glow);
|
box-shadow: 0 4px 14px rgb(24 24 27 / 0.12);
|
||||||
}
|
}
|
||||||
.cv-select,
|
.cv-select,
|
||||||
.cv-input {
|
.cv-input {
|
||||||
@@ -1171,25 +1177,77 @@ const saveLabel = computed(() => ({
|
|||||||
background: var(--app-bg);
|
background: var(--app-bg);
|
||||||
}
|
}
|
||||||
.cv-top {
|
.cv-top {
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
z-index: 20; /* 下拉面板不能被舞台盖住 */
|
z-index: 20; /* 下拉面板不能被舞台盖住 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 9px 14px;
|
pointer-events: none;
|
||||||
border-bottom: 1px solid var(--ink-soft);
|
}
|
||||||
background: var(--surface);
|
.cv-island {
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgb(24 24 27 / 0.08);
|
||||||
|
background: rgb(255 255 255 / 0.92);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
flex-wrap: wrap;
|
box-shadow: 0 1px 2px rgb(24 24 27 / 0.04), 0 12px 32px -12px rgb(24 24 27 / 0.2);
|
||||||
}
|
}
|
||||||
|
.cv-btn--ghost {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
width: 32px;
|
||||||
|
padding: 0;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.cv-tools {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 15;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgb(24 24 27 / 0.08);
|
||||||
|
background: rgb(255 255 255 / 0.92);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
box-shadow: 0 1px 2px rgb(24 24 27 / 0.04), 0 12px 32px -12px rgb(24 24 27 / 0.2);
|
||||||
|
}
|
||||||
|
.cv-tool {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
width: 56px;
|
||||||
|
padding: 9px 0 7px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg-2);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s ease, color 0.12s ease;
|
||||||
|
}
|
||||||
|
.cv-tool:hover { background: rgb(24 24 27 / 0.06); color: #18181b; }
|
||||||
.cv-top__left,
|
.cv-top__left,
|
||||||
.cv-top__right {
|
.cv-top__right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 2px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
/* 统一自定义下拉的宽度/紧凑度(SelectMenu 内部是 .field 按钮) */
|
/* 统一自定义下拉的宽度/紧凑度(SelectMenu 内部是 .field 按钮) */
|
||||||
.cv-dd {
|
.cv-dd {
|
||||||
@@ -1211,11 +1269,9 @@ const saveLabel = computed(() => ({
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.cv-sep {
|
.cv-sep {
|
||||||
width: 2.5px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
border-radius: 2px;
|
background: rgb(24 24 27 / 0.12);
|
||||||
background: var(--ink);
|
|
||||||
opacity: 0.25;
|
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
.cv-zoom {
|
.cv-zoom {
|
||||||
@@ -1224,10 +1280,10 @@ const saveLabel = computed(() => ({
|
|||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
min-width: 46px;
|
min-width: 46px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--hover);
|
background: transparent;
|
||||||
border: 1px solid rgb(24 24 27 / 0.35);
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 8px;
|
||||||
padding: 1px 8px;
|
padding: 1px 4px;
|
||||||
}
|
}
|
||||||
.cv-save {
|
.cv-save {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -1329,20 +1385,16 @@ const saveLabel = computed(() => ({
|
|||||||
/* 生成输入框:参考图缩略图在上,文本框居中,控制条在下 */
|
/* 生成输入框:参考图缩略图在上,文本框居中,控制条在下 */
|
||||||
.cv-composer {
|
.cv-composer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid rgb(24 24 27 / 0.1);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background:
|
background: #fff;
|
||||||
linear-gradient(160deg, rgb(24 24 27 / 0.06), transparent 40%),
|
box-shadow: 0 2px 4px rgb(24 24 27 / 0.04), 0 18px 48px -12px rgb(24 24 27 / 0.2);
|
||||||
var(--surface);
|
|
||||||
-webkit-backdrop-filter: blur(22px) saturate(1.15);
|
|
||||||
backdrop-filter: blur(22px) saturate(1.15);
|
|
||||||
box-shadow: var(--glass-shadow-lg), 0 0 42px var(--accent-glow);
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
overflow: visible; /* 下拉面板要从输入框上方弹出,不能被裁切 */
|
overflow: visible; /* 下拉面板要从输入框上方弹出,不能被裁切 */
|
||||||
}
|
}
|
||||||
.cv-composer:focus-within {
|
.cv-composer:focus-within {
|
||||||
border-color: rgb(24 24 27 / 0.55);
|
border-color: rgb(24 24 27 / 0.55);
|
||||||
box-shadow: var(--glass-shadow-lg), 0 0 0 3px rgb(24 24 27 / 0.16), 0 0 42px var(--accent-glow);
|
box-shadow: 0 0 0 3px rgb(24 24 27 / 0.08), 0 18px 48px -12px rgb(24 24 27 / 0.24);
|
||||||
}
|
}
|
||||||
.cv-composer__refs {
|
.cv-composer__refs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user