fix:基本实现相关功能
This commit is contained in:
10
src/renderer/auto-imports.d.ts
vendored
Normal file
10
src/renderer/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
33
src/renderer/components.d.ts
vendored
Normal file
33
src/renderer/components.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
ACollapse: typeof import('@arco-design/web-vue')['Collapse']
|
||||
ACollapseItem: typeof import('@arco-design/web-vue')['CollapseItem']
|
||||
ActiveMenu: typeof import('./src/components/ActiveMenu.vue')['default']
|
||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
||||
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
||||
AOption: typeof import('@arco-design/web-vue')['Option']
|
||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||
ASlider: typeof import('@arco-design/web-vue')['Slider']
|
||||
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
||||
ATabs: typeof import('@arco-design/web-vue')['Tabs']
|
||||
ATag: typeof import('@arco-design/web-vue')['Tag']
|
||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
17
src/renderer/index.html
Normal file
17
src/renderer/index.html
Normal file
@@ -0,0 +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>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
58
src/renderer/src/App.vue
Normal file
58
src/renderer/src/App.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import ActiveMenu from '@renderer/components/ActiveMenu.vue'
|
||||
import { Plus } from '@icon-park/vue-next'
|
||||
import nativeHook from '@renderer/hooks/useRouterHook'
|
||||
import TaskList from '@renderer/pages/task/components/TaskList.vue'
|
||||
|
||||
const { go } = nativeHook()
|
||||
|
||||
const goTaskCreatePage = () => {
|
||||
go('/task/create')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen overflow-hidden">
|
||||
<div class="flex w-full h-full overflow-hidden">
|
||||
<aside class="w-60 flex flex-col mr-4">
|
||||
<div class="px-4 py-8 flex items-center space-x-3 shrink-0 w-full">
|
||||
<div class="flex-1 flex flex-row space-x-3 items-center">
|
||||
<!--产品图标-->
|
||||
<div class="w-9 h-9 bg-black rounded-xl flex items-center justify-center shadow-lg">
|
||||
<span class="text-white text-lg font-bold">Z</span>
|
||||
</div>
|
||||
<!--产品名称-->
|
||||
<span class="font-black text-base tracking-tight">读书心得助手</span>
|
||||
</div>
|
||||
<div class="flex justify-end p-r-20px">
|
||||
<a-button size="mini" @click="goTaskCreatePage">
|
||||
<plus theme="outline" size="15" fill="#333" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!--菜单-->
|
||||
<nav class="flex-1 px-2 space-y-1.5 overflow-y-auto custom-scroll">
|
||||
<div class="space-y-2">
|
||||
<TaskList />
|
||||
</div>
|
||||
</nav>
|
||||
<ActiveMenu />
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
129
src/renderer/src/assets/global.less
Normal file
129
src/renderer/src/assets/global.less
Normal file
@@ -0,0 +1,129 @@
|
||||
// 1. 全局覆盖 Button 组件基础变量(水墨风格)
|
||||
:root {
|
||||
// 主色调(水墨黑/深灰,替代原紫色)
|
||||
--arcoblue-6: #2a2a2a; // 基础墨色(主按钮默认)
|
||||
--arcoblue-5: #444444; // 浅墨色(hover 状态)
|
||||
--arcoblue-7: #1a1a1a; // 深墨色(active 状态)
|
||||
|
||||
// 成功色(水墨绿,低饱和度)
|
||||
--arcogreen-6: #3a4d39;
|
||||
--arcogreen-5: #4b604a;
|
||||
--arcogreen-7: #2d3c2c;
|
||||
|
||||
// 危险色(水墨红,低饱和度)
|
||||
--arco-red-6: #5c3a3a;
|
||||
--arco-red-5: #6d4b4b;
|
||||
--arco-red-7: #4a2d2d;
|
||||
|
||||
// 警告色(水墨黄/赭石色,低饱和度)
|
||||
--arcoyellow-6: #5c4a3a;
|
||||
--arcoyellow-5: #6d5b4b;
|
||||
--arcoyellow-7: #4a3d2d;
|
||||
|
||||
// Button 基础样式变量(适配水墨风格的简约质感)
|
||||
--btn-height-default: 42px; // 略高的按钮,更稳的视觉感
|
||||
--btn-font-size-default: 15px; // 适中字号,提升阅读性
|
||||
--btn-border-radius-default: 4px; // 小圆角,贴合水墨的简约感
|
||||
--btn-border-width: 1px; // 细边框,精致感
|
||||
--btn-border-color: #333333; // 基础边框墨色
|
||||
}
|
||||
|
||||
// 2. 主按钮(水墨黑核心样式)
|
||||
.arco-btn-primary {
|
||||
// 核心背景/文字/边框色
|
||||
--btn-bg-color: var(--arcoblue-6);
|
||||
--btn-text-color: #f5f5f5; // 浅灰文字,对比水墨黑更柔和
|
||||
--btn-border-color: var(--arcoblue-6);
|
||||
|
||||
// 交互状态(水墨渐变)
|
||||
--btn-bg-color-hover: var(--arcoblue-5);
|
||||
--btn-border-color-hover: var(--arcoblue-5);
|
||||
--btn-bg-color-active: var(--arcoblue-7);
|
||||
--btn-border-color-active: var(--arcoblue-7);
|
||||
|
||||
// 禁用状态(浅墨色)
|
||||
--btn-bg-color-disabled: #e0e0e0;
|
||||
--btn-text-color-disabled: #666666;
|
||||
--btn-border-color-disabled: #e0e0e0;
|
||||
|
||||
// 额外样式(强化水墨质感)
|
||||
font-weight: 400; // 常规字重,避免粗体破坏水墨的柔和
|
||||
letter-spacing: 0.5px; // 轻微字间距,提升呼吸感
|
||||
}
|
||||
|
||||
// 3. 次要按钮(水墨浅灰风格)
|
||||
.arco-btn-secondary {
|
||||
--btn-bg-color: #f8f8f8; // 极浅灰背景
|
||||
--btn-text-color: var(--arcoblue-6); // 墨色文字
|
||||
--btn-border-color: #e0e0e0; // 浅灰边框
|
||||
|
||||
// 交互状态
|
||||
--btn-bg-color-hover: #f0f0f0;
|
||||
--btn-border-color-hover: #d0d0d0;
|
||||
--btn-bg-color-active: #e8e8e8;
|
||||
--btn-border-color-active: #c0c0c0;
|
||||
}
|
||||
|
||||
// 4. 虚线按钮(水墨简约虚线)
|
||||
.arco-btn-dashed {
|
||||
--btn-border-style: dashed;
|
||||
--btn-border-color: var(--arcoblue-6); // 墨色虚线
|
||||
--btn-bg-color: transparent;
|
||||
--btn-text-color: var(--arcoblue-6);
|
||||
|
||||
// 交互状态
|
||||
--btn-bg-color-hover: #f8f8f8;
|
||||
--btn-border-color-hover: var(--arcoblue-5);
|
||||
--btn-text-color-hover: var(--arcoblue-5);
|
||||
}
|
||||
|
||||
// 5. 文本按钮(纯墨色文字)
|
||||
.arco-btn-text {
|
||||
--btn-text-color: var(--arcoblue-6);
|
||||
--btn-bg-color: transparent;
|
||||
--btn-border-color: transparent;
|
||||
|
||||
// 交互状态
|
||||
--btn-text-color-hover: var(--arcoblue-5);
|
||||
--btn-bg-color-hover: #f8f8f8;
|
||||
--btn-text-color-active: var(--arcoblue-7);
|
||||
--btn-bg-color-active: #f0f0f0;
|
||||
}
|
||||
|
||||
// 6. 补充:不同状态按钮的水墨风格统一(可选,强化风格)
|
||||
// 成功按钮
|
||||
.arco-btn-success {
|
||||
--btn-bg-color: var(--arcogreen-6);
|
||||
--btn-text-color: #f5f5f5;
|
||||
--btn-border-color: var(--arcogreen-6);
|
||||
--btn-bg-color-hover: var(--arcogreen-5);
|
||||
--btn-bg-color-active: var(--arcogreen-7);
|
||||
}
|
||||
|
||||
// 危险按钮
|
||||
.arco-btn-danger {
|
||||
--btn-bg-color: var(--arco-red-6);
|
||||
--btn-text-color: #f5f5f5;
|
||||
--btn-border-color: var(--arco-red-6);
|
||||
--btn-bg-color-hover: var(--arco-red-5);
|
||||
--btn-bg-color-active: var(--arco-red-7);
|
||||
}
|
||||
|
||||
// 警告按钮
|
||||
.arco-btn-warning {
|
||||
--btn-bg-color: var(--arcoyellow-6);
|
||||
--btn-text-color: #f5f5f5;
|
||||
--btn-border-color: var(--arcoyellow-6);
|
||||
--btn-bg-color-hover: var(--arcoyellow-5);
|
||||
--btn-bg-color-active: var(--arcoyellow-7);
|
||||
}
|
||||
|
||||
// 7. 全局按钮基础样式(统一水墨风格的视觉基调)
|
||||
.arco-btn {
|
||||
// 移除默认阴影,贴合水墨的扁平质感
|
||||
box-shadow: none !important;
|
||||
// 平滑过渡,交互更自然
|
||||
transition: all 0.2s ease-in-out;
|
||||
// 边框样式统一
|
||||
border-width: var(--btn-border-width) !important;
|
||||
}
|
||||
6
src/renderer/src/common/taskStatus.ts
Normal file
6
src/renderer/src/common/taskStatus.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const TASK_STATUS: Record<string, { color: string; text: string; desc: string }> = {
|
||||
PENDING: { color: '#7816ff', text: '任务排队中', desc: '任务排队中,等待系统调度算力...' },
|
||||
WRITING: { color: '#ff5722', text: '运行中', desc: '深度学习模型正在分析文本,请稍后...' },
|
||||
COMPLETED: { color: '#00b42a', text: '任务完成', desc: '任务已完成...' },
|
||||
FAILED: { color: '#86909c', text: '任务失败', desc: '任务失败,正在重试...' }
|
||||
}
|
||||
88
src/renderer/src/components/ActiveMenu.vue
Normal file
88
src/renderer/src/components/ActiveMenu.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
// 保持图标库一致,使用 IconPark 或 Arco Icons
|
||||
import { Help, Info, SettingTwo } from '@icon-park/vue-next'
|
||||
import useRouterHook from '@renderer/hooks/useRouterHook'
|
||||
|
||||
const { go } = useRouterHook()
|
||||
|
||||
const activeBtn = ref('')
|
||||
|
||||
// 定义按钮配置,方便维护
|
||||
const navButtons = [
|
||||
{ key: 'setting', title: '设置', icon: SettingTwo },
|
||||
{ key: 'faq', title: '帮助', icon: Help },
|
||||
{ key: 'about', title: '关于', icon: Info }
|
||||
]
|
||||
|
||||
const active = (key: string) => {
|
||||
console.log(key)
|
||||
go('/' + key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 w-full flex flex-col items-center gap-4">
|
||||
<div class="flex items-center bg-white border border-slate-100 p-1.5 rounded-xl shadow-sm">
|
||||
<div
|
||||
v-for="btn in navButtons"
|
||||
:key="btn.key"
|
||||
class="nav-btn group"
|
||||
:class="{ active: activeBtn === btn.key }"
|
||||
:title="btn.title"
|
||||
@click="active(btn.key)"
|
||||
>
|
||||
<component
|
||||
:is="btn.icon"
|
||||
theme="outline"
|
||||
size="16"
|
||||
class="transition-colors"
|
||||
:fill="activeBtn === btn.key ? '#7816ff' : '#64748b'"
|
||||
/>
|
||||
|
||||
<span class="text-[10px] ml-1.5 hidden group-hover:inline-block text-slate-500 font-medium">
|
||||
{{ btn.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f3ff; // 极淡的紫色背景
|
||||
.arco-icon,
|
||||
.i-icon {
|
||||
color: #7816ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #f5f3ff;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #7816ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮之间的分割线(可选)
|
||||
.nav-btn:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
1
src/renderer/src/env.d.ts
vendored
Normal file
1
src/renderer/src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
84
src/renderer/src/hooks/useRouterHook.ts
Normal file
84
src/renderer/src/hooks/useRouterHook.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { type LocationQueryRaw, type RouteParamsRaw, useRoute, useRouter } from 'vue-router'
|
||||
import { warn } from 'vue'
|
||||
|
||||
export function useRouterHook() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const go = (to: string, query?: LocationQueryRaw, params?: RouteParamsRaw) => {
|
||||
if (!to) return warn('useRouterHook: 路由路径不能为空')
|
||||
|
||||
// 如果有 params,通常意味着你在尝试匹配动态路由
|
||||
// 在 Vue Router 4 中,这种情况下必须配合 name 使用,或者手动拼接到 path 字符串中
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
// 方案 1:强制使用 name 跳转 (前提是你的 to 传入的是路由名字)
|
||||
router
|
||||
.push({
|
||||
name: to,
|
||||
query,
|
||||
params
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
} else {
|
||||
// 方案 2:普通的路径跳转
|
||||
router
|
||||
.push({
|
||||
path: to,
|
||||
query
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
}
|
||||
const replace = (to: string, query?: LocationQueryRaw, params?: RouteParamsRaw) => {
|
||||
if (!to) return warn('useRouterHook: 路由路径不能为空')
|
||||
if (route.path === to) return warn('useRouterHook: 路径不能与当前路径相同')
|
||||
|
||||
// 如果有 params,通常意味着你在尝试匹配动态路由
|
||||
// 在 Vue Router 4 中,这种情况下必须配合 name 使用,或者手动拼接到 path 字符串中
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
// 方案 1:强制使用 name 跳转 (前提是你的 to 传入的是路由名字)
|
||||
router
|
||||
.replace({
|
||||
name: to as string,
|
||||
query,
|
||||
params
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
} else {
|
||||
// 方案 2:普通的路径跳转
|
||||
router
|
||||
.replace({
|
||||
path: to as string,
|
||||
query
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// --- 其他方法保持不变 ---
|
||||
const goBack = (n: number = 1) => {
|
||||
if (window.history.length <= 1) return warn('useRouterHook: 无可返回记录')
|
||||
router.go(-n)
|
||||
}
|
||||
|
||||
const getQuery = (key?: string) => {
|
||||
return key ? (route.query[key] as string | undefined) : { ...route.query }
|
||||
}
|
||||
|
||||
const getParams = (key?: string) => {
|
||||
return key ? (route.params[key] as string | undefined) : { ...route.params }
|
||||
}
|
||||
|
||||
return {
|
||||
router,
|
||||
route,
|
||||
go,
|
||||
replace,
|
||||
goBack,
|
||||
getQuery,
|
||||
getParams,
|
||||
refresh: () => router.go(0)
|
||||
}
|
||||
}
|
||||
|
||||
export default useRouterHook
|
||||
7
src/renderer/src/lib/eventBus.ts
Normal file
7
src/renderer/src/lib/eventBus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import mitt from 'mitt'
|
||||
|
||||
type Events = {
|
||||
'refresh-task-list': void // 定义事件名和参数类型
|
||||
}
|
||||
|
||||
export const eventBus = mitt<Events>()
|
||||
13
src/renderer/src/lib/trpc.ts
Normal file
13
src/renderer/src/lib/trpc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createTRPCProxyClient } from '@trpc/client'
|
||||
import { ipcLink } from 'electron-trpc/renderer'
|
||||
import type { AppRouter } from '@rpc/router'
|
||||
|
||||
export const trpc = createTRPCProxyClient<AppRouter>({
|
||||
transformer: {
|
||||
serialize: (v) => v,
|
||||
deserialize: (v) => v
|
||||
} as any,
|
||||
links: [
|
||||
ipcLink() // 不传任何参数
|
||||
]
|
||||
})
|
||||
10
src/renderer/src/main.ts
Normal file
10
src/renderer/src/main.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from '@renderer/router'
|
||||
import '@arco-design/web-vue/dist/arco.css'
|
||||
import 'virtual:uno.css'
|
||||
import './style.css'
|
||||
import './assets/global.less'
|
||||
import '@icon-park/vue-next/styles/index.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
181
src/renderer/src/pages/about/index.vue
Normal file
181
src/renderer/src/pages/about/index.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouterHook } from '@renderer/hooks/useRouterHook'
|
||||
import {
|
||||
AdProduct,
|
||||
ArrowRight,
|
||||
Github,
|
||||
HoldInterface,
|
||||
InternalData,
|
||||
Mail,
|
||||
Twitter
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
const { goBack } = useRouterHook()
|
||||
|
||||
const coreValues = [
|
||||
{
|
||||
icon: InternalData,
|
||||
title: '数据驱动',
|
||||
desc: '基于自研深度学习模型,精准提取每一本书籍的灵魂。'
|
||||
},
|
||||
{
|
||||
icon: AdProduct,
|
||||
title: '极致体验',
|
||||
desc: '化繁为简,让 AI 创作如同呼吸般自然、流畅。'
|
||||
},
|
||||
{
|
||||
icon: HoldInterface,
|
||||
title: '连接未来',
|
||||
desc: '探索人机协作的新范式,重新定义阅读与写作。'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<p class="max-w-xl mx-auto text-slate-400 text-sm leading-relaxed mb-8">
|
||||
我们致力于通过最前沿的 AI 技术,帮助深度阅读者高效消化知识。
|
||||
从海量文本到结构化心得,只需一键。
|
||||
</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<a-button
|
||||
type="primary"
|
||||
shape="round"
|
||||
class="bg-[#7816ff] border-none px-8 h-10 shadow-lg shadow-purple-100"
|
||||
>
|
||||
开始体验
|
||||
</a-button>
|
||||
<a-button shape="round" class="px-8 h-10 border-slate-200" @click="goBack()">
|
||||
返回探索
|
||||
</a-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto py-20 px-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div
|
||||
v-for="(item, index) in coreValues"
|
||||
:key="index"
|
||||
class="group p-8 bg-white rounded-3xl border border-slate-100 shadow-sm hover:shadow-md transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center mb-6 group-hover:bg-purple-50 transition-colors"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
theme="outline"
|
||||
size="24"
|
||||
class="text-slate-400 group-hover:text-[#7816ff]"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-slate-800 mb-3">{{ item.title }}</h3>
|
||||
<p class="text-xs text-slate-500 leading-relaxed">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto pb-20 px-6">
|
||||
<div
|
||||
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>
|
||||
<p class="text-sm text-slate-500 leading-relaxed mb-8">
|
||||
在一个信息碎片化的时代,深度阅读正变得前所未有的奢侈。我们不希望 AI
|
||||
替代阅读,而是希望它能作为你的“数字笔友”,帮你梳理逻辑、捕捉灵感,让你从繁琐的摘要工作中解放,回归思考本身。
|
||||
</p>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 w-full h-[400px] bg-slate-50 flex items-center justify-center relative">
|
||||
<div
|
||||
class="absolute inset-0 opacity-20"
|
||||
style="
|
||||
background-image: radial-gradient(#7816ff 0.5px, transparent 0.5px);
|
||||
background-size: 20px 20px;
|
||||
"
|
||||
></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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="py-12 px-6 text-center border-t border-slate-100">
|
||||
<div class="flex justify-center space-x-6 mb-8 text-slate-400">
|
||||
<a href="#" class="hover:text-[#7816ff] transition-colors"><github size="20" /></a>
|
||||
<a href="#" class="hover:text-[#7816ff] transition-colors"><twitter size="20" /></a>
|
||||
<a href="#" class="hover:text-[#7816ff] transition-colors"><mail size="20" /></a>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-300 font-medium">
|
||||
© 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 柔和的漂浮动画 */
|
||||
@keyframes bounce-slow {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 按钮微调 */
|
||||
:deep(.arco-btn) {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
199
src/renderer/src/pages/faq/index.vue
Normal file
199
src/renderer/src/pages/faq/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
ArrowRight,
|
||||
Equalizer,
|
||||
HeadsetOne,
|
||||
Help,
|
||||
Search,
|
||||
Security,
|
||||
Wallet
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
// 分类配置
|
||||
const categories = [
|
||||
{ key: 'general', name: '常规问题', icon: Help },
|
||||
{ key: 'usage', name: '使用技巧', icon: Equalizer },
|
||||
{ key: 'billing', name: '订阅支付', icon: Wallet },
|
||||
{ key: 'privacy', name: '隐私安全', icon: Security }
|
||||
]
|
||||
|
||||
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) => {
|
||||
const matchesCategory = item.category === activeCategory.value
|
||||
const matchesSearch = item.q.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
return matchesCategory && matchesSearch
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] custom-scroll">
|
||||
<section class="pt-16 pb-12 px-6 text-center">
|
||||
<h2 class="text-3xl font-black text-slate-800 mb-4">有什么可以帮到你?</h2>
|
||||
<div class="max-w-2xl mx-auto relative">
|
||||
<a-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索您遇到的问题..."
|
||||
class="faq-search-input"
|
||||
size="large"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><search theme="outline" size="18" fill="#94a3b8" /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-6 flex flex-col md:flex-row gap-8 pb-20">
|
||||
<aside class="w-full md:w-56 shrink-0">
|
||||
<div class="flex md:flex-col gap-2 overflow-x-auto pb-4 md:pb-0">
|
||||
<div
|
||||
v-for="cat in categories"
|
||||
:key="cat.key"
|
||||
@click="activeCategory = cat.key"
|
||||
:class="[
|
||||
'flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all whitespace-nowrap',
|
||||
activeCategory === cat.key
|
||||
? 'bg-white shadow-sm text-[#7816ff] font-bold border border-slate-100'
|
||||
: 'text-slate-500 hover:bg-white/60'
|
||||
]"
|
||||
>
|
||||
<component :is="cat.icon" theme="outline" size="18" />
|
||||
<span class="text-[13px]">{{ cat.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 space-y-4">
|
||||
<div v-if="filteredFaqs.length > 0" class="space-y-3">
|
||||
<a-collapse :bordered="false" expand-icon-position="right">
|
||||
<template #expand-icon="{ active }">
|
||||
<div :class="['transition-transform duration-300', active ? 'rotate-90' : '']">
|
||||
<arrow-right theme="outline" size="16" :fill="active ? '#7816ff' : '#94a3b8'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-collapse-item v-for="faq in filteredFaqs" :key="faq.id" class="faq-card">
|
||||
<template #header>
|
||||
<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 }}
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-white rounded-2xl p-12 text-center border border-slate-100">
|
||||
<div
|
||||
class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<search theme="outline" size="24" fill="#cbd5e1" />
|
||||
</div>
|
||||
<p class="text-sm text-slate-400">未找到相关问题,请尝试其他关键词</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 bg-white rounded-2xl p-6 border border-slate-100 flex items-center justify-between shadow-sm"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center text-[#7816ff]"
|
||||
>
|
||||
<headset-one theme="outline" size="24" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-slate-800">仍有疑问?</h4>
|
||||
<p class="text-[11px] text-slate-400">我们的团队通常在 2 小时内回复您的邮件</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="primary" shape="round" class="bg-[#7816ff] border-none px-6">
|
||||
联系支持
|
||||
</a-button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 深度定制搜索框 */
|
||||
:deep(.faq-search-input) {
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid #f1f5f9 !important;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
|
||||
background: white !important;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
:deep(.faq-search-input.arco-input-focus) {
|
||||
border-color: #7816ff !important;
|
||||
box-shadow: 0 10px 25px -5px rgba(120, 22, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 深度定制手风琴卡片 */
|
||||
:deep(.faq-card) {
|
||||
background: white !important;
|
||||
border: 1px solid #f1f5f9 !important;
|
||||
border-radius: 16px !important;
|
||||
margin-bottom: 12px !important;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.faq-card:hover) {
|
||||
border-color: #7816ff;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item-header) {
|
||||
background: transparent !important;
|
||||
padding: 20px 24px !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item-content) {
|
||||
padding: 0 24px 20px 24px !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
5
src/renderer/src/pages/home/index.vue
Normal file
5
src/renderer/src/pages/home/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template></template>
|
||||
|
||||
<style scoped></style>
|
||||
181
src/renderer/src/pages/setting/components/ModelSection.vue
Normal file
181
src/renderer/src/pages/setting/components/ModelSection.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { SettingConfig } from '@icon-park/vue-next'
|
||||
|
||||
interface ModelOption {
|
||||
id: string // 这里的 id 即 modelName
|
||||
name: string
|
||||
desc: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
interface ModelConfig {
|
||||
apiKey: string
|
||||
baseURL: string
|
||||
modelName: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
// 包含两种类型的配置
|
||||
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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="modelValue" class="space-y-6 animate-in">
|
||||
<a-tabs v-model:active-key="activeTab" type="capsule" class="custom-tabs">
|
||||
<a-tab-pane key="reflection" 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"
|
||||
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="modelValue[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="modelValue[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"
|
||||
: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
|
||||
? '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="modelValue[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">
|
||||
正在配置
|
||||
<span class="font-bold underline">{{
|
||||
activeTab === 'reflection' ? '心得生成' : '文本总结'
|
||||
}}</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
|
||||
>
|
||||
<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" />
|
||||
</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)"
|
||||
>
|
||||
保存 {{ activeTab === 'reflection' ? '心得' : '总结' }} 配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
/* Tabs 风格微调适配你的紫色主题 */
|
||||
: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;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
307
src/renderer/src/pages/setting/index.vue
Normal file
307
src/renderer/src/pages/setting/index.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, toRaw } 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
|
||||
}
|
||||
|
||||
// --- 状态管理 ---
|
||||
const activeMenu = ref('model')
|
||||
const activeTab = ref<'reading' | 'summary'>('reading') // 初始设为 reading
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'model', name: '大模型配置', icon: SettingConfig },
|
||||
{ key: 'account', name: '账号安全', icon: ShieldAdd },
|
||||
{ key: 'billing', name: '订阅与账单', icon: Wallet },
|
||||
{ key: 'notification', name: '通知设置', icon: Remind }
|
||||
]
|
||||
|
||||
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()
|
||||
})
|
||||
</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="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">
|
||||
<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>
|
||||
</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>
|
||||
50
src/renderer/src/pages/setting/layout/SettingLayout.vue
Normal file
50
src/renderer/src/pages/setting/layout/SettingLayout.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<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>
|
||||
188
src/renderer/src/pages/task/components/TaskList.vue
Normal file
188
src/renderer/src/pages/task/components/TaskList.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import nativeHook from '@renderer/hooks/useRouterHook'
|
||||
import { CheckOne } from '@icon-park/vue-next'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { IReadingReflectionTaskBatch } from '@shared/types/IReadingReflectionTask'
|
||||
import { TASK_STATUS } from '@renderer/common/taskStatus'
|
||||
import { eventBus } from '@renderer/lib/eventBus'
|
||||
|
||||
const { go, getQuery } = nativeHook()
|
||||
|
||||
// 初始化时从 URL 获取当前选中的 ID,否则默认为空
|
||||
const activeTaskId = ref<string | number>((getQuery('id') as string) || '')
|
||||
const tasks = ref<IReadingReflectionTaskBatch[]>([])
|
||||
|
||||
/**
|
||||
* 获取初始列表数据
|
||||
*/
|
||||
const fetchData = async () => {
|
||||
const result = await trpc.task.getBatches.query()
|
||||
tasks.value = result as unknown as IReadingReflectionTaskBatch[]
|
||||
return tasks.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转并选中
|
||||
*/
|
||||
const goTaskManage = (taskId: string | number) => {
|
||||
activeTaskId.value = taskId
|
||||
// 确保路径是你详情页的正确路径,比如 /task/manager 或 /manager
|
||||
go('/task', { id: taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心修复:监听刷新并自动跳转
|
||||
*/
|
||||
const handleRefreshAndSelectFirst = async () => {
|
||||
const latestTasks = await fetchData()
|
||||
|
||||
// 使用 nextTick 确保数据已经渲染到响应式系统
|
||||
await nextTick()
|
||||
|
||||
if (latestTasks.length > 0) {
|
||||
// 默认选中第一项
|
||||
const firstTask = latestTasks[0]
|
||||
goTaskManage(firstTask.id)
|
||||
} else {
|
||||
// 任务全删光了,跳回创建页
|
||||
go('/task/create')
|
||||
}
|
||||
}
|
||||
|
||||
// --- 订阅逻辑 ---
|
||||
const statusSub = trpc.task.onReadingReflectionStatusUpdate.subscribe(undefined, {
|
||||
onData(data) {
|
||||
const target = tasks.value.find((t) => t.id === data.taskId)
|
||||
if (target) {
|
||||
target.progress = data.progress
|
||||
// 可以在这里根据进度更新状态
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const batchSub = trpc.task.onBatchProgressUpdate.subscribe(undefined, {
|
||||
onData(data) {
|
||||
const targetTask = tasks.value.find((t) => t.id === data.batchId)
|
||||
if (targetTask) {
|
||||
targetTask.progress = data.progress
|
||||
targetTask.status = data.status
|
||||
} else {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const list = await fetchData()
|
||||
// 初始加载时,如果 URL 没参数且列表有数据,自动选第一个
|
||||
if (!getQuery('id') && list.length > 0) {
|
||||
goTaskManage(list[0].id)
|
||||
}
|
||||
|
||||
eventBus.on('refresh-task-list', handleRefreshAndSelectFirst)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
statusSub.unsubscribe()
|
||||
batchSub.unsubscribe()
|
||||
eventBus.off('refresh-task-list', handleRefreshAndSelectFirst)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<TransitionGroup name="list">
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
@click="goTaskManage(task.id)"
|
||||
:class="[
|
||||
'group relative p-4 rounded-xl cursor-pointer transition-all duration-300 border',
|
||||
activeTaskId === task.id
|
||||
? 'bg-white border-slate-200 shadow-sm'
|
||||
: 'border-transparent hover:bg-white/60'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="activeTaskId === task.id"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-[#7816ff] rounded-r-full"
|
||||
></div>
|
||||
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span
|
||||
class="text-[13px] font-bold truncate max-w-[140px] transition-colors"
|
||||
:class="
|
||||
activeTaskId === task.id
|
||||
? 'text-slate-800'
|
||||
: 'text-slate-500 group-hover:text-slate-700'
|
||||
"
|
||||
>
|
||||
{{ task.bookName }}
|
||||
</span>
|
||||
<span class="text-[10px] text-slate-400 font-medium">
|
||||
{{ task.totalCount }} 篇心得
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
v-if="task.status === 'PROCESSING'"
|
||||
class="flex items-center justify-center w-5 h-5 rounded-full bg-purple-50"
|
||||
>
|
||||
<div class="w-1.5 h-1.5 bg-[#7816ff] rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center w-5 h-5 rounded-full bg-green-50 text-green-500"
|
||||
>
|
||||
<check-one theme="filled" size="12" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex justify-between items-center text-[9px] font-bold text-slate-400">
|
||||
<span>{{ TASK_STATUS[task.status]?.text || '进行中' }}</span>
|
||||
<span :class="{ 'text-[#7816ff]': activeTaskId === task.id }"
|
||||
>{{ task.progress }}%</span
|
||||
>
|
||||
</div>
|
||||
<div class="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-700 ease-out rounded-full"
|
||||
:class="
|
||||
task.status === 'COMPLETED' || task.status === 'completed'
|
||||
? 'bg-[#00b42a]'
|
||||
: 'bg-[#7816ff]'
|
||||
"
|
||||
:style="{ width: task.progress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 列表动画样式 */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
/* 确保移动时的平滑效果 */
|
||||
.list-move {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
195
src/renderer/src/pages/task/create.vue
Normal file
195
src/renderer/src/pages/task/create.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { h, reactive } from 'vue'
|
||||
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 { eventBus } from '@renderer/lib/eventBus'
|
||||
|
||||
const { go } = useRouterHook()
|
||||
|
||||
const form = reactive({
|
||||
bookName: '打开心智',
|
||||
description:
|
||||
'心智,是我们对外部世界的认知和一切思维方式的集合,决定了人的信念、思考和行动。成长的本质,就是不断用新的认知打破旧的认知,重塑自己的心智模式。深度思考践行者、成长类公号“L先生说”主理人李睿秋,带你探索心智的底层原理,搭建一套行之有效的成长系统,提供情绪、自驱、行动、学习、思考、创造六个方面的提升路径,从而获得更明晰的头脑,更平静的心态和更有主动权的人生',
|
||||
occupation: 'teacher',
|
||||
quantity: 1,
|
||||
prompt: '',
|
||||
wordCount: 500
|
||||
} as ReadingReflectionsTask)
|
||||
|
||||
const occupationOptions = [
|
||||
{ label: '学生', value: 'student' },
|
||||
{ label: '职场白领', value: 'professional' },
|
||||
{ label: '学者/研究员', value: 'scholar' },
|
||||
{ label: '自由职业者', value: 'freelancer' }
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.bookName) {
|
||||
Message.error('请输入书籍名称')
|
||||
return
|
||||
}
|
||||
const result = await trpc.task.createReadingReflectionsTask.mutate({ ...form })
|
||||
|
||||
if (result.success) {
|
||||
Message.success({
|
||||
content: '任务已加入队列',
|
||||
icon: () => h(Send, { theme: 'filled', fill: '#7816ff' })
|
||||
})
|
||||
|
||||
// 成功后延迟一小会儿跳转,让用户看清提示
|
||||
setTimeout(() => {
|
||||
go('/task') // 跳转到列表页,列表页会自动 fetch 数据
|
||||
eventBus.emit('refresh-task-list')
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(form, {
|
||||
bookName: '',
|
||||
description: '',
|
||||
occupation: 'student',
|
||||
quantity: 1,
|
||||
prompt: '',
|
||||
wordCount: 1000
|
||||
})
|
||||
}
|
||||
</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 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>
|
||||
<a-input
|
||||
v-model="form.bookName"
|
||||
placeholder="请输入书籍完整名称..."
|
||||
class="custom-input"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="description" label="书籍简介">
|
||||
<template #label><span class="text-xs font-bold text-slate-700">书籍简介</span></template>
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="简要描述书籍内容,帮助 AI 提取更准确的关键点..."
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<a-form-item field="occupation" label="阅读职业">
|
||||
<template #label
|
||||
><span class="text-xs font-bold text-slate-700">目标受众职业</span></template
|
||||
>
|
||||
<a-select v-model="form.occupation" class="custom-input">
|
||||
<a-option v-for="item in occupationOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="quantity" label="任务数量">
|
||||
<template #label
|
||||
><span class="text-xs font-bold text-slate-700">生成心得篇数</span></template
|
||||
>
|
||||
<a-input-number v-model="form.quantity" :min="1" :max="100" class="custom-input" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<a-form-item field="wordCount" label="每篇字数">
|
||||
<template #label
|
||||
><span class="text-xs font-bold text-slate-700"
|
||||
>单篇心得字数: {{ form.wordCount }} 字</span
|
||||
></template
|
||||
>
|
||||
<div class="px-2 w-full">
|
||||
<a-slider v-model="form.wordCount" :min="100" :max="5000" :step="100" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="prompt" label="个性化提示词">
|
||||
<template #label
|
||||
><span class="text-xs font-bold text-slate-700">补充要求 (Prompt)</span></template
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.prompt"
|
||||
placeholder="例如:使用鲁迅的文风、增加 3 个实战案例、针对小白用户..."
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
class="custom-input"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div class="flex justify-end items-center gap-3 pt-6 border-t border-slate-50 mt-4">
|
||||
<a-button @click="handleReset" class="rounded-lg px-6">
|
||||
<template #icon><Refresh theme="outline" size="14" /></template>
|
||||
重置
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
class="rounded-lg px-8 bg-[#7816ff] border-none hover:bg-[#620fd9]"
|
||||
>
|
||||
<template #icon><Send theme="outline" size="14" /></template>
|
||||
立即开启任务
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 针对 C 端风格微调 Arco 控件 */
|
||||
:deep(.custom-input) {
|
||||
background-color: #fcfcfd !important;
|
||||
border: 1px solid #f1f5f9 !important;
|
||||
border-radius: 8px !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
:deep(.custom-input:hover) {
|
||||
background-color: #fff !important;
|
||||
border-color: #7816ff !important;
|
||||
}
|
||||
|
||||
:deep(.arco-form-item-label) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-slider-bar) {
|
||||
background-color: #7816ff;
|
||||
}
|
||||
|
||||
:deep(.arco-slider-dot) {
|
||||
border-color: #7816ff;
|
||||
}
|
||||
|
||||
:deep(.arco-slider-btn::after) {
|
||||
border-color: #7816ff;
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
145
src/renderer/src/pages/task/detail.vue
Normal file
145
src/renderer/src/pages/task/detail.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import nativeHook from '@renderer/hooks/useRouterHook'
|
||||
import { Copy, Left, Quote } from '@icon-park/vue-next'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { IReadingReflectionTaskItem } from '@shared/types/IReadingReflectionTask'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
const { getQuery, goBack } = nativeHook()
|
||||
|
||||
const subTaskId = computed(() => getQuery('id') as string)
|
||||
const batchId = computed(() => getQuery('batchId') as string)
|
||||
const content = ref<IReadingReflectionTaskItem | null>(null)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!subTaskId.value) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 从批次列表中筛选出当前心得项
|
||||
const data = await trpc.task.getBatchItems.query({ batchId: batchId.value })
|
||||
content.value = data.find((item) => item.id === subTaskId.value) || null
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅复制正文内容
|
||||
* 排除标题、关键词和摘要
|
||||
*/
|
||||
const handleCopyContent = async () => {
|
||||
if (!content.value?.content) {
|
||||
Message.warning('内容尚未加载,无法复制')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(content.value.content)
|
||||
Message.success({
|
||||
content: '正文已成功复制到剪贴板',
|
||||
duration: 2000
|
||||
})
|
||||
} catch (err) {
|
||||
Message.error('复制失败,请手动选择复制')
|
||||
console.error('Clipboard Error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchDetail())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-white">
|
||||
<header class="h-14 border-b border-slate-50 px-6 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-button type="text" size="small" class="text-slate-400" @click="goBack(1)">
|
||||
<template #icon><left theme="outline" size="18" /></template>
|
||||
</a-button>
|
||||
<span class="text-sm font-bold text-slate-800 tracking-tight">返回列表</span>
|
||||
</div>
|
||||
<a-button
|
||||
size="mini"
|
||||
class="rounded-lg text-slate-500"
|
||||
@click="handleCopyContent"
|
||||
:disabled="!content"
|
||||
>
|
||||
<template #icon><copy theme="outline" size="14" /></template>
|
||||
复制正文
|
||||
</a-button>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto custom-scroll bg-[#FAFAFB] py-12">
|
||||
<article
|
||||
v-if="content"
|
||||
class="w-full max-w-3xl mx-auto bg-white rounded-2xl border border-slate-100 p-12 relative shadow-sm"
|
||||
>
|
||||
<div class="flex flex-col justify-between items-start mb-5 border-b border-slate-50">
|
||||
<h1 class="text-2xl font-black text-slate-800 leading-tight flex-1">
|
||||
{{ content.title }}
|
||||
</h1>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="tag in content.keywords"
|
||||
:key="tag"
|
||||
class="px-2 py-0.5 bg-purple-50 text-[#7816ff] text-[10px] font-bold rounded border border-purple-100/50"
|
||||
>
|
||||
# {{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-600 text-[15px] leading-loose whitespace-pre-wrap min-h-[300px]">
|
||||
{{ content.content }}
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 bg-slate-50 rounded-xl p-6 relative">
|
||||
<quote class="absolute top-4 right-4 text-slate-200" size="24" theme="filled" />
|
||||
<div class="text-[15px] font-black uppercase tracking-widest mb-3">文章摘要</div>
|
||||
<p class="text-xs text-slate-500 leading-relaxed italic">{{ content.summary }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 沉浸式阅读字体微调 */
|
||||
.prose p {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保留功能 */
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #f1f5f9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 打印样式优化 */
|
||||
@media print {
|
||||
header,
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.bg-[#FAFAFB] {
|
||||
background: white;
|
||||
}
|
||||
article {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
208
src/renderer/src/pages/task/index.vue
Normal file
208
src/renderer/src/pages/task/index.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
import nativeHook from '@renderer/hooks/useRouterHook'
|
||||
import { Delete, FilePdfOne, Pause, PreviewOpen } from '@icon-park/vue-next'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import {
|
||||
IReadingReflectionTaskBatch,
|
||||
IReadingReflectionTaskItem
|
||||
} from '@shared/types/IReadingReflectionTask'
|
||||
import { TASK_STATUS } from '@renderer/common/taskStatus'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { eventBus } from '@renderer/lib/eventBus'
|
||||
|
||||
const { getQuery, go } = nativeHook()
|
||||
|
||||
const bookSubTasks = ref<IReadingReflectionTaskItem[]>([])
|
||||
const activeTaskId = computed(() => getQuery('id') as string)
|
||||
const currentTask = ref<IReadingReflectionTaskBatch>({
|
||||
id: '',
|
||||
bookName: '加载中...',
|
||||
totalCount: 0,
|
||||
status: 'PENDING',
|
||||
progress: 0,
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
/**
|
||||
* 删除任务逻辑
|
||||
*/
|
||||
const handleDeleteTask = () => {
|
||||
Modal.warning({
|
||||
title: '确认删除任务',
|
||||
content: '此操作将永久删除该书籍的所有心得记录及生成进度。',
|
||||
hideCancel: false,
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const loading = Message.loading('正在从磁盘清理...')
|
||||
try {
|
||||
const result = await trpc.task.deleteBatch.mutate({
|
||||
batchId: activeTaskId.value as string
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
Message.success('任务已成功删除')
|
||||
eventBus.emit('refresh-task-list')
|
||||
}
|
||||
} catch (err) {
|
||||
Message.error('删除失败,请检查数据库连接')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 预览功能
|
||||
const handlePreview = (subTaskId: string) => {
|
||||
go('/task/detail', {
|
||||
id: subTaskId,
|
||||
batchId: activeTaskId.value
|
||||
})
|
||||
}
|
||||
const statusSub = trpc.task.onReadingReflectionStatusUpdate.subscribe(undefined, {
|
||||
onData(data) {
|
||||
const index = bookSubTasks.value.findIndex((item) => item.id === data.taskId)
|
||||
|
||||
if (index !== -1) {
|
||||
const taskItem = bookSubTasks.value[index]
|
||||
|
||||
// 1. 处理状态映射逻辑
|
||||
if (data.progress === 100) {
|
||||
taskItem.status = 'COMPLETED'
|
||||
} else if (TASK_STATUS[data.status]) {
|
||||
taskItem.status = data.status as any
|
||||
} else {
|
||||
taskItem.status = 'WRITING'
|
||||
}
|
||||
|
||||
// 2. 更新进度
|
||||
taskItem.progress = data.progress
|
||||
|
||||
// 3. 处理生成结果内容
|
||||
if (data.result) {
|
||||
console.log('✅ 收到最终生成内容:', data.result.title)
|
||||
taskItem.title = data.result.title || taskItem.title
|
||||
taskItem.summary = data.result.summary || taskItem.summary
|
||||
}
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
console.error('订阅流异常:', err)
|
||||
}
|
||||
})
|
||||
const batchSub = trpc.task.onBatchProgressUpdate.subscribe(undefined, {
|
||||
onData(data) {
|
||||
if (data.batchId === activeTaskId.value) {
|
||||
currentTask.value.progress = data.progress
|
||||
currentTask.value.status = data.status
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const fetchCurrentTaskDetail = async () => {
|
||||
if (!activeTaskId.value) return
|
||||
const data = await trpc.task.getBatchDetail.query({ batchId: activeTaskId.value })
|
||||
if (data) currentTask.value = data as unknown as IReadingReflectionTaskBatch
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!activeTaskId.value) return
|
||||
bookSubTasks.value = await trpc.task.getBatchItems.query({ batchId: activeTaskId.value })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCurrentTaskDetail()
|
||||
fetchData()
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (activeTaskId.value) {
|
||||
fetchCurrentTaskDetail()
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
statusSub.unsubscribe()
|
||||
batchSub.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-[#FAFAFB]">
|
||||
<header
|
||||
class="h-16 bg-white border-b border-slate-100 flex items-center justify-between px-8 shrink-0 shadow-[0_1px_2px_rgba(0,0,0,0.02)] z-10"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm font-bold text-slate-800 tracking-tight">
|
||||
{{ currentTask.bookName }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-400 font-medium">
|
||||
总体进度:{{ currentTask.progress }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<a-button size="mini" class="hover:bg-slate-50">
|
||||
<template #icon><pause theme="outline" size="12" /></template>
|
||||
暂停队列
|
||||
</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
class="bg-[#7816ff] border-none shadow-sm shadow-purple-200"
|
||||
>
|
||||
<template #icon><file-pdf-one theme="outline" size="12" /></template>
|
||||
打包成果
|
||||
</a-button>
|
||||
<a-button size="mini" status="danger" class="hover:bg-slate-50" @click="handleDeleteTask">
|
||||
<template #icon><delete theme="outline" size="12" /></template>
|
||||
删除任务
|
||||
</a-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 custom-scroll">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="book in bookSubTasks"
|
||||
:key="book.id"
|
||||
class="batch-card group bg-white p-5 flex flex-col border border-slate-100 shadow-sm hover:shadow-md hover:border-slate-200 transition-all duration-300 rounded-xl relative overflow-hidden"
|
||||
@click="book.status === 'COMPLETED' ? handlePreview(book.id) : null"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 right-0 w-16 h-16 opacity-[0.03] -mr-4 -mt-4 transition-transform group-hover:scale-110"
|
||||
>
|
||||
<preview-open theme="outline" size="64" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between relative z-10 flex-row">
|
||||
<p class="flex-1 font-bold text-sm text-slate-800 leading-tight line-clamp-2 pr-4">
|
||||
{{ book.title || '生成中...' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-1 text-[11px] leading-relaxed text-slate-500 line-clamp-3 italic mt-2">
|
||||
{{ book.summary || TASK_STATUS[book.status]?.desc }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-slate-50 mt-4">
|
||||
<a-tag :color="TASK_STATUS[book.status]?.color" size="small" round>
|
||||
{{ TASK_STATUS[book.status]?.text }}
|
||||
</a-tag>
|
||||
|
||||
<a-button
|
||||
size="mini"
|
||||
type="secondary"
|
||||
class="rounded-lg text-[#7816ff] bg-purple-50 border-none hover:bg-purple-100"
|
||||
:disabled="book.status !== 'COMPLETED'"
|
||||
@click.stop="handlePreview(book.id)"
|
||||
>
|
||||
预览成果
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
127
src/renderer/src/pages/task/list.vue
Normal file
127
src/renderer/src/pages/task/list.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouterHook } from '@renderer/hooks/useRouterHook'
|
||||
|
||||
const { go } = useRouterHook()
|
||||
|
||||
// 模拟书籍子任务数据
|
||||
const bookSubTasks = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '《三体》:硬核科幻中的人性博弈',
|
||||
status: 'done',
|
||||
description: '基于黑暗森林法则的深度解析,探讨文明生存的终极逻辑...'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '《人类简史》:从认知革命到智人兴起',
|
||||
status: 'running',
|
||||
description: '正在调用深度学习模型进行文本分析,生成结构化心得,请耐心等待排队...'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '《原则》:生活与工作的实战手册',
|
||||
status: 'loading',
|
||||
description: '任务已进入队列,等待算力调度中...'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '《认知觉醒》:开启自我改变的原动力',
|
||||
status: 'success',
|
||||
description: '生成完成!包含思维导图、核心金句及实践建议...'
|
||||
}
|
||||
])
|
||||
|
||||
// 状态映射颜色
|
||||
const getStatusTag = (status: string) => {
|
||||
const map: any = {
|
||||
done: { color: '#7816ff', text: '已完成' },
|
||||
success: { color: '#00b42a', text: '解析成功' },
|
||||
running: { color: '#ff5722', text: '生成中' },
|
||||
loading: { color: '#86909c', text: '排队中' }
|
||||
}
|
||||
return map[status] || map.loading
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto bg-[#FAFAFB] p-6 custom-scroll">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<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="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
@click="go('/task')"
|
||||
style="background: #7816ff; border: none"
|
||||
>
|
||||
+ 新建任务
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="book in bookSubTasks"
|
||||
:key="book.id"
|
||||
class="batch-card bg-white p-5 flex flex-col border border-slate-100 shadow-sm hover:shadow-md transition-shadow duration-300 rounded-lg"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<h3
|
||||
class="font-bold text-sm leading-tight text-slate-800 line-clamp-1 pr-2"
|
||||
:title="book.title"
|
||||
>
|
||||
{{ book.title }}
|
||||
</h3>
|
||||
<a-tag :color="getStatusTag(book.status).color" size="small" round border>
|
||||
{{ getStatusTag(book.status).text }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-[11px] leading-relaxed text-slate-500 mb-6 line-clamp-3 h-12">
|
||||
{{ book.description }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end pt-4 border-t border-slate-50">
|
||||
<a-button size="mini" type="text" style="color: #64748b"> 编辑 </a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
:disabled="book.status === 'loading'"
|
||||
:style="book.status === 'done' ? 'background: #7816ff' : ''"
|
||||
>
|
||||
预览结果
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 保持你的 custom-scroll 样式 */
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 卡片微动效 */
|
||||
.batch-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.batch-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Arco Tag 微调 */
|
||||
:deep(.arco-tag) {
|
||||
font-size: 10px;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
45
src/renderer/src/router/index.ts
Normal file
45
src/renderer/src/router/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/task'
|
||||
},
|
||||
{
|
||||
path: '/task',
|
||||
name: 'Task',
|
||||
component: () => import('@renderer/pages/task/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/task/create',
|
||||
name: 'TaskCreate',
|
||||
component: () => import('@renderer/pages/task/create.vue')
|
||||
},
|
||||
{
|
||||
path: '/task/detail',
|
||||
name: 'TaskDetail',
|
||||
component: () => import('@renderer/pages/task/detail.vue')
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'Setting',
|
||||
component: () => import('@renderer/pages/setting/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('@renderer/pages/about/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/faq',
|
||||
name: 'FAQ',
|
||||
component: () => import('@renderer/pages/faq/index.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
routes,
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
export default router
|
||||
33
src/renderer/src/style.css
Normal file
33
src/renderer/src/style.css
Normal file
@@ -0,0 +1,33 @@
|
||||
:root {
|
||||
--color-primary: #000000;
|
||||
--color-bg: #f6f7f9;
|
||||
--radius-card: 10px;
|
||||
--arcoblue-6: 0, 0, 0
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--color-bg);
|
||||
font-family: 'Inter',
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保留功能 */
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
}
|
||||
Reference in New Issue
Block a user