新增: 资料导入增强+剪贴板+框选多选+未保存自动暂存+图片占位
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
import { computed, watch, ref } from 'vue'
|
||||
import { store } from '../../core/store'
|
||||
import { elementTypes } from '../../core/sample'
|
||||
import { generateImage, isImageConfigured } from '../../core/ai'
|
||||
import { generateImage, isImageConfigured, checkImageQuota } from '../../core/ai'
|
||||
import { readAsDataURL } from '../../core/importer'
|
||||
import { markdownToSegments, segmentsToPlain, hasFormatting } from '../../core/richtext'
|
||||
import AddGrid from './AddGrid.vue'
|
||||
import type { ElementType, ChartType } from '../../core/types'
|
||||
@@ -23,6 +24,7 @@ const CHART_TYPES: Array<{ k: ChartType; label: string; icon: string }> = [
|
||||
|
||||
const selected = computed(() => store.getSelected())
|
||||
const slide = computed(() => store.currentSlide.value)
|
||||
const multiCount = computed(() => store.getSelection().length)
|
||||
|
||||
/** 临时输入态(v-model 绑定) */
|
||||
const content = ref('')
|
||||
@@ -80,6 +82,16 @@ function onAdd(type: ElementType) {
|
||||
store.addElement(type)
|
||||
}
|
||||
|
||||
/* 多选批量操作 */
|
||||
function onMultiCopy() {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length) store.copyElements(sel)
|
||||
}
|
||||
function onMultiDel() {
|
||||
const sel = store.getSelection()
|
||||
if (sel.length) store.delElements(sel)
|
||||
}
|
||||
|
||||
function onContentInput() {
|
||||
if (!selected.value) return
|
||||
store.updateElement(selected.value.id, { content: content.value })
|
||||
@@ -161,7 +173,7 @@ function applyMarkdown() {
|
||||
if (!el) return
|
||||
const segs = markdownToSegments(content.value)
|
||||
if (segs.length && hasFormatting(segs)) {
|
||||
store.updateElement(el.id, { segments: segs } as any)
|
||||
store.updateElement(el.id, { segments: segs })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,10 +181,10 @@ function applyMarkdown() {
|
||||
function clearRich() {
|
||||
const el = selected.value
|
||||
if (!el) return
|
||||
store.updateElement(el.id, { segments: undefined } as any)
|
||||
store.updateElement(el.id, { segments: undefined })
|
||||
}
|
||||
|
||||
/* ---------- AI 配图 ---------- */
|
||||
/* ---------- AI 配图 / 本地换图 ---------- */
|
||||
async function onAiImage() {
|
||||
if (imgBusy.value) return
|
||||
const el = selected.value
|
||||
@@ -184,6 +196,8 @@ async function onAiImage() {
|
||||
imgAbort = new AbortController()
|
||||
try {
|
||||
const r = await generateImage({ prompt: promptText, signal: imgAbort.signal })
|
||||
const quotaErr = checkImageQuota(r.url)
|
||||
if (quotaErr) { alert(quotaErr); return }
|
||||
store.updateElement(el.id, { content: r.url })
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') alert('配图失败:' + (e?.message || String(e)))
|
||||
@@ -191,6 +205,28 @@ async function onAiImage() {
|
||||
imgBusy.value = false; imgAbort = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 本地上传图片替换当前图片元素内容 */
|
||||
function onLocalImage() {
|
||||
const el = selected.value
|
||||
if (!el || el.type !== 'image') return
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
const dataUrl = await readAsDataURL(file)
|
||||
const quotaErr = checkImageQuota(dataUrl)
|
||||
if (quotaErr) { alert(quotaErr); return }
|
||||
store.updateElement(el.id, { content: dataUrl })
|
||||
} catch (e: any) {
|
||||
alert('读取图片失败:' + (e?.message || String(e)))
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -201,6 +237,24 @@ async function onAiImage() {
|
||||
<AddGrid @add="onAdd" />
|
||||
</section>
|
||||
|
||||
<!-- 多选态:批量操作 -->
|
||||
<section v-else-if="multiCount > 1" class="prop-section" id="propMulti">
|
||||
<h4 class="prop-title">多选 · <span>{{ multiCount }} 个元素</span></h4>
|
||||
<div class="prop-row">
|
||||
<label>批量操作</label>
|
||||
<div class="seg">
|
||||
<button @click="onMultiCopy" title="Ctrl+C">⧉ 复制</button>
|
||||
<button class="danger" @click="onMultiDel" title="Delete">🗑 删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label>提示</label>
|
||||
<div style="font-size:12px;color:var(--ui-muted);line-height:1.6">
|
||||
拖动整组移动 · Shift+点击加选/减选<br />空白处拖动框选 · Esc 取消多选
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 选中元素 -->
|
||||
<section v-else class="prop-section" id="propElement">
|
||||
<h4 class="prop-title">元素 · <span>{{ typeLabel }}</span></h4>
|
||||
@@ -321,10 +375,13 @@ async function onAiImage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 配图(仅图片元素) -->
|
||||
<!-- 图片来源(仅图片元素) -->
|
||||
<div v-if="selected.type === 'image'" class="prop-row">
|
||||
<label>AI 配图</label>
|
||||
<button class="btn" :disabled="imgBusy" @click="onAiImage">{{ imgBusy ? '生成中…' : '🎨 AI 配图' }}</button>
|
||||
<label>图片来源</label>
|
||||
<div class="seg">
|
||||
<button @click="onLocalImage" title="从本地选择图片">📁 本地图片</button>
|
||||
<button :disabled="imgBusy" @click="onAiImage">{{ imgBusy ? '生成中…' : '🎨 AI 配图' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 层级 -->
|
||||
|
||||
Reference in New Issue
Block a user