feat(canvas): 画布 Memphis 化并补全编辑功能
- 后端新增 POST /admin/api/my-images 上传接口:图片存入用户 owner 目录并生成缩略图,复用 /images 服务与 my-images picker - 本地图片三种入画布路径:上传按钮 / 拖入舞台(落点即拖放 位置) / Ctrl+V 粘贴 - 节点选中后显示悬浮工具条(预览/复制/置顶/删除)与右下角 缩放手柄;图片等比缩放,便签自由缩放;Ctrl+D 复制选中(含 相互连线);双击空白建笔记;按 1 / ⤢ 按钮适应全部内容 - 整套画布 UI 换 Memphis:纸张底+墨点网格、黑边硬影节点、 黄色贴纸便签、橙色连线/选中态、虚线框选、顶栏/生成面板/ 预览灯箱/图片选择器全部换墨边 chunky 样式,随站点明暗主题 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -49,6 +49,9 @@ const selectedNotes = computed(() =>
|
||||
const pickerOpen = ref(false)
|
||||
const pickerItems = ref([])
|
||||
const pickerLoading = ref(false)
|
||||
const fileInput = ref(null)
|
||||
const uploadBusy = ref(false)
|
||||
const dropHover = ref(false)
|
||||
|
||||
// ================= 文档加载 / 保存 =================
|
||||
|
||||
@@ -285,18 +288,21 @@ function redo() {
|
||||
// ================= 节点操作 =================
|
||||
|
||||
function addNote() {
|
||||
const c = viewportCenterWorld()
|
||||
addNoteAt(viewportCenterWorld())
|
||||
}
|
||||
|
||||
function addNoteAt(w) {
|
||||
const n = {
|
||||
id: newId('t'), type: 'note', text: '',
|
||||
x: Math.round(c.x - 130), y: Math.round(c.y - 70), w: 260, h: 140, z: 0,
|
||||
x: Math.round(w.x - 130), y: Math.round(w.y - 70), w: 260, h: 140, z: 0,
|
||||
}
|
||||
doc.nodes.push(n)
|
||||
selectionIds.value = [n.id]
|
||||
pushHistory()
|
||||
}
|
||||
|
||||
function addImageNode(src, prompt = '') {
|
||||
const c = viewportCenterWorld()
|
||||
function addImageNode(src, prompt = '', at = null) {
|
||||
const c = at || viewportCenterWorld()
|
||||
const n = {
|
||||
id: newId('i'), type: 'image', src, prompt,
|
||||
x: Math.round(c.x - 256 + cascade()), y: Math.round(c.y - 256 + cascade()),
|
||||
@@ -322,6 +328,32 @@ function viewportCenterWorld() {
|
||||
return screenToWorld(w / 2, h / 2, vp)
|
||||
}
|
||||
|
||||
// ---- 复制选中节点(带相互连线,偏移落位) ----
|
||||
function duplicateSelection() {
|
||||
const sel = doc.nodes.filter((n) => selectionIds.value.includes(n.id) && n.type !== 'gen')
|
||||
if (!sel.length) return
|
||||
const idMap = {}
|
||||
const clones = sel.map((n) => {
|
||||
const c = {
|
||||
...n,
|
||||
id: newId(n.type === 'note' ? 't' : 'i'),
|
||||
x: n.x + 36,
|
||||
y: n.y + 36,
|
||||
}
|
||||
idMap[n.id] = c.id
|
||||
return c
|
||||
})
|
||||
doc.nodes.push(...clones)
|
||||
for (const e of [...doc.edges]) {
|
||||
if (idMap[e.from] && idMap[e.to]) {
|
||||
doc.edges.push({ id: newId('e'), from: idMap[e.from], to: idMap[e.to] })
|
||||
}
|
||||
}
|
||||
selectionIds.value = clones.map((c) => c.id)
|
||||
stage.value?.selectIds?.(selectionIds.value)
|
||||
pushHistory()
|
||||
}
|
||||
|
||||
function deleteSelection() {
|
||||
const dead = new Set(selectionIds.value)
|
||||
const deadEdges = stage.value?.consumeSelectedEdges() || []
|
||||
@@ -403,6 +435,60 @@ async function openPicker() {
|
||||
pickerLoading.value = false
|
||||
}
|
||||
|
||||
// ---- 上传本地图片(按钮/拖拽/粘贴三条路径都汇到这里) ----
|
||||
async function uploadImageFile(file, at = null) {
|
||||
if (!file || !file.type?.startsWith('image/')) return
|
||||
uploadBusy.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const r = await api('/my-images', { method: 'POST', body: fd })
|
||||
if (r.ok && r.data?.name) {
|
||||
pickerItems.value.unshift({ name: r.data.name })
|
||||
addImageNode(r.data.name, '', at)
|
||||
} else {
|
||||
genMsg.value = r.data?.detail || `上传失败 (${r.status})`
|
||||
}
|
||||
} catch (e) {
|
||||
genMsg.value = String(e)
|
||||
} finally {
|
||||
uploadBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFilePicked(e) {
|
||||
const files = [...(e.target.files || [])]
|
||||
e.target.value = ''
|
||||
files.forEach((f) => uploadImageFile(f))
|
||||
}
|
||||
|
||||
// 拖拽本地图片落到画布:落点即拖放位置
|
||||
function onDrop(e) {
|
||||
dropHover.value = false
|
||||
const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type?.startsWith('image/'))
|
||||
if (!files.length) return
|
||||
const el = document.querySelector('.cv-stage')
|
||||
const rect = el?.getBoundingClientRect()
|
||||
const vp = stage.value?.vp
|
||||
const at =
|
||||
rect && vp ? screenToWorld(e.clientX - rect.left, e.clientY - rect.top, vp) : null
|
||||
files.forEach((f) => uploadImageFile(f, at))
|
||||
}
|
||||
|
||||
function onPaste(e) {
|
||||
if (isTyping(e.target)) return
|
||||
for (const it of e.clipboardData?.items || []) {
|
||||
if (it.type?.startsWith('image/')) {
|
||||
const f = it.getAsFile()
|
||||
if (f) {
|
||||
e.preventDefault()
|
||||
uploadImageFile(f)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================= 生成 =================
|
||||
|
||||
async function refToBase64(name) {
|
||||
@@ -540,9 +626,20 @@ function onPanZoom(v) {
|
||||
scalePct.value = Math.round(v.scale * 100)
|
||||
}
|
||||
function onNodeAction({ type, id }) {
|
||||
if (type !== 'preview') return
|
||||
const n = doc.nodes.find((x) => x.id === id)
|
||||
if (n?.type === 'image') previewSrc.value = n.src
|
||||
if (!n) return
|
||||
if (type === 'preview') {
|
||||
if (n.type === 'image') previewSrc.value = n.src
|
||||
} else if (type === 'duplicate') {
|
||||
selectionIds.value = [id]
|
||||
duplicateSelection()
|
||||
} else if (type === 'delete') {
|
||||
selectionIds.value = [id]
|
||||
deleteSelection()
|
||||
} else if (type === 'front') {
|
||||
stage.value?.bumpNode?.(id)
|
||||
markDirty()
|
||||
}
|
||||
}
|
||||
|
||||
// ================= 快捷键 =================
|
||||
@@ -567,6 +664,9 @@ function onKeydown(e) {
|
||||
} else if (mod && e.key.toLowerCase() === 'a') {
|
||||
e.preventDefault()
|
||||
stage.value?.selectAll()
|
||||
} else if (mod && e.key.toLowerCase() === 'd') {
|
||||
e.preventDefault()
|
||||
duplicateSelection()
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
e.preventDefault()
|
||||
deleteSelection()
|
||||
@@ -581,6 +681,8 @@ function onKeydown(e) {
|
||||
stage.value?.zoomBy(1 / 1.2)
|
||||
} else if (e.key === '0') {
|
||||
stage.value?.setViewport({ x: 0, y: 0, scale: 1 })
|
||||
} else if (e.key === '1') {
|
||||
stage.value?.fitToContent?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +694,7 @@ function selectAll() {
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
window.addEventListener('paste', onPaste)
|
||||
window.addEventListener('beforeunload', flushSync)
|
||||
document.addEventListener('visibilitychange', onVisibility)
|
||||
const mm = await api('/managed-models')
|
||||
@@ -610,6 +713,7 @@ function onVisibility() {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
window.removeEventListener('paste', onPaste)
|
||||
window.removeEventListener('beforeunload', flushSync)
|
||||
document.removeEventListener('visibilitychange', onVisibility)
|
||||
clearTimeout(saveTimer)
|
||||
@@ -655,17 +759,28 @@ const saveLabel = computed(() => ({
|
||||
<button class="cv-btn" title="缩小" @click="stage?.zoomBy(1 / 1.2)">−</button>
|
||||
<span class="cv-zoom">{{ scalePct }}%</span>
|
||||
<button class="cv-btn" title="放大" @click="stage?.zoomBy(1.2)">+</button>
|
||||
<button class="cv-btn" title="重置视图" @click="stage?.setViewport({ x: 0, y: 0, scale: 1 })">⌂</button>
|
||||
<button class="cv-btn" title="重置视图 (0)" @click="stage?.setViewport({ x: 0, y: 0, scale: 1 })">⌂</button>
|
||||
<button class="cv-btn" title="适应内容 (1)" @click="stage?.fitToContent?.()">⤢</button>
|
||||
<span class="cv-sep" />
|
||||
<button class="cv-btn" @click="addNote">+ 笔记</button>
|
||||
<button class="cv-btn" @click="openPicker">+ 图片</button>
|
||||
<button class="cv-btn cv-btn--accent" @click="addNote">+ 笔记</button>
|
||||
<button class="cv-btn cv-btn--accent" @click="openPicker">+ 图片</button>
|
||||
<button class="cv-btn" :disabled="uploadBusy" title="上传本地图片(也可直接拖入/粘贴)" @click="fileInput?.click()">
|
||||
{{ uploadBusy ? '上传中…' : '⤒ 上传' }}
|
||||
</button>
|
||||
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onFilePicked">
|
||||
<span class="cv-save" :data-state="saveState">{{ saveLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 舞台 + 生成面板 -->
|
||||
<div class="cv-body">
|
||||
<div class="cv-stagewrap">
|
||||
<div
|
||||
class="cv-stagewrap"
|
||||
@dragover.prevent="dropHover = true"
|
||||
@dragleave="dropHover = false"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<div v-if="dropHover" class="cv-drop">松开把图片放进画布</div>
|
||||
<div v-if="loading" class="cv-loading">加载中…</div>
|
||||
<div v-else-if="loadError" class="cv-error">{{ loadError }} <button class="cv-btn" @click="boot">重试</button></div>
|
||||
<CanvasStage
|
||||
@@ -681,6 +796,7 @@ const saveLabel = computed(() => ({
|
||||
@viewport-rest="onViewportRest"
|
||||
@pan-zoom="onPanZoom"
|
||||
@node-action="onNodeAction"
|
||||
@blank-dblclick="addNoteAt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -727,8 +843,8 @@ const saveLabel = computed(() => ({
|
||||
<div v-if="genMsg" class="cv-genmsg">{{ genMsg }}</div>
|
||||
|
||||
<div class="cv-tips">
|
||||
拖节点右侧圆点可连线(点击连线选中,Delete 删除);双击笔记编辑;
|
||||
生成结果会自动连线到来源节点;Ctrl+Z 撤销。
|
||||
双击空白建笔记 · 拖右侧圆点连线 · 选中后右下角缩放 · Ctrl+D 复制 ·
|
||||
本地图片直接拖入或 Ctrl+V 粘贴 · 按 1 适应全部内容
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -743,7 +859,10 @@ const saveLabel = computed(() => ({
|
||||
<div class="cv-picker__box">
|
||||
<div class="cv-picker__head">
|
||||
<span>插入图片(点击放置到画布)</span>
|
||||
<button class="cv-btn" @click="pickerOpen = false">✕</button>
|
||||
<div class="cv-picker__acts">
|
||||
<button class="cv-btn cv-btn--accent" @click="fileInput?.click()">⤒ 上传本地</button>
|
||||
<button class="cv-btn" @click="pickerOpen = false">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pickerLoading" class="cv-loading">加载中…</div>
|
||||
<div v-else-if="!pickerItems.length" class="cv-loading">还没有生成过图片</div>
|
||||
@@ -764,41 +883,53 @@ const saveLabel = computed(() => ({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 画布:Memphis/neo-brutalism 全屏工作台(随站点明暗主题走变量) ===== */
|
||||
.cv-btn {
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgb(255 255 255 / 0.14);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
color: #c6cbd8;
|
||||
padding: 5px 11px;
|
||||
border-radius: 9px;
|
||||
border: 2.5px solid var(--ink);
|
||||
background: var(--surface);
|
||||
color: var(--fg);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
|
||||
}
|
||||
.cv-btn:hover:not(:disabled) {
|
||||
background: rgb(255 255 255 / 0.09);
|
||||
color: #fff;
|
||||
background: var(--m-yellow);
|
||||
}
|
||||
.cv-btn:active:not(:disabled) {
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: 1px 1px 0 var(--shadow-ink);
|
||||
}
|
||||
.cv-btn:disabled {
|
||||
opacity: 0.4;
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.cv-btn--danger:hover:not(:disabled) {
|
||||
border-color: #6b3b44;
|
||||
color: #e58c96;
|
||||
background: #ffb4ab;
|
||||
}
|
||||
.cv-btn--accent {
|
||||
background: var(--m-blue);
|
||||
}
|
||||
.cv-btn--accent:hover:not(:disabled) {
|
||||
background: var(--m-mint);
|
||||
}
|
||||
.cv-select,
|
||||
.cv-input {
|
||||
padding: 6px 9px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgb(255 255 255 / 0.14);
|
||||
background: #0e1015;
|
||||
color: #e8eaf0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 9px;
|
||||
border: 2.5px solid var(--ink);
|
||||
background: var(--surface);
|
||||
color: var(--fg);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.cv-select:focus,
|
||||
.cv-input:focus {
|
||||
border-color: #7c6cff;
|
||||
box-shadow: 3px 3px 0 var(--m-orange);
|
||||
}
|
||||
|
||||
.cv-page {
|
||||
@@ -808,50 +939,62 @@ const saveLabel = computed(() => ({
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0e1015;
|
||||
background: var(--app-bg);
|
||||
}
|
||||
.cv-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.08);
|
||||
background: #12141b;
|
||||
padding: 9px 14px;
|
||||
border-bottom: 3px solid var(--ink);
|
||||
background: var(--surface);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cv-top__left,
|
||||
.cv-top__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cv-select {
|
||||
max-width: 220px;
|
||||
}
|
||||
.cv-sep {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: rgb(255 255 255 / 0.12);
|
||||
width: 2.5px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
background: var(--ink);
|
||||
opacity: 0.25;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.cv-zoom {
|
||||
font-size: 12px;
|
||||
color: #9aa1b2;
|
||||
min-width: 44px;
|
||||
font-weight: 700;
|
||||
color: var(--fg);
|
||||
min-width: 46px;
|
||||
text-align: center;
|
||||
background: var(--m-yellow);
|
||||
border: 2px solid var(--ink);
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
.cv-save {
|
||||
font-size: 11px;
|
||||
color: #7f8798;
|
||||
font-weight: 700;
|
||||
color: var(--fg-3);
|
||||
margin-left: 8px;
|
||||
}
|
||||
.cv-save[data-state='error'] {
|
||||
color: #e58c96;
|
||||
}
|
||||
.cv-save[data-state='saved'] {
|
||||
color: #6fae8c;
|
||||
color: #15803d;
|
||||
}
|
||||
.cv-save[data-state='dirty'],
|
||||
.cv-save[data-state='saving'] {
|
||||
color: var(--m-orange);
|
||||
}
|
||||
.cv-save[data-state='error'] {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.cv-body {
|
||||
@@ -864,6 +1007,21 @@ const saveLabel = computed(() => ({
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cv-drop {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--fg);
|
||||
border: 3px dashed var(--ink);
|
||||
border-radius: 18px;
|
||||
background: color-mix(in srgb, var(--m-blue) 55%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.cv-loading,
|
||||
.cv-error {
|
||||
position: absolute;
|
||||
@@ -872,33 +1030,40 @@ const saveLabel = computed(() => ({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: #9aa1b2;
|
||||
background: #0e1015;
|
||||
color: var(--fg-2);
|
||||
font-weight: 600;
|
||||
background: var(--app-bg);
|
||||
z-index: 5;
|
||||
}
|
||||
.cv-error {
|
||||
color: #e58c96;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* 右侧生成面板 = 白纸卡片 */
|
||||
.cv-panel {
|
||||
width: 260px;
|
||||
width: 264px;
|
||||
flex-shrink: 0;
|
||||
border-left: 1px solid rgb(255 255 255 / 0.08);
|
||||
background: #12141b;
|
||||
padding: 14px;
|
||||
border-left: 3px solid var(--ink);
|
||||
background: var(--surface);
|
||||
padding: 16px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 9px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.cv-panel__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #e8eaf0;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--fg);
|
||||
display: inline-block;
|
||||
background: linear-gradient(transparent 55%, var(--m-yellow) 55%);
|
||||
align-self: flex-start;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.cv-label {
|
||||
font-size: 11px;
|
||||
color: #7f8798;
|
||||
font-weight: 700;
|
||||
color: var(--fg-3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cv-input {
|
||||
@@ -914,35 +1079,49 @@ const saveLabel = computed(() => ({
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cv-chip {
|
||||
padding: 3px 10px;
|
||||
padding: 3px 11px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgb(255 255 255 / 0.14);
|
||||
background: transparent;
|
||||
color: #b6bccb;
|
||||
border: 2.5px solid var(--ink);
|
||||
background: var(--surface);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cv-chip:hover {
|
||||
background: var(--m-blue);
|
||||
}
|
||||
.cv-chip.is-on {
|
||||
border-color: #7c6cff;
|
||||
color: #a99dff;
|
||||
background: rgb(124 108 255 / 0.12);
|
||||
background: var(--m-yellow);
|
||||
box-shadow: 2.5px 2.5px 0 var(--shadow-ink);
|
||||
}
|
||||
.cv-refs {
|
||||
font-size: 11px;
|
||||
color: #7f8798;
|
||||
border: 1px dashed rgb(255 255 255 / 0.14);
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--fg-2);
|
||||
border: 2px dashed var(--ink-soft);
|
||||
border-radius: 9px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.cv-gen {
|
||||
padding: 9px;
|
||||
border-radius: 9px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #7c6cff, #5d8bff);
|
||||
padding: 10px;
|
||||
border-radius: 11px;
|
||||
border: 2.5px solid var(--ink);
|
||||
background: var(--m-orange);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
.cv-gen:hover:not(:disabled) {
|
||||
transform: translate(-1px, -1px);
|
||||
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||
}
|
||||
.cv-gen:active:not(:disabled) {
|
||||
transform: translate(3px, 3px);
|
||||
box-shadow: 1px 1px 0 var(--shadow-ink);
|
||||
}
|
||||
.cv-gen:disabled {
|
||||
opacity: 0.5;
|
||||
@@ -950,28 +1129,30 @@ const saveLabel = computed(() => ({
|
||||
}
|
||||
.cv-genmsg {
|
||||
font-size: 11px;
|
||||
color: #9aa1b2;
|
||||
color: var(--fg-2);
|
||||
word-break: break-all;
|
||||
}
|
||||
.cv-nomodels {
|
||||
font-size: 11px;
|
||||
color: #d8a657;
|
||||
font-weight: 600;
|
||||
color: #92400e;
|
||||
line-height: 1.6;
|
||||
border: 1px dashed rgb(216 166 87 / 0.4);
|
||||
border-radius: 8px;
|
||||
border: 2px dashed #f59e0b;
|
||||
border-radius: 9px;
|
||||
background: var(--m-yellow);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.cv-tips {
|
||||
font-size: 11px;
|
||||
color: #5f6675;
|
||||
line-height: 1.6;
|
||||
color: var(--fg-faint);
|
||||
line-height: 1.7;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cv-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(4 5 8 / 0.9);
|
||||
background: rgb(17 17 17 / 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -981,13 +1162,15 @@ const saveLabel = computed(() => ({
|
||||
.cv-lightbox img {
|
||||
max-width: 92vw;
|
||||
max-height: 92vh;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 10px 10px 0 var(--m-orange);
|
||||
}
|
||||
|
||||
.cv-picker {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(4 5 8 / 0.6);
|
||||
background: rgb(17 17 17 / 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -996,36 +1179,46 @@ const saveLabel = computed(() => ({
|
||||
.cv-picker__box {
|
||||
width: min(720px, 92vw);
|
||||
max-height: 80vh;
|
||||
background: #161922;
|
||||
border: 1px solid rgb(255 255 255 / 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: var(--surface);
|
||||
border: 3px solid var(--ink);
|
||||
border-radius: 16px;
|
||||
box-shadow: 8px 8px 0 var(--shadow-ink);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
.cv-picker__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #e8eaf0;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--fg);
|
||||
}
|
||||
.cv-picker__acts {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.cv-picker__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.cv-picker__grid img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
border: 2.5px solid var(--ink);
|
||||
cursor: pointer;
|
||||
background: #0e1015;
|
||||
background: var(--surface-2);
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
.cv-picker__grid img:hover {
|
||||
outline: 2px solid #7c6cff;
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 4px 4px 0 var(--m-orange);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user