feat(desktop): 优化一些逻辑

1. 优化通知配置

2. 优化命名规范

3. 优化代码逻辑
This commit is contained in:
2026-01-10 23:37:28 +08:00
parent bce411af7e
commit 36cf521851
30 changed files with 674 additions and 575 deletions

View File

@@ -12,6 +12,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AButton: typeof import('@arco-design/web-vue')['Button']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACollapse: typeof import('@arco-design/web-vue')['Collapse']
ACollapseItem: typeof import('@arco-design/web-vue')['CollapseItem']
ActiveMenu: typeof import('./src/components/ActiveMenu.vue')['default']
@@ -23,6 +24,7 @@ declare module 'vue' {
AOption: typeof import('@arco-design/web-vue')['Option']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASlider: typeof import('@arco-design/web-vue')['Slider']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']

View File

@@ -1,17 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<head>
<meta charset="UTF-8" />
<title>读书心得助手 | 让每一篇阅读者享受阅读与思考的乐趣</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -16,7 +16,6 @@ const navButtons = [
]
const active = (key: string) => {
console.log(key)
go('/' + key)
}
</script>
@@ -40,7 +39,7 @@ const active = (key: string) => {
:fill="activeBtn === btn.key ? '#7816ff' : '#64748b'"
/>
<span class="text-[10px] ml-1.5 hidden group-hover:inline-block text-slate-500 font-medium">
<span class="text-[10px] ml-1.5 group-hover:inline-block text-slate-500 font-medium">
{{ btn.title }}
</span>
</div>

View File

@@ -1,22 +1,15 @@
<script setup lang="ts">
import { useRouterHook } from '@renderer/hooks/useRouterHook'
import {
AdProduct,
ArrowRight,
Github,
HoldInterface,
InternalData,
Mail,
Twitter
} from '@icon-park/vue-next'
import { AdProduct, Github, HoldInterface, InternalData, Mail, Twitter } from '@icon-park/vue-next'
import applicationImg from '@renderer/assets/application.png'
const { goBack } = useRouterHook()
const { go } = useRouterHook()
const coreValues = [
{
icon: InternalData,
title: '数据驱动',
desc: '基于自研深度学习模型,精准提取每一本书籍的灵魂。'
desc: '基于模型,精准提取每一本书籍的灵魂。'
},
{
icon: AdProduct,
@@ -34,15 +27,9 @@ const coreValues = [
<template>
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] custom-scroll">
<section class="pt-20 pb-16 px-6 text-center bg-white border-b border-slate-50">
<div class="inline-flex items-center space-x-2 bg-purple-50 px-3 py-1 rounded-full mb-6">
<span class="w-2 h-2 bg-[#7816ff] rounded-full animate-pulse"></span>
<span class="text-[10px] font-bold text-[#7816ff] uppercase tracking-widest"
>Version 2.0.0 Now Live</span
>
</div>
<h1 class="text-4xl font-black text-slate-800 mb-4 tracking-tight">
让每一页文字<br />
<span class="text-[#7816ff]">都有迹可循</span>
<h1 class="text-4xl font-black text-slate-800 mb-4 tracking-tight flex flex-col gap-4">
<span>让每一篇阅读者</span>
<span class="text-[#7816ff]">享受阅读与思考的乐趣</span>
</h1>
<p class="max-w-xl mx-auto text-slate-400 text-sm leading-relaxed mb-8">
我们致力于通过最前沿的 AI 技术帮助深度阅读者高效消化知识
@@ -53,12 +40,10 @@ const coreValues = [
type="primary"
shape="round"
class="bg-[#7816ff] border-none px-8 h-10 shadow-lg shadow-purple-100"
@click="go('/task/create')"
>
开始体验
</a-button>
<a-button shape="round" class="px-8 h-10 border-slate-200" @click="goBack()">
返回探索
</a-button>
</div>
</section>
@@ -90,10 +75,7 @@ const coreValues = [
class="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden flex flex-col md:flex-row items-center"
>
<div class="flex-1 p-12 md:p-16">
<div class="text-[11px] font-bold text-[#7816ff] uppercase tracking-[0.2em] mb-4">
Our Mission
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-6">为了那些真正热爱文字的人</h2>
<h2 class="text-2xl font-bold text-slate-800 mb-6">让那些真正热爱文字的人</h2>
<p class="text-sm text-slate-500 leading-relaxed mb-8">
在一个信息碎片化的时代深度阅读正变得前所未有的奢侈我们不希望 AI
替代阅读而是希望它能作为你的数字笔友帮你梳理逻辑捕捉灵感让你从繁琐的摘要工作中解放回归思考本身
@@ -101,16 +83,16 @@ const coreValues = [
<div class="flex items-center space-x-6">
<div class="flex flex-col">
<span class="text-xl font-black text-slate-800">12,400+</span>
<span class="text-[10px] text-slate-400 font-bold uppercase">Active Users</span>
<span class="text-[10px] text-slate-400 font-bold uppercase">使用用户</span>
</div>
<div class="w-[1px] h-8 bg-slate-100"></div>
<div class="flex flex-col">
<span class="text-xl font-black text-slate-800">8.5M</span>
<span class="text-[10px] text-slate-400 font-bold uppercase">Words Generated</span>
<span class="text-[10px] text-slate-400 font-bold uppercase">阅读量</span>
</div>
</div>
</div>
<div class="flex-1 w-full h-[400px] bg-slate-50 flex items-center justify-center relative">
<div class="flex-1 w-full h-[400px] flex items-center justify-center relative">
<div
class="absolute inset-0 opacity-20"
style="
@@ -119,19 +101,10 @@ const coreValues = [
"
></div>
<div
class="relative z-10 w-64 h-64 bg-white rounded-3xl shadow-xl border border-slate-100 flex flex-col p-6 animate-bounce-slow"
class="relative z-10 w-90 bg-white rounded-3xl shadow-xl border border-slate-100 flex flex-col p-6 animate-bounce-slow"
>
<div class="w-8 h-8 bg-purple-50 rounded-lg mb-4"></div>
<div class="h-2 w-3/4 bg-slate-100 rounded-full mb-3"></div>
<div class="h-2 w-full bg-slate-50 rounded-full mb-3"></div>
<div class="h-2 w-2/3 bg-slate-50 rounded-full"></div>
<div class="mt-auto flex justify-end">
<div
class="w-8 h-8 rounded-full bg-[#7816ff] flex items-center justify-center text-white"
>
<arrow-right size="14" />
</div>
</div>
<img :src="applicationImg" alt="" />
<div class="mt-auto flex justify-end"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,45 @@
export const faqList = [
{
id: 1,
category: 'general',
q: '这个 AI 阅读工具是如何工作的?',
a: '我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的“职业背景”提取特定的知识点。'
},
{
id: 2,
category: 'usage',
q: '生成的字数可以超过 5000 字吗?',
a: '目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。'
},
{
id: 4,
category: 'billing',
q: '当前有订阅计划吗?',
a: '当前工具为免费使用不需要任何费用但是需要自己使用大模型KEY'
},
{
id: 5,
category: 'privacy',
q: '如何查看我的数据?',
a: '你可以在设置页面查看你的数据。'
},
{
id: 6,
category: 'usage',
q: '如何设置大模型?',
a: `
您可以在<span style="color: red">『设置中心』-『大模型配置』中分别设置读书心得与摘要模型</span>。
<br/>推荐使用【阿里云-心流平台 (iflow.cn)】提供的免费 API其单次生成支持长文本处理。若生成内容接近 5000 字上限,建议分章节创建任务以获得最佳逻辑效果。
<br/><br/>可以参考以下操作指南:
<br/>
<ul>
<li>获取 API Key访问 阿里云心流平台 (iflow.cn)。</li>
注册并登录后,在后台创建一个新的 API Key。心流平台目前提供稳定的免费额度非常适合个人阅读助手使用。
<li>配置接口地址:</li>
在应用设置的 Base URL 处填入https://apis.iflow.cn/v1。
<li>模型选择:</li>
平台提供的最新模型 ID。
</ul>
`
}
]

View File

@@ -9,6 +9,7 @@ import {
Security,
Wallet
} from '@icon-park/vue-next'
import { faqList } from './data/faqData'
// 分类配置
const categories = [
@@ -21,34 +22,6 @@ const categories = [
const activeCategory = ref('general')
const searchQuery = ref('')
// 问题数据
const faqList = [
{
id: 1,
category: 'general',
q: '这个 AI 阅读工具是如何工作的?',
a: '我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的“职业背景”提取特定的知识点。'
},
{
id: 2,
category: 'usage',
q: '生成的字数可以超过 5000 字吗?',
a: '目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。'
},
{
id: 3,
category: 'billing',
q: '订阅后可以申请退款吗?',
a: '在订阅后的 24 小时内且未使用过算力资源的情况下,您可以联系客服申请全额退款。'
},
{
id: 4,
category: 'general',
q: '支持哪些格式的书籍?',
a: '目前支持 PDF, EPUB, TXT 格式。为了获得最佳效果,建议上传排版清晰的文档。'
}
]
// 搜索过滤逻辑
const filteredFaqs = computed(() => {
return faqList.filter((item) => {
@@ -110,7 +83,7 @@ const filteredFaqs = computed(() => {
<span class="text-[14px] font-bold text-slate-700">{{ faq.q }}</span>
</template>
<div class="text-[13px] leading-relaxed text-slate-500 py-2">
{{ faq.a }}
<span v-html="faq.a"></span>
</div>
</a-collapse-item>
</a-collapse>

View File

@@ -1,62 +1,91 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, reactive, ref, toRaw } from 'vue'
import { SettingConfig } from '@icon-park/vue-next'
import { Message } from '@arco-design/web-vue'
import { trpc } from '@renderer/lib/trpc'
import { modelOptions } from '../data/ModelData'
import { ModelForm } from '@renderer/pages/setting/types/IModel'
interface ModelOption {
id: string // id modelName
name: string
desc: string
tag?: string
const activeTab = ref<'reading' | 'summary'>('reading') // reading
const modelForm = reactive<ModelForm>({
reading: {
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
baseURL: 'https://apis.iflow.cn/v1',
modelName: 'deepseek-v3.2',
temperature: 0.7
},
summary: {
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
baseURL: 'https://apis.iflow.cn/v1',
modelName: 'deepseek-v3.2',
temperature: 0.3
}
})
// --- ---
const loadConfig = async () => {
try {
const savedConfig = (await trpc.config.getChatConfigs.query()) as any
if (savedConfig) {
// Key chatModels.reading
if (savedConfig.reading) Object.assign(modelForm.reading, savedConfig.reading)
if (savedConfig.summary) Object.assign(modelForm.summary, savedConfig.summary)
}
} catch (error) {
console.error('加载配置失败:', error)
}
}
interface ModelConfig {
apiKey: string
baseURL: string
modelName: string
temperature: number
const handleSave = async (type: 'reading' | 'summary') => {
try {
// 使 toRaw Vue
const configToSave = toRaw(modelForm[type])
// Proxy
console.log('正在保存纯对象数据:', configToSave)
await trpc.config.saveChatConfig.mutate({
type,
config: configToSave
})
Message.success({
content: `${type === 'reading' ? '读书心得' : '摘要总结'}配置已安全保存`,
showIcon: true
})
} catch (error) {
Message.error('保存失败,无法克隆对象')
console.error('tRPC 错误详情:', error)
}
}
//
interface ModelForm {
reflection: ModelConfig
summary: ModelConfig
}
const props = defineProps<{
modelValue: ModelForm // reactive
options?: ModelOption[]
}>()
const emit = defineEmits(['update:modelValue', 'save'])
//
const activeTab = ref<'reflection' | 'summary'>('reflection')
// Tab modelName
const selectModel = (modelId: string) => {
const newData = { ...props.modelValue }
newData[activeTab.value].modelName = modelId
emit('update:modelValue', newData)
modelForm[activeTab.value].modelName = modelId
}
onMounted(() => {
loadConfig()
})
</script>
<template>
<div v-if="modelValue" class="space-y-6 animate-in">
<div class="space-y-6 animate-in pb-10">
<a-tabs v-model:active-key="activeTab" type="capsule" class="custom-tabs">
<a-tab-pane key="reflection" title="读书心得模型" />
<a-tab-pane key="reading" title="读书心得模型" />
<a-tab-pane key="summary" title="摘要总结模型" />
</a-tabs>
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
<div class="flex items-center space-x-2 mb-8">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">接口配置</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="setting-label">Base URL (API 代理地址)</label>
<a-input
v-model="modelValue[activeTab].baseURL"
v-model="modelForm[activeTab].baseURL"
placeholder="https://api.openai.com/v1"
class="custom-input"
/>
@@ -64,7 +93,7 @@ const selectModel = (modelId: string) => {
<div class="space-y-2">
<label class="setting-label">API Key</label>
<a-input-password
v-model="modelValue[activeTab].apiKey"
v-model="modelForm[activeTab].apiKey"
placeholder="sk-..."
class="custom-input"
/>
@@ -77,37 +106,34 @@ const selectModel = (modelId: string) => {
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">模型名称 (modelName)</h3>
</div>
<div class="mb-6">
<a-input
v-model="modelValue[activeTab].modelName"
placeholder="或者手动输入模型 ID如 gpt-4o"
v-model="modelForm[activeTab].modelName"
placeholder="手动输入模型 ID如 gpt-4o"
class="custom-input"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="m in options"
v-for="m in modelOptions"
:key="m.id"
@click="selectModel(m.id)"
:class="[
'p-4 rounded-xl border-2 cursor-pointer transition-all relative overflow-hidden',
modelValue[activeTab].modelName === m.id
modelForm[activeTab].modelName === m.id
? 'border-[#7816ff] bg-purple-50/30'
: 'border-slate-50 bg-slate-50/50 hover:border-slate-200'
]"
>
<div class="flex justify-between items-start mb-2">
<span class="text-xs font-bold text-slate-800">{{ m.name }}</span>
<span v-if="m.tag" class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded">
{{ m.tag }}
</span>
<span v-if="m.tag" class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded">{{
m.tag
}}</span>
</div>
<p class="text-[10px] text-slate-400 leading-relaxed">{{ m.desc }}</p>
<div
v-if="modelValue[activeTab].modelName === m.id"
v-if="modelForm[activeTab].modelName === m.id"
class="absolute -right-2 -bottom-2 text-[#7816ff]/10"
>
<SettingConfig theme="outline" size="48" />
@@ -120,33 +146,31 @@ const selectModel = (modelId: string) => {
<div class="flex items-center space-x-2 mb-8">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">运行参数</h3>
<p class="text-[11px] text-[#7816ff] leading-relaxed">
<p class="text-[11px] text-[#7816ff] leading-relaxed ml-4 italic">
正在配置
<span class="font-bold underline">{{
activeTab === 'reflection' ? '心得生成' : '文本总结'
activeTab === 'reading' ? '心得生成' : '文本总结'
}}</span>
专用模型 建议为总结模型设置较低的 Temperature 以保证稳定性
专用模型
</p>
</div>
<div class="space-y-8">
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="setting-label"
>随机性 (Temperature): {{ modelValue[activeTab].temperature }}</label
>随机性 (Temperature): {{ modelForm[activeTab].temperature }}</label
>
<span class="text-[10px] text-slate-400">建议心得 0.7-0.8总结 0.2-0.3</span>
</div>
<a-slider v-model="modelValue[activeTab].temperature" :min="0" :max="1.2" :step="0.1" />
<a-slider v-model="modelForm[activeTab].temperature" :min="0" :max="1.2" :step="0.1" />
</div>
<div class="flex justify-end pt-4 border-t border-slate-50">
<a-button
type="primary"
class="rounded-lg bg-[#7816ff] border-none px-8 font-bold"
@click="emit('save', activeTab)"
@click="handleSave(activeTab)"
>
保存 {{ activeTab === 'reflection' ? '心得' : '总结' }} 配置
保存 {{ activeTab === 'reading' ? '心得' : '总结' }} 配置
</a-button>
</div>
</div>
@@ -155,7 +179,7 @@ const selectModel = (modelId: string) => {
</template>
<style scoped>
/* 继承原有样式 */
/* 样式部分保持不变... */
.setting-label {
display: block;
font-size: 11px;
@@ -165,17 +189,24 @@ const selectModel = (modelId: string) => {
letter-spacing: 0.05em;
padding: 0 4px;
}
/* Tabs 风格微调适配你的紫色主题 */
:deep(.custom-tabs .arco-tabs-nav-capsule) {
background-color: #f1f5f9;
border-radius: 12px;
padding: 4px;
:deep(.custom-input) {
background-color: #fcfcfd !important;
border: 1px solid #f1f5f9 !important;
border-radius: 12px !important;
font-size: 13px;
}
:deep(.custom-tabs .arco-tabs-nav-capsule-light) {
background-color: #fff;
color: #7816ff;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeIn 0.4s ease-out forwards;
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import { Hourglass } from '@icon-park/vue-next'
</script>
<template>
<div class="h-[600px] flex flex-col items-center justify-center animate-in">
<div
class="relative bg-white rounded-3xl border border-slate-100 shadow-xl shadow-slate-200/50 p-12 max-w-md w-full text-center overflow-hidden"
>
<div class="space-y-3 relative z-10">
<h2 class="text-xl font-bold text-slate-800 tracking-tight">
<span class="text-[#7816ff]">正在构建中</span>
</h2>
<p class="text-sm text-slate-400 leading-relaxed px-3">
我们正在为该模块编写最后一行代码以确保为您提供最极致的体验
</p>
</div>
<div class="mt-10 space-y-4">
<div class="h-2 w-full bg-slate-100 rounded-full overflow-hidden">
<div
class="h-full bg-gradient-to-r from-[#7816ff] to-[#a855f7] w-[50%] rounded-full relative"
>
<div
class="absolute top-0 right-0 h-full w-2 bg-white/30 skew-x-12 animate-shimmer"
></div>
</div>
</div>
</div>
<div class="mt-10 pt-8 border-t border-slate-50 flex items-center justify-center gap-6">
<div class="flex items-center gap-2 text-[11px] text-slate-400 font-medium">
<hourglass theme="outline" size="14" fill="#94a3b8" />
预计下个版本上线
</div>
</div>
</div>
</div>
</template>
<style scoped>
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes shimmer {
from {
transform: translateX(-100%) skewX(-12deg);
}
to {
transform: translateX(200%) skewX(-12deg);
}
}
.animate-shimmer {
animation: shimmer 2s infinite linear;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
</style>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue'
import { Remind, Tips, SettingTwo, MessageSuccess } from '@icon-park/vue-next'
import { Message } from '@arco-design/web-vue'
import { trpc } from '@renderer/lib/trpc'
import { INoticeConfig } from '@shared/types/IConfig'
// 1. 数据结构定义
const notificationForm = reactive<INoticeConfig>({
masterSwitch: true, // 总开关
taskCompleted: true, // 任务完成时提醒
taskFailed: true, // 任务失败时提醒
silentMode: false // 静默模式(仅弹窗无声音)
})
// 2. 加载与保存逻辑
const loadSettings = async () => {
try {
const saved = await trpc.config.getNoticeConfigs.query()
if (saved) Object.assign(notificationForm, saved)
} catch (e) {
console.error('加载通知设置失败', e)
}
}
const handleToggle = async () => {
try {
await trpc.config.saveNoticeConfigs.mutate(toRaw(notificationForm))
Message.success({ content: '通知偏好已更新', showIcon: true })
} catch (e) {
Message.error('保存设置失败')
}
}
onMounted(loadSettings)
</script>
<template>
<div class="space-y-6 animate-in">
<div
class="bg-gradient-to-br from-[#7816ff] to-[#a855f7] rounded-2xl p-8 text-white shadow-lg shadow-purple-200/50"
>
<div class="flex justify-between items-start">
<div class="space-y-2">
<h2 class="text-xl font-bold flex items-center gap-2">
<Remind theme="filled" size="24" fill="#fff" /> 通知管理中心
</h2>
<p class="text-purple-100 text-xs opacity-80">
实时掌握读书心得生成进度确保每一份灵感都能及时送达
</p>
</div>
<a-switch
v-model="notificationForm.masterSwitch"
type="line"
@change="handleToggle"
class="custom-white-switch"
/>
</div>
</div>
<div
class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8"
:class="{ 'opacity-60 grayscale-[0.5] pointer-events-none': !notificationForm.masterSwitch }"
>
<div class="flex items-center justify-between mb-8">
<div class="flex items-center space-x-2">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">任务状态通知</h3>
</div>
<a-tag size="small" color="arcoblue" class="rounded-md font-bold text-[10px]"
>实时推送</a-tag
>
</div>
<div class="space-y-6">
<div
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center">
<message-success theme="outline" size="20" fill="#00b42a" />
</div>
<div>
<div class="text-sm font-bold text-slate-700">生成成功提醒</div>
<div class="text-[11px] text-slate-400">
当读书心得摘要生成完毕时发送桌面通知
</div>
</div>
</div>
<a-checkbox v-model="notificationForm.taskCompleted" @change="handleToggle" />
</div>
<div
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center">
<tips theme="outline" size="20" fill="#f53f3f" />
</div>
<div>
<div class="text-sm font-bold text-slate-700">异常中断提醒</div>
<div class="text-[11px] text-slate-400">
若因网络波动或 API 余额不足导致生成失败时提醒
</div>
</div>
</div>
<a-checkbox v-model="notificationForm.taskFailed" @change="handleToggle" />
</div>
<div
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center">
<setting-two theme="outline" size="20" fill="#7816ff" />
</div>
<div>
<div class="text-sm font-bold text-slate-700">静默模式</div>
<div class="text-[11px] text-slate-400">通知弹出时不再播放系统提示音</div>
</div>
</div>
<a-switch v-model="notificationForm.silentMode" size="small" @change="handleToggle" />
</div>
</div>
</div>
<div class="flex items-start gap-3 p-4 bg-blue-50/50 rounded-xl border border-blue-100">
<remind theme="outline" size="16" fill="#0066ff" class="mt-0.5" />
<p class="text-[11px] text-blue-500 leading-relaxed">
提示通知效果受操作系统专注模式勿扰模式影响如果在设置开启后仍未收到通知请检查系统的通知管理权限
</p>
</div>
</div>
</template>
<style scoped>
.custom-white-switch :deep(.arco-switch-dot) {
background-color: #7816ff;
}
.custom-white-switch :deep(.arco-switch-checked) {
background-color: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.5);
}
/* 继承你之前的动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeIn 0.4s ease-out forwards;
}
</style>

View File

@@ -0,0 +1,8 @@
import { IModelOption } from '../types/IModel'
export const modelOptions: IModelOption[] = [
{ id: 'gpt-4o', name: 'GPT-4o', desc: '能力最强,适合复杂文学分析', tag: '推荐' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5', desc: '响应速度极快', tag: '超快' },
{ id: 'claude-3-5-sonnet-20240620', name: 'Claude 3.5', desc: '文笔细腻', tag: '文笔佳' },
{ id: 'deepseek-v3.2', name: 'deepseekV3.2', desc: '适合快速生成内容', tag: '推荐' }
]

View File

@@ -1,112 +1,28 @@
<script setup lang="ts">
import { onMounted, reactive, ref, toRaw } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { Remind, Right, SettingConfig, ShieldAdd, Wallet } from '@icon-park/vue-next'
import { Message } from '@arco-design/web-vue'
import { trpc } from '@renderer/lib/trpc'
// --- 类型定义 ---
interface ModelOption {
id: string
name: string
desc: string
tag?: string
}
interface ModelConfig {
apiKey: string
baseURL: string
modelName: string
temperature: number
}
interface ModelForm {
reading: ModelConfig // 统一使用 reading
summary: ModelConfig
}
import ModelSetting from '@renderer/pages/setting/components/ModelSetting.vue'
import NotFound from '@renderer/pages/setting/components/NotFound.vue'
import NotificationSetting from '@renderer/pages/setting/components/NotificationSetting.vue'
// --- 状态管理 ---
const activeMenu = ref('model')
const activeTab = ref<'reading' | 'summary'>('reading') // 初始设为 reading
const activeMenuItem = computed(() => {
return menuItems.find((item) => item.key === activeMenu.value)?.component
})
const menuItems = [
{ key: 'model', name: '大模型配置', icon: SettingConfig },
{ key: 'model', name: '大模型配置', icon: SettingConfig, component: ModelSetting },
{ key: 'account', name: '账号安全', icon: ShieldAdd },
{ key: 'billing', name: '订阅与账单', icon: Wallet },
{ key: 'notification', name: '通知设置', icon: Remind }
{ key: 'notification', name: '通知设置', icon: Remind, component: NotificationSetting }
]
const modelForm = reactive<ModelForm>({
reading: {
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
baseURL: 'https://apis.iflow.cn/v1',
modelName: 'deepseek-v3.2',
temperature: 0.7
},
summary: {
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
baseURL: 'https://apis.iflow.cn/v1',
modelName: 'deepseek-v3.2',
temperature: 0.3
}
})
const modelOptions: ModelOption[] = [
{ id: 'gpt-4o', name: 'GPT-4o', desc: '能力最强,适合复杂文学分析', tag: '推荐' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5', desc: '响应速度极快', tag: '超快' },
{ id: 'claude-3-5-sonnet-20240620', name: 'Claude 3.5', desc: '文笔细腻', tag: '文笔佳' },
{ id: 'deepseek-v3.2', name: 'deepseekV3.2', desc: '适合快速生成内容', tag: '推荐' }
]
// --- 逻辑方法 ---
const loadConfig = async () => {
try {
const savedConfig = (await trpc.config.getChatConfigs.query()) as any
if (savedConfig) {
// 这里的 Key 必须与后端存储的 chatModels.reading 对应
if (savedConfig.reading) Object.assign(modelForm.reading, savedConfig.reading)
if (savedConfig.summary) Object.assign(modelForm.summary, savedConfig.summary)
}
} catch (error) {
console.error('加载配置失败:', error)
}
}
const handleSave = async (type: 'reading' | 'summary') => {
try {
// 关键点:使用 toRaw 转换,剥离 Vue 的响应式代理
const configToSave = toRaw(modelForm[type])
// 打印检查一下,确保是普通对象而非 Proxy
console.log('正在保存纯对象数据:', configToSave)
await trpc.config.saveChatConfig.mutate({
type,
config: configToSave
})
Message.success({
content: `${type === 'reading' ? '读书心得' : '摘要总结'}配置已安全保存`,
showIcon: true
})
} catch (error) {
Message.error('保存失败,无法克隆对象')
console.error('tRPC 错误详情:', error)
}
}
const selectModel = (modelId: string) => {
modelForm[activeTab.value].modelName = modelId
}
onMounted(() => {
loadConfig()
})
onMounted(() => {})
</script>
<template>
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] p-8 custom-scroll">
<div class="max-w-5xl mx-auto flex gap-8">
<div class="flex-1 h-full bg-[#FAFAFB] p-8 custom-scroll">
<div class="max-w-5xl mx-auto h-full flex gap-8">
<aside class="w-64 shrink-0">
<div class="flex items-center space-x-2 mb-8 px-2">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
@@ -137,171 +53,16 @@ onMounted(() => {
</div>
</nav>
</aside>
<main class="flex-1">
<div v-if="activeMenu === 'model'" class="space-y-6 animate-in">
<a-tabs v-model:active-key="activeTab" type="capsule" class="custom-tabs">
<a-tab-pane key="reading" title="读书心得模型" />
<a-tab-pane key="summary" title="摘要总结模型" />
</a-tabs>
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
<div class="flex items-center space-x-2 mb-8">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">接口配置</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="setting-label">Base URL (API 代理地址)</label>
<a-input
v-model="modelForm[activeTab].baseURL"
placeholder="https://api.openai.com/v1"
class="custom-input"
/>
</div>
<div class="space-y-2">
<label class="setting-label">API Key</label>
<a-input-password
v-model="modelForm[activeTab].apiKey"
placeholder="sk-..."
class="custom-input"
/>
</div>
</div>
</div>
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
<div class="flex items-center space-x-2 mb-6">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">模型名称 (modelName)</h3>
</div>
<div class="mb-6">
<a-input
v-model="modelForm[activeTab].modelName"
placeholder="手动输入模型 ID如 gpt-4o"
class="custom-input"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="m in modelOptions"
:key="m.id"
@click="selectModel(m.id)"
:class="[
'p-4 rounded-xl border-2 cursor-pointer transition-all relative overflow-hidden',
modelForm[activeTab].modelName === m.id
? 'border-[#7816ff] bg-purple-50/30'
: 'border-slate-50 bg-slate-50/50 hover:border-slate-200'
]"
>
<div class="flex justify-between items-start mb-2">
<span class="text-xs font-bold text-slate-800">{{ m.name }}</span>
<span
v-if="m.tag"
class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded"
>{{ m.tag }}</span
>
</div>
<p class="text-[10px] text-slate-400 leading-relaxed">{{ m.desc }}</p>
<div
v-if="modelForm[activeTab].modelName === m.id"
class="absolute -right-2 -bottom-2 text-[#7816ff]/10"
>
<SettingConfig theme="outline" size="48" />
</div>
</div>
</div>
</div>
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
<div class="flex items-center space-x-2 mb-8">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h3 class="text-sm font-bold text-slate-800">运行参数</h3>
<p class="text-[11px] text-[#7816ff] leading-relaxed ml-4 italic">
正在配置
<span class="font-bold underline">{{
activeTab === 'reading' ? '心得生成' : '文本总结'
}}</span>
专用模型
</p>
</div>
<div class="space-y-8">
<div class="space-y-4">
<div class="flex justify-between items-center">
<label class="setting-label"
>随机性 (Temperature): {{ modelForm[activeTab].temperature }}</label
>
<span class="text-[10px] text-slate-400">建议心得 0.7-0.8总结 0.2-0.3</span>
</div>
<a-slider
v-model="modelForm[activeTab].temperature"
:min="0"
:max="1.2"
:step="0.1"
/>
</div>
<div class="flex justify-end pt-4 border-t border-slate-50">
<a-button
type="primary"
class="rounded-lg bg-[#7816ff] border-none px-8 font-bold"
@click="handleSave(activeTab)"
>
保存 {{ activeTab === 'reading' ? '心得' : '总结' }} 配置
</a-button>
</div>
</div>
</div>
</div>
<main class="flex-1 overflow-y-auto">
<template v-if="activeMenuItem">
<component :is="activeMenuItem"></component>
</template>
<template v-else>
<NotFound />
</template>
</main>
</div>
</div>
</template>
<style scoped>
/* 样式部分保持不变... */
.setting-label {
display: block;
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0 4px;
}
:deep(.custom-input) {
background-color: #fcfcfd !important;
border: 1px solid #f1f5f9 !important;
border-radius: 12px !important;
font-size: 13px;
}
:deep(.custom-tabs .arco-tabs-nav-capsule) {
background-color: #f1f5f9;
border-radius: 12px;
padding: 4px;
}
:deep(.custom-tabs .arco-tabs-nav-capsule-light) {
background-color: #fff;
color: #7816ff;
font-weight: bold;
}
.custom-scroll::-webkit-scrollbar {
width: 4px;
}
.custom-scroll::-webkit-scrollbar-thumb {
background: #e5e7eb;
border-radius: 10px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeIn 0.4s ease-out forwards;
}
</style>
<style scoped></style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { Right } from '@icon-park/vue-next'
defineProps<{
menuItems: Array<{ key: string; name: string; icon: any }>
activeMenu: string
}>()
const emit = defineEmits(['update:activeMenu'])
</script>
<template>
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] p-8 custom-scroll">
<div class="max-w-5xl mx-auto flex gap-8">
<aside class="w-64 shrink-0">
<div class="flex items-center space-x-2 mb-8 px-2">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h2 class="text-base font-bold text-slate-800">设置中心</h2>
</div>
<nav class="space-y-1">
<div
v-for="item in menuItems"
:key="item.key"
@click="emit('update:activeMenu', item.key)"
:class="[
'flex items-center justify-between px-4 py-3 rounded-xl cursor-pointer transition-all duration-300',
activeMenu === item.key
? 'bg-white shadow-sm text-[#7816ff] font-bold border border-slate-100'
: 'text-slate-500 hover:bg-white/60 hover:text-slate-700'
]"
>
<div class="flex items-center gap-3">
<component
:is="item.icon"
theme="outline"
size="18"
:fill="activeMenu === item.key ? '#7816ff' : '#94a3b8'"
/>
<span class="text-[13px]">{{ item.name }}</span>
</div>
<right v-if="activeMenu === item.key" theme="outline" size="14" />
</div>
</nav>
</aside>
<main class="flex-1">
<slot></slot>
</main>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
import { ILLMConfig } from '@shared/types/IConfig'
/**
* 模型类型定义
* */
export interface IModelOption {
id: string
name: string
desc: string
tag?: string
}
/**
* 定义模型表单类型
* */
export interface ModelForm {
reading: ILLMConfig['config']
summary: ILLMConfig['config']
}

View File

@@ -4,7 +4,7 @@ import { Message } from '@arco-design/web-vue'
import { useRouterHook } from '@renderer/hooks/useRouterHook'
import { Refresh, Send } from '@icon-park/vue-next'
import { trpc } from '@renderer/lib/trpc'
import { ReadingReflectionsTask } from '@shared/types/reflections'
import { IReadingReflectionsTask } from '@shared/types/IReadingReflectionTask'
import { eventBus } from '@renderer/lib/eventBus'
const { go } = useRouterHook()
@@ -17,13 +17,14 @@ const form = reactive({
quantity: 1,
prompt: '',
wordCount: 500
} as ReadingReflectionsTask)
} as IReadingReflectionsTask)
const occupationOptions = [
{ label: '学生', value: 'student' },
{ label: '职场白领', value: 'professional' },
{ label: '学者/研究员', value: 'scholar' },
{ label: '自由职业者', value: 'freelancer' }
{ label: '自由职业者', value: 'freelancer' },
{ label: '教师', value: 'teacher' }
]
const handleSubmit = async () => {
@@ -60,18 +61,12 @@ const handleReset = () => {
</script>
<template>
<div class="flex-1 overflow-y-auto bg-[#FAFAFB] p-6 custom-scroll">
<div class="flex items-center justify-between mb-6 mx-auto">
<div class="flex-1 p-6">
<div class="flex-1 bg-white rounded-xl border border-slate-100 shadow-sm p-8 custom-scroll">
<div class="flex items-center space-x-2">
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
<h2 class="text-base font-bold text-slate-800">创建读书心得任务</h2>
</div>
<a-button type="text" size="small" class="text-slate-400" @click="go('/task/list')">
返回列表
</a-button>
</div>
<div class="x-auto bg-white rounded-xl border border-slate-100 shadow-sm p-8">
<a-form :model="form" layout="vertical" @submit="handleSubmit">
<a-form-item field="bookName" label="书籍名称" required>
<template #label><span class="text-xs font-bold text-slate-700">书籍名称</span></template>