feat 存档

This commit is contained in:
2026-03-21 20:21:37 +08:00
parent 21965ef95e
commit 86c63292b2
39 changed files with 8881 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, watch } from 'vue'
import Sidebar from './components/Sidebar.vue'
import { GetNovelContent, WriteNovelContent } from '../wailsjs/go/backend/App'
import { GetNovelContent, WriteNovelContent, GetFangWenContent, GetAIContent, GetToken } from '../wailsjs/go/backend/App'
const activeButton = ref('')
const content = ref('请选择要查看的小说')
@@ -9,14 +9,43 @@ const currentTitle = ref('小说内容')
const viewMode = ref('original')
const currentFilename = ref('')
const isLoading = ref(false)
const aiMessages = ref([])
const tokenInfo = ref('')
// 监听 viewMode 变化
watch(viewMode, (newMode) => {
if (newMode === 'imitation') {
content.value = currentFilename.value + ' 的仿写内容'
watch(viewMode, async (newMode) => {
if (newMode === 'imitation' && currentFilename.value) {
isLoading.value = true
try {
const fangWenContent = await GetFangWenContent(currentFilename.value)
if (fangWenContent) {
content.value = fangWenContent
} else {
content.value = currentFilename.value + ' 的仿写内容'
}
// 获取 token 信息
const token = await GetToken(currentFilename.value)
tokenInfo.value = token
} catch (error) {
console.error('Error getting fangwen content:', error)
content.value = currentFilename.value + ' 的仿写内容'
} finally {
isLoading.value = false
}
} else if (newMode === 'original' && currentFilename.value) {
// 切换回原文模式时,重新加载原文
handleMenuClick({ id: activeButton.value, filename: currentFilename.value })
} else if (newMode === 'ai' && currentFilename.value) {
isLoading.value = true
try {
const aiContent = await GetAIContent(currentFilename.value)
aiMessages.value = aiContent
} catch (error) {
console.error('Error getting AI content:', error)
aiMessages.value = []
} finally {
isLoading.value = false
}
}
})
@@ -53,6 +82,9 @@ async function handleImitation() {
try {
const imitationContent = await WriteNovelContent(currentFilename.value)
content.value = imitationContent
// 更新 token 信息
const token = await GetToken(currentFilename.value)
tokenInfo.value = token
} catch (error) {
console.error('Error generating imitation content:', error)
content.value = '生成仿写内容失败'
@@ -63,6 +95,13 @@ async function handleImitation() {
content.value = '请先选择要仿写的小说'
}
}
// 计算去掉换行符后的字数
function countCharacters(text) {
if (!text) return 0
// 去掉所有换行符,然后统计长度
return text.replace(/\n/g, '').length
}
</script>
<template>
@@ -87,11 +126,22 @@ async function handleImitation() {
>
仿文
</el-button>
<el-button
:type="viewMode === 'ai' ? 'primary' : 'default'"
@click="viewMode = 'ai'"
>
AI对话记录
</el-button>
</div>
</div>
<div class="content-body">
<div v-if="viewMode === 'original'">
{{ content }}
<div v-if="viewMode === 'original'" class="content-wrapper">
<div class="content-scrollable">
{{ content }}
</div>
<div class="content-footer">
字数: {{ countCharacters(content) }}
</div>
</div>
<div v-else-if="viewMode === 'imitation'" class="imitation-container">
<div v-if="!isLoading && (content === '请选择要查看的小说' || content === '正在生成仿写内容...' || content === currentFilename + ' 的仿写内容')" class="imitation-button-container">
@@ -106,12 +156,42 @@ async function handleImitation() {
仿写
</el-button>
</div>
<div v-else class="imitation-content">
{{ content }}
<div v-else class="content-wrapper">
<div class="content-scrollable">
{{ content }}
</div>
<div class="content-footer">
<span class="token-info">{{ tokenInfo }}</span>
<span class="word-count">字数: {{ countCharacters(content) }}</span>
</div>
</div>
</div>
<div v-else-if="viewMode === 'ai'" class="content-wrapper">
<div class="content-scrollable ai-chat-container">
<div
v-for="(message, index) in aiMessages"
:key="index"
:class="['ai-chat-message', message.role === 'user' ? 'user-message' : 'ai-message']"
>
<div class="message-content">
{{ message.content }}
</div>
</div>
<div v-if="aiMessages.length === 0" class="empty-ai-messages">
暂无AI对话记录
</div>
</div>
<div class="content-footer">
对话条数: {{ aiMessages.length }}
</div>
</div>
<div v-else>
{{ content }}
<div v-else class="content-wrapper">
<div class="content-scrollable">
{{ content }}
</div>
<div class="content-footer">
字数: {{ countCharacters(content) }}
</div>
</div>
</div>
</div>
@@ -172,18 +252,78 @@ body {
padding: 20px;
color: #ecf0f1;
min-height: 400px;
display: flex;
flex-direction: column;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.content-scrollable {
flex: 1;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
padding-right: 20px;
text-align: left;
min-height: 0;
}
.content-footer {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: space-between;
align-items: center;
}
.token-info {
text-align: left;
}
.word-count {
text-align: right;
}
.content-scrollable::-webkit-scrollbar {
width: 8px;
}
.content-scrollable::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.content-scrollable::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.content-scrollable::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.imitation-container {
height: 100%;
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
padding: 0;
min-height: 0;
}
.imitation-button-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 0;
}
.imitation-button {
@@ -191,10 +331,42 @@ body {
font-size: 18px;
}
.imitation-content {
height: 100%;
overflow-y: auto;
white-space: pre-wrap;
.ai-chat-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.ai-chat-message {
max-width: 80%;
padding: 10px 15px;
border-radius: 18px;
margin-bottom: 10px;
}
.user-message {
align-self: flex-end;
background-color: #3498db;
color: white;
border-bottom-right-radius: 4px;
}
.ai-message {
align-self: flex-start;
background-color: rgba(255, 255, 255, 0.1);
color: #ecf0f1;
border-bottom-left-radius: 4px;
}
.message-content {
word-break: break-word;
white-space: pre-wrap;
}
.empty-ai-messages {
text-align: center;
color: rgba(255, 255, 255, 0.5);
padding: 40px 0;
font-style: italic;
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { Document } from '@element-plus/icons-vue'
import { GetNovelList } from '../../wailsjs/go/backend/App'
import { ref, onMounted, onUnmounted } from 'vue'
import { Document, Setting, Folder } from '@element-plus/icons-vue'
import { GetNovelList, GetCallWord, SetCallWord, GetModelConfig, SetModelConfig, OpenSourceDir, OpenGenerateDir } from '../../wailsjs/go/backend/App'
const props = defineProps({
activeButton: {
@@ -13,12 +13,99 @@ const props = defineProps({
const emit = defineEmits(['update:activeButton', 'menu-click'])
const novels = ref([])
const dialogVisible = ref(false)
const modelDialogVisible = ref(false)
const callWord = ref([])
const newCallWord = ref('')
const modelConfig = ref({})
const isLoading = ref(false)
function handleNovelClick(novelId, novelLabel) {
emit('update:activeButton', novelId)
emit('menu-click', { id: novelId, filename: novelLabel })
}
async function openCallWordDialog() {
isLoading.value = true
try {
const words = await GetCallWord()
callWord.value = words
dialogVisible.value = true
} catch (error) {
console.error('Error getting call word:', error)
} finally {
isLoading.value = false
}
}
async function openModelDialog() {
isLoading.value = true
try {
const config = await GetModelConfig()
modelConfig.value = config
modelDialogVisible.value = true
} catch (error) {
console.error('Error getting model config:', error)
} finally {
isLoading.value = false
}
}
async function saveModelConfig() {
isLoading.value = true
try {
await SetModelConfig(modelConfig.value)
modelDialogVisible.value = false
} catch (error) {
console.error('Error saving model config:', error)
} finally {
isLoading.value = false
}
}
// 打开原文目录
async function openSourceDirectory() {
try {
await OpenSourceDir()
} catch (error) {
console.error('Error opening source directory:', error)
}
}
// 打开仿文目录
async function openGenerateDirectory() {
try {
await OpenGenerateDir()
} catch (error) {
console.error('Error opening generate directory:', error)
}
}
async function saveCallWord() {
isLoading.value = true
try {
await SetCallWord(callWord.value)
dialogVisible.value = false
} catch (error) {
console.error('Error saving call word:', error)
} finally {
isLoading.value = false
}
}
function addCallWord() {
if (newCallWord.value.trim()) {
callWord.value.push(newCallWord.value.trim())
newCallWord.value = ''
}
}
function adjustTextareaHeight(event) {
const textarea = event.target
textarea.style.height = 'auto'
textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px'
}
async function loadNovelList() {
try {
const novelList = await GetNovelList()
@@ -35,6 +122,14 @@ async function loadNovelList() {
onMounted(() => {
// 组件挂载时加载小说列表
loadNovelList()
// 添加窗口焦点事件监听器
window.addEventListener('focus', loadNovelList)
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
window.removeEventListener('focus', loadNovelList)
})
</script>
@@ -55,7 +150,163 @@ onMounted(() => {
<div v-if="novels.length === 0" class="empty-state">
暂无小说文件
</div>
<div class="setting-button">
<el-button
type="default"
size="small"
@click="openCallWordDialog"
:loading="isLoading"
>
<el-icon><Setting /></el-icon>
<span>设置提示词</span>
</el-button>
<el-button
type="default"
size="small"
@click="openModelDialog"
:loading="isLoading"
>
<el-icon><Setting /></el-icon>
<span>模型设置</span>
</el-button>
<div class="directory-buttons">
<el-button
type="default"
size="small"
@click="openSourceDirectory"
>
<el-icon><Folder /></el-icon>
<span>打开原文目录</span>
</el-button>
<el-button
type="default"
size="small"
@click="openGenerateDirectory"
>
<el-icon><Folder /></el-icon>
<span>打开仿文目录</span>
</el-button>
</div>
</div>
</div>
<!-- 提示词设置对话框 -->
<el-dialog
v-model="dialogVisible"
title="设置提示词"
width="800px"
:close-on-click-modal="false"
>
<div v-if="isLoading" class="dialog-loading">
<el-icon class="is-loading"><i-ep-loading /></el-icon>
加载中...
</div>
<div v-else>
<el-form>
<el-form-item>
<div
v-for="(word, index) in callWord"
:key="index"
class="call-word-item"
>
<el-input
v-model="callWord[index]"
type="textarea"
:rows="2"
class="call-word-textarea"
placeholder="输入提示词"
@input="adjustTextareaHeight"
/>
<el-button
type="danger"
size="small"
@click="callWord.splice(index, 1)"
class="delete-button"
>
删除
</el-button>
</div>
<div class="new-call-word-container">
<el-input
v-model="newCallWord"
type="textarea"
:rows="2"
placeholder="输入新的提示词"
class="new-call-word-input"
@keyup.enter="addCallWord"
@input="adjustTextareaHeight"
/>
<el-button
type="primary"
@click="addCallWord"
class="add-button"
>
添加
</el-button>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveCallWord" :loading="isLoading">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 模型设置对话框 -->
<el-dialog
v-model="modelDialogVisible"
title="模型设置"
width="600px"
:close-on-click-modal="false"
>
<div v-if="isLoading" class="dialog-loading">
<el-icon class="is-loading"><i-ep-loading /></el-icon>
加载中...
</div>
<div v-else>
<el-form>
<el-form-item label="API Key">
<el-input
v-model="modelConfig.key"
placeholder="输入API Key"
class="model-config-input"
/>
</el-form-item>
<el-form-item label="模型名称">
<el-input
v-model="modelConfig.model"
placeholder="输入模型名称"
class="model-config-input"
/>
</el-form-item>
<el-form-item label="输入价格">
<el-input
v-model.number="modelConfig.in"
type="number"
placeholder="输入价格"
class="model-config-input"
/>
</el-form-item>
<el-form-item label="输出价格">
<el-input
v-model.number="modelConfig.out"
type="number"
placeholder="输出价格"
class="model-config-input"
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="modelDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveModelConfig" :loading="isLoading">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@@ -111,4 +362,89 @@ onMounted(() => {
color: #95a5a6;
font-style: italic;
}
.setting-button {
margin-top: 20px;
padding: 0 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.setting-button .el-button {
width: 100%;
justify-content: center;
height: 40px;
font-size: 14px;
margin: 0;
}
.directory-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.directory-buttons .el-button {
flex: 1;
justify-content: center;
height: 40px;
font-size: 14px;
margin: 0;
}
.call-word-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
gap: 10px;
width: 100%;
}
.call-word-textarea {
flex: 1;
min-height: 60px;
resize: none;
overflow: hidden;
}
.delete-button {
margin-top: 4px;
white-space: nowrap;
}
.new-call-word-container {
margin-top: 15px;
display: flex;
gap: 10px;
align-items: flex-start;
width: 100%;
}
.new-call-word-input {
flex: 1;
min-height: 60px;
resize: none;
overflow: hidden;
}
.add-button {
margin-top: 4px;
white-space: nowrap;
}
.model-config-input {
width: 100%;
}
.dialog-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.dialog-loading .el-icon {
margin-right: 10px;
}
</style>