- 核心架构: Vue 3 + Vite + TypeScript,零运行时依赖(仅 Vue) - 编辑器: 幻灯片增删排序、元素拖拽八向缩放、双击编辑、12 种元素类型 (标题/正文/列表/数据/金句/图片/形状/图表/卡片/表格/代码/公式) - 图表: 8 种 SVG 自绘(柱状/条形/折线/面积/饼图/环形/雷达/进度) - 富文本: 结构化 segments(加粗/斜体/颜色/高亮/上下标/代码/链接) - AI 能力: 对话编辑、生成整套、润色本页、大纲→逐页生成、一键美化、AI 配图 - 会话绑定: 每份 PPT 独立会话,切换 PPT 自动切换对话历史 - 模板系统: 7 个内置版式 + 用户自存模板 - 演示模式: 全屏播放、键盘/鼠标/滚轮导航、入场动画 - 持久化: localStorage 存 deck/文库/会话/模板/配置 - 支持多服务商: 智谱/DeepSeek/通义/Kimi/豆包/OpenAI/Anthropic/Gemini/Groq/Ollama
193 lines
7.5 KiB
Vue
193 lines
7.5 KiB
Vue
<!-- =====================================================================
|
||
SettingsModal.vue — AI 设置弹窗
|
||
===================================================================== -->
|
||
<script setup lang="ts">
|
||
import { ref, watch } from 'vue'
|
||
import type { AiCfg } from '../../core/types'
|
||
import { store } from '../../core/store'
|
||
|
||
const props = defineProps<{ visible: boolean }>()
|
||
const emit = defineEmits<{
|
||
(e: 'close'): void
|
||
(e: 'toast', msg: string): void
|
||
}>()
|
||
|
||
const PRESETS: Record<string, { protocol: 'openai' | 'anthropic'; base: string; model: string; label: string }> = {
|
||
zhipu: { protocol: 'openai', base: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4.6', label: '智谱 GLM (OpenAI 协议)' },
|
||
zhipu_anth: { protocol: 'anthropic', base: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-4.6', label: '智谱 GLM (Anthropic 协议)' },
|
||
deepseek: { protocol: 'openai', base: 'https://api.deepseek.com', model: 'deepseek-chat', label: 'DeepSeek' },
|
||
qwen: { protocol: 'openai', base: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', label: '通义千问' },
|
||
kimi: { protocol: 'openai', base: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k', label: 'Kimi' },
|
||
doubao: { protocol: 'openai', base: 'https://ark.cn-beijing.volces.com/api/v3', model: 'doubao-pro-32k', label: '豆包' },
|
||
openai: { protocol: 'openai', base: 'https://api.openai.com/v1', model: 'gpt-4o-mini', label: 'OpenAI' },
|
||
anthropic: { protocol: 'anthropic', base: 'https://api.anthropic.com', model: 'claude-sonnet-5', label: 'Anthropic' },
|
||
gemini: { protocol: 'openai', base: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash', label: 'Gemini' },
|
||
groq: { protocol: 'openai', base: 'https://api.groq.com/openai/v1', model: 'llama-3.3-70b-versatile', label: 'Groq' },
|
||
ollama: { protocol: 'openai', base: 'http://localhost:11434/v1', model: 'llama3.1', label: 'Ollama (本地)' }
|
||
}
|
||
|
||
const PROTO_DEFAULTS: Record<string, { base: string; model: string }> = {
|
||
openai: { base: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-4.6' },
|
||
anthropic: { base: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-4.6' }
|
||
}
|
||
|
||
const form = ref<AiCfg>({
|
||
preset: 'zhipu', protocol: 'openai',
|
||
base: '', key: '', model: '', proxy: '',
|
||
imgBase: '', imgKey: '', imgModel: ''
|
||
})
|
||
|
||
/** 从 store 读取并填充表单 */
|
||
function loadFromStore() {
|
||
const c = store.getCfg()
|
||
form.value = {
|
||
preset: PRESETS[c.preset] ? c.preset : 'custom',
|
||
protocol: c.protocol || 'openai',
|
||
base: c.base, key: c.key, model: c.model, proxy: c.proxy,
|
||
imgBase: c.imgBase || '',
|
||
imgKey: c.imgKey || '',
|
||
imgModel: c.imgModel || ''
|
||
}
|
||
}
|
||
|
||
watch(() => props.visible, (v) => {
|
||
if (v) loadFromStore()
|
||
}, { immediate: true })
|
||
|
||
function applyPreset(key: string) {
|
||
const p = PRESETS[key]
|
||
if (!p) return
|
||
form.value.protocol = p.protocol
|
||
form.value.base = p.base
|
||
form.value.model = p.model
|
||
}
|
||
|
||
function onProviderChange() {
|
||
if (form.value.preset !== 'custom') applyPreset(form.value.preset)
|
||
}
|
||
|
||
function applyProtoDefaults(p: string) {
|
||
const d = PROTO_DEFAULTS[p] || PROTO_DEFAULTS.openai
|
||
const other = PROTO_DEFAULTS[p === 'openai' ? 'anthropic' : 'openai']
|
||
if (!form.value.base.trim() || form.value.base.trim() === other.base) form.value.base = d.base
|
||
if (!form.value.model.trim() || form.value.model.trim() === other.model) form.value.model = d.model
|
||
}
|
||
|
||
function onProtocolChange() {
|
||
applyProtoDefaults(form.value.protocol)
|
||
}
|
||
|
||
function save() {
|
||
store.setCfg({
|
||
preset: form.value.preset,
|
||
protocol: form.value.protocol,
|
||
base: form.value.base.trim(),
|
||
key: form.value.key.trim(),
|
||
model: form.value.model.trim(),
|
||
proxy: form.value.proxy.trim(),
|
||
imgBase: (form.value.imgBase || '').trim(),
|
||
imgKey: (form.value.imgKey || '').trim(),
|
||
imgModel: (form.value.imgModel || '').trim()
|
||
})
|
||
const label = form.value.preset === 'custom'
|
||
? (form.value.protocol === 'anthropic' ? 'Anthropic' : 'OpenAI') + ' 自定义'
|
||
: (PRESETS[form.value.preset]?.label || form.value.preset)
|
||
emit('toast', '已保存 AI 设置(' + label + ')')
|
||
emit('close')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="modal-mask" :class="{ hidden: !visible }" @click.self="emit('close')">
|
||
<div class="modal">
|
||
<h3>AI 设置</h3>
|
||
<p class="modal-tip">配置大模型服务商。数据仅保存在本地浏览器。</p>
|
||
|
||
<div class="form-row">
|
||
<label>服务商</label>
|
||
<select v-model="form.preset" @change="onProviderChange">
|
||
<optgroup label="国内">
|
||
<option value="zhipu">智谱 GLM (OpenAI 协议)</option>
|
||
<option value="zhipu_anth">智谱 GLM (Anthropic 协议)</option>
|
||
<option value="deepseek">DeepSeek</option>
|
||
<option value="qwen">通义千问</option>
|
||
<option value="kimi">Kimi</option>
|
||
<option value="doubao">豆包</option>
|
||
</optgroup>
|
||
<optgroup label="海外">
|
||
<option value="openai">OpenAI</option>
|
||
<option value="anthropic">Anthropic</option>
|
||
<option value="gemini">Gemini</option>
|
||
<option value="groq">Groq</option>
|
||
</optgroup>
|
||
<optgroup label="本地">
|
||
<option value="ollama">Ollama</option>
|
||
</optgroup>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>API 协议</label>
|
||
<select v-model="form.protocol" @change="onProtocolChange">
|
||
<option value="openai">OpenAI</option>
|
||
<option value="anthropic">Anthropic</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>Base URL</label>
|
||
<input type="text" v-model="form.base" placeholder="https://..." />
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>API Key</label>
|
||
<input type="password" v-model="form.key" placeholder="sk-..." autocomplete="off" />
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>模型</label>
|
||
<input type="text" v-model="form.model" placeholder="模型名称" />
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>代理 URL(可选)</label>
|
||
<input type="text" v-model="form.proxy" placeholder="留空则直连" />
|
||
</div>
|
||
|
||
<div class="section-title">图像模型(可选,用于 AI 配图)</div>
|
||
|
||
<div class="form-row">
|
||
<label>图像 Base URL</label>
|
||
<input type="text" v-model="form.imgBase" placeholder="留空则复用上方 Base URL" />
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>图像 API Key</label>
|
||
<input type="password" v-model="form.imgKey" placeholder="留空则复用上方 Key" autocomplete="off" />
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label>图像模型</label>
|
||
<input type="text" v-model="form.imgModel" placeholder="dall-e-3" />
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn" @click="emit('close')">取消</button>
|
||
<button class="btn primary" @click="save">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.section-title {
|
||
margin: 18px 0 10px;
|
||
padding-top: 14px;
|
||
border-top: 1px solid var(--ui-border);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--ui-text);
|
||
}
|
||
</style>
|