将输入区域提取为组件
This commit is contained in:
parent
e8c034937e
commit
7b056d476d
@ -3,7 +3,7 @@ const dotenv = require('dotenv')
|
||||
const packageJson = require('../../package.json')
|
||||
|
||||
const baseConfig = {
|
||||
productName: packageJson.name,
|
||||
productName: '文案助手',
|
||||
appId: packageJson.appId,
|
||||
asar: true,
|
||||
extends: null,
|
||||
|
||||
36
lingtropy-client/package-lock.json
generated
36
lingtropy-client/package-lock.json
generated
@ -1,27 +1,28 @@
|
||||
{
|
||||
"name": "lingtropy",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lingtropy",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"axios": "^1.8.3",
|
||||
"electron-store": "^10.0.1",
|
||||
"lingtropy": "file:",
|
||||
"lingtropy2": "file:",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuetify": "^3.7.14",
|
||||
"vutron": "file:"
|
||||
"vuetify": "^3.7.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@types/electron-store": "^3.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.1",
|
||||
"@typescript-eslint/parser": "7.16.1",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
@ -41,7 +42,7 @@
|
||||
"playwright": "^1.50.1",
|
||||
"prettier": "^3.5.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
@ -1321,6 +1322,17 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/electron-store": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/electron-store/-/electron-store-3.2.2.tgz",
|
||||
"integrity": "sha512-N3X45mnsfnwmeZoXSZmeE7/Tne8kdbIKO1vQdbbEV04TzrMbWIeDVJJjnX2n5GH9O61zI612tet4s2jCZ55DXw==",
|
||||
"deprecated": "This is a stub types definition. electron-store provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-store": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.56.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz",
|
||||
@ -6333,6 +6345,10 @@
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/lingtropy2": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@ -8697,9 +8713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@ -9151,10 +9167,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vutron": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "lingtropy",
|
||||
"appId": "com.lingnite.lingtropy",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "文案助手",
|
||||
"homepage": "https://www.lingnite.com",
|
||||
"author": "沈阳泠启网络科技有限公司",
|
||||
@ -45,6 +45,8 @@
|
||||
"@mdi/font": "^7.4.47",
|
||||
"axios": "^1.8.3",
|
||||
"electron-store": "^10.0.1",
|
||||
"lingtropy": "file:",
|
||||
"lingtropy2": "file:",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
@ -53,6 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@types/electron-store": "^3.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.1",
|
||||
"@typescript-eslint/parser": "7.16.1",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
@ -72,7 +75,7 @@
|
||||
"playwright": "^1.50.1",
|
||||
"prettier": "^3.5.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
|
||||
@ -3,7 +3,7 @@ import Constants from './utils/Constants'
|
||||
import Store from 'electron-store'
|
||||
import axios from 'axios'
|
||||
import OpenAI from 'openai'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
|
||||
const store = new Store()
|
||||
/*
|
||||
* IPC Communications
|
||||
@ -20,6 +20,7 @@ const store = new Store()
|
||||
// }
|
||||
// }
|
||||
// const baseUrl = ""
|
||||
|
||||
export default class IPCs {
|
||||
static initialize(): void {
|
||||
// Get application version
|
||||
|
||||
@ -10,13 +10,14 @@ import { createTray, hideWindow, showWindow } from './tray.ts'
|
||||
|
||||
const options = {
|
||||
width: Constants.IS_DEV_ENV ? 1500 : 1250,
|
||||
height: 850,
|
||||
height: 900,
|
||||
tray: {
|
||||
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here
|
||||
enabled: true,
|
||||
menu: false, // true, to use a tray menu ; false to toggle visibility on click on tray icon
|
||||
trayWindow: false // true, to use a tray floating window attached to top try icon
|
||||
}
|
||||
},
|
||||
title: '文案助手'
|
||||
}
|
||||
|
||||
const exitApp = (mainWindow: BrowserWindow): void => {
|
||||
|
||||
@ -19,6 +19,7 @@ app.on('ready', async () => {
|
||||
*/
|
||||
|
||||
mainWindow = await createMainWindow()
|
||||
// mainWindow.setTile("23")
|
||||
})
|
||||
|
||||
app.on('activate', async () => {
|
||||
|
||||
@ -15,7 +15,7 @@ export interface TrayOptions {
|
||||
|
||||
export default class Constants {
|
||||
// Display app name (uppercase first letter)
|
||||
static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1)
|
||||
static APP_NAME = '文案助手' //name.charAt(0).toUpperCase() + name.slice(1)
|
||||
|
||||
static APP_VERSION = version
|
||||
|
||||
|
||||
193
lingtropy-client/src/renderer/components/DisclaimerComponent.vue
Normal file
193
lingtropy-client/src/renderer/components/DisclaimerComponent.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<!-- 免责声明弹窗 -->
|
||||
<div v-if="showDisclaimer" class="disclaimer-modal">
|
||||
<div class="disclaimer-content">
|
||||
<h2 class="disclaimer-title">免责声明</h2>
|
||||
<div class="disclaimer-text">
|
||||
<p>您正在使用本软件,请您仔细阅读以下内容并确认:</p>
|
||||
<ol>
|
||||
<li
|
||||
>根据小红书平台的相关要求,通过反复发布重复、近似交易笔记,或发布大量发布工业化、低质量创作的交易笔记等方式获取流量的行为系违规行为。</li
|
||||
>
|
||||
<li>本软件充分尊重原创者的著作权等知识产权,仅为用户提供参考素材与灵感。</li>
|
||||
<li
|
||||
>本软件郑重提醒用户:请各用户遵守知识产权相关规定,秉持“真诚分享”精神,自行对在小红书平台及其他社交媒体平台发布的内容进行判断、负责,不要直接搬运原创作品,严禁用于辅助违规后端操作引流。</li
|
||||
>
|
||||
<li>对用户违反本声明,所产生的后果,炬梦私域与泠启科技不承担任何责任。</li>
|
||||
<li>本软件使用的ai仅供内部测试,严禁生成违背各种法律法规或公序良俗的内容。</li>
|
||||
</ol>
|
||||
<p>继续使用本工具,即表示您已阅读并同意上述声明。</p>
|
||||
</div>
|
||||
<div class="disclaimer-actions">
|
||||
<label class="disclaimer-checkbox">
|
||||
<input type="checkbox" v-model="doNotShowAgain" />
|
||||
<span>不再显示</span>
|
||||
</label>
|
||||
<button @click="acceptDisclaimer" class="disclaimer-button">我已阅读并同意</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
// 免责声明状态
|
||||
const showDisclaimer = ref(false)
|
||||
const doNotShowAgain = ref(false)
|
||||
|
||||
// 检查是否需要显示免责声明
|
||||
const checkDisclaimerStatus = async () => {
|
||||
try {
|
||||
const hideDisclaimer = await window.mainApi.invoke('store-get', 'hideDisclaimer')
|
||||
showDisclaimer.value = !hideDisclaimer
|
||||
// showDisclaimer.value = true
|
||||
// console.log('显示免责声明:', showDisclaimer.value)
|
||||
} catch (err) {
|
||||
// console.error('获取免责声明状态失败:', err)
|
||||
showDisclaimer.value = true // 默认显示
|
||||
}
|
||||
}
|
||||
|
||||
// 接受免责声明
|
||||
const acceptDisclaimer = async () => {
|
||||
if (doNotShowAgain.value) {
|
||||
try {
|
||||
await window.mainApi.invoke('store-set', 'hideDisclaimer', true)
|
||||
} catch (err) {
|
||||
console.error('保存免责声明偏好失败:', err)
|
||||
}
|
||||
}
|
||||
showDisclaimer.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkDisclaimerStatus()
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.disclaimer-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 90;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.disclaimer-content {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.disclaimer-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 20px 0;
|
||||
color: #2ecc71;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.disclaimer-text {
|
||||
margin-bottom: 25px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.disclaimer-text p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.disclaimer-text ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.disclaimer-text li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.disclaimer-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.disclaimer-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disclaimer-checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.disclaimer-button {
|
||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 12px 24px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
|
||||
}
|
||||
|
||||
.disclaimer-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(46, 204, 113, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.disclaimer-content {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.disclaimer-actions {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.disclaimer-checkbox {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.disclaimer-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.disclaimer-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.disclaimer-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
lingtropy-client/src/renderer/components/FooterComponent.vue
Normal file
52
lingtropy-client/src/renderer/components/FooterComponent.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="company-footer">
|
||||
<div class="company-footer-content">
|
||||
<p>炬梦私域X泠启科技</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 组件逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.company-footer {
|
||||
position: fixed;
|
||||
margin-top: 10px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.company-footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.company-footer-content p {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.company-footer {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.company-footer-content p {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="logo-icon">AI</div>
|
||||
</div>
|
||||
<div class="header-content">
|
||||
<h1 class="title">文案生成工具</h1>
|
||||
<h1 class="title">文案助手</h1>
|
||||
<p class="subtitle">智能改写,提升文案质量</p>
|
||||
</div>
|
||||
<button @click="toggleSettings" class="settings-button">
|
||||
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="copyright"> © 2025 沈阳泠启网络科技有限公司版权所有 </div>
|
||||
<div class="copyright"> 仅供测试开发使用,严禁对外销售 </div>
|
||||
<div class="button-group">
|
||||
<button @click="toggleSettings" class="cancel-button">取消</button>
|
||||
<button @click="saveSettings" class="save-button">保存设置</button>
|
||||
|
||||
212
lingtropy-client/src/renderer/components/InputSection.vue
Normal file
212
lingtropy-client/src/renderer/components/InputSection.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="input-section">
|
||||
<HeaderComponent v-model:tokenvalue="currentKey" />
|
||||
<SelectModel v-model:modelvalue="currentModel" />
|
||||
|
||||
<div class="rewrite-mode-section">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">⚙️</span>
|
||||
改写模式
|
||||
</h2>
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
:class="{ active: mode === 'single' }"
|
||||
@click="switchMode('single')"
|
||||
class="mode-tab"
|
||||
>
|
||||
<span class="mode-icon">🔄</span>
|
||||
单次改写
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'batch' }"
|
||||
@click="switchMode('batch')"
|
||||
class="mode-tab"
|
||||
>
|
||||
<span class="mode-icon">🔀</span>
|
||||
批量改写
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="mode === 'batch'" class="generation-count">
|
||||
<label for="count-input">生成数量:</label>
|
||||
<div class="count-control">
|
||||
<button
|
||||
:disabled="generationCount <= 1"
|
||||
@click="decrementCount"
|
||||
class="count-button"
|
||||
>-</button>
|
||||
<input
|
||||
id="count-input"
|
||||
v-model="generationCount"
|
||||
@input="validateInput"
|
||||
type="number"
|
||||
class="count-input"
|
||||
/>
|
||||
<button
|
||||
:disabled="generationCount >= ARTICLE_MAX_COUNT"
|
||||
@click="incrementCount"
|
||||
class="count-button"
|
||||
>+</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="original-text-section">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">📝</span>
|
||||
原始文案
|
||||
<span v-if="originalText" class="char-count">{{ originalText.length }} 字</span>
|
||||
</h2>
|
||||
<textarea
|
||||
v-model="originalText"
|
||||
class="text-input"
|
||||
placeholder="请在此输入需要改写的文本..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button :disabled="isGenerating" @click="handleRewrite" class="rewrite-button">
|
||||
<span v-if="isGenerating" class="loading-spinner"></span>
|
||||
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ARTICLE_MAX_COUNT } from '../constants/constants'
|
||||
import HeaderComponent from './HeaderComponent.vue'
|
||||
import SelectModel from './SelectModelComponent.vue'
|
||||
|
||||
const { isGenerating } = defineProps({
|
||||
isGenerating: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['rewrite'])
|
||||
|
||||
const mode = ref('single')
|
||||
const originalText = ref('')
|
||||
const generationCount = ref(3)
|
||||
const currentModel = ref('gpt-4o')
|
||||
const currentKey = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
// 从存储中获取生成数量
|
||||
const count = await window.mainApi.invoke('store-get', 'generationCount')
|
||||
if (count) {
|
||||
generationCount.value = count
|
||||
}
|
||||
})
|
||||
|
||||
const validateInput = () => {
|
||||
if (generationCount.value < 1) {
|
||||
setGenerationCount(1)
|
||||
} else if (generationCount.value > ARTICLE_MAX_COUNT) {
|
||||
setGenerationCount(ARTICLE_MAX_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
const setGenerationCount = async (value) => {
|
||||
generationCount.value = value
|
||||
await window.mainApi.invoke('store-set', 'generationCount', value)
|
||||
}
|
||||
|
||||
const switchMode = (newMode) => {
|
||||
if (mode.value !== newMode) {
|
||||
mode.value = newMode
|
||||
}
|
||||
}
|
||||
|
||||
const incrementCount = () => {
|
||||
if (generationCount.value < ARTICLE_MAX_COUNT) {
|
||||
setGenerationCount(generationCount.value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const decrementCount = () => {
|
||||
if (generationCount.value > 1) {
|
||||
setGenerationCount(generationCount.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRewrite = () => {
|
||||
if (!originalText.value.trim()) {
|
||||
// 可以通过 emit 发送错误消息给父组件显示
|
||||
return
|
||||
}
|
||||
|
||||
emit('rewrite', {
|
||||
mode: mode.value,
|
||||
text: originalText.value,
|
||||
count: mode.value === 'single' ? 1 : generationCount.value,
|
||||
model: currentModel.value,
|
||||
key: currentKey.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-section {
|
||||
flex: 0 0 40%;
|
||||
max-width: 600px;
|
||||
min-width: 320px;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* 其他样式从 TextRewritingTool.vue 复制相关的部分 */
|
||||
.rewrite-mode-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ... 复制其他相关样式 ... */
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.input-section {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.input-section {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.generation-count {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.input-section {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,82 +1,11 @@
|
||||
<template>
|
||||
<div class="text-rewriter-container">
|
||||
<div class="text-rewriter-layout">
|
||||
<!-- 左侧输入区域 -->
|
||||
<div class="input-section">
|
||||
<HeaderComponent v-model:tokenvalue="currentKey" />
|
||||
<SelectModel v-model:modelvalue="currentModel" />
|
||||
|
||||
<div class="rewrite-mode-section">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">⚙️</span>
|
||||
改写模式
|
||||
</h2>
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
:class="{ active: mode === 'single' }"
|
||||
@click="switchMode('single')"
|
||||
class="mode-tab"
|
||||
>
|
||||
<span class="mode-icon">🔄</span>
|
||||
单次改写
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'batch' }"
|
||||
@click="switchMode('batch')"
|
||||
class="mode-tab"
|
||||
>
|
||||
<span class="mode-icon">🔀</span>
|
||||
批量改写
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="mode === 'batch'" class="generation-count">
|
||||
<label for="count-input">生成数量:</label>
|
||||
<div class="count-control">
|
||||
<button
|
||||
:disabled="generationCount <= 1"
|
||||
@click="decrementCount"
|
||||
class="count-button"
|
||||
>-</button
|
||||
>
|
||||
<input
|
||||
id="count-input"
|
||||
v-model="generationCount"
|
||||
@input="validateInput"
|
||||
type="number"
|
||||
class="count-input"
|
||||
/>
|
||||
<button
|
||||
:disabled="generationCount >= ARTICLE_MAX_COUNT"
|
||||
@click="incrementCount"
|
||||
class="count-button"
|
||||
>+</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="original-text-section">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">📝</span>
|
||||
原始文案
|
||||
<span v-if="originalText" class="char-count">{{ originalText.length }} 字</span>
|
||||
</h2>
|
||||
<textarea
|
||||
v-model="originalText"
|
||||
class="text-input"
|
||||
placeholder="请在此输入需要改写的文本..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button :disabled="isGenerating" @click="rewriteText" class="rewrite-button">
|
||||
<span v-if="isGenerating" class="loading-spinner"></span>
|
||||
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DisclaimerComponent />
|
||||
<InputSection
|
||||
:is-generating="isGenerating"
|
||||
@rewrite="handleRewrite"
|
||||
/>
|
||||
<!-- 右侧结果区域 -->
|
||||
<transition name="fade">
|
||||
<div v-if="rewrittenText.length > 0 || isGenerating" class="result-section">
|
||||
@ -104,7 +33,6 @@
|
||||
</div>
|
||||
<span class="model-badge">{{ currentModel }}</span>
|
||||
<span class="count-badge">{{ rewrittenText.length }} 个结果</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -160,58 +88,23 @@
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<FooterComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ARTICLE_MAX_COUNT } from '../constants/constants'
|
||||
import HeaderComponent from '../components/HeaderComponent.vue'
|
||||
import SelectModel from '../components/SelectModelComponent.vue'
|
||||
import DisclaimerComponent from '../components/DisclaimerComponent.vue'
|
||||
import InputSection from '../components/InputSection.vue'
|
||||
import FooterComponent from '../components/FooterComponent.vue'
|
||||
|
||||
// 基础状态
|
||||
const mode = ref('single')
|
||||
const originalText = ref('')
|
||||
const rewrittenText = ref([])
|
||||
const generationCount = ref(3)
|
||||
const expandedResults = reactive({})
|
||||
const isGenerating = ref(false)
|
||||
const copiedIndex = ref(null)
|
||||
|
||||
const baseUrl = ref('https://www.apimini2.org/v1/')
|
||||
const currentModel = ref('gpt-4o')
|
||||
const currentKey = ref('')
|
||||
|
||||
const layoutMode = ref('list')
|
||||
// 生成数量限制
|
||||
const validateInput = () => {
|
||||
if (generationCount.value < 1) {
|
||||
setGenerationCount(1)
|
||||
} else if (generationCount.value > ARTICLE_MAX_COUNT) {
|
||||
setGenerationCount(ARTICLE_MAX_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
const setGenerationCount = async (value) => {
|
||||
generationCount.value = value
|
||||
|
||||
await window.mainApi.invoke('store-set', 'generationCount', value)
|
||||
}
|
||||
|
||||
// 新增:切换布局模式
|
||||
const toggleLayout = (newLayout) => {
|
||||
layoutMode.value = newLayout
|
||||
// 可选:保存用户偏好
|
||||
window.mainApi.invoke('store-set', 'layoutMode', newLayout).catch((err) => {
|
||||
console.error('保存布局偏好失败:', err)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const count = await window.mainApi.invoke('store-get', 'generationCount')
|
||||
generationCount.value = count || 3
|
||||
console.log('generationCount', generationCount.value)
|
||||
// 新增:获取保存的布局偏好
|
||||
try {
|
||||
const savedLayout = await window.mainApi.invoke('store-get', 'layoutMode')
|
||||
if (savedLayout) {
|
||||
@ -220,91 +113,48 @@ onMounted(async () => {
|
||||
} catch (err) {
|
||||
console.error('获取布局偏好失败:', err)
|
||||
}
|
||||
|
||||
baseUrl.value = await window.mainApi.invoke('getBaseUrl')
|
||||
console.log('baseUrl', baseUrl.value)
|
||||
// currentKey.value =
|
||||
// console.log("currentKey", currentKey.value)
|
||||
})
|
||||
|
||||
// 其他方法
|
||||
const switchMode = (newMode) => {
|
||||
if (mode.value !== newMode) {
|
||||
mode.value = newMode
|
||||
}
|
||||
}
|
||||
|
||||
const incrementCount = () => {
|
||||
if (generationCount.value < ARTICLE_MAX_COUNT) {
|
||||
// generationCount.value++
|
||||
setGenerationCount(generationCount.value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const decrementCount = () => {
|
||||
if (generationCount.value > 1) {
|
||||
setGenerationCount(generationCount.value - 1)
|
||||
// generationCount.value--
|
||||
}
|
||||
}
|
||||
function callOpenAI(apiKey, model, count, rawArticle) {
|
||||
window.mainApi.send('call-openai', baseUrl, apiKey, model, count, rawArticle)
|
||||
}
|
||||
const rewriteText = () => {
|
||||
if (!originalText.value.trim()) {
|
||||
showToast('请输入需要改写的文本')
|
||||
return
|
||||
}
|
||||
|
||||
const handleRewrite = async ({ mode, text, count, model, key }) => {
|
||||
// 清空之前的改写结果
|
||||
Object.keys(expandedResults).forEach((key) => {
|
||||
delete expandedResults[key]
|
||||
})
|
||||
console.log(`userToekn:${currentKey.value}`)
|
||||
|
||||
isGenerating.value = true
|
||||
rewrittenText.value = []
|
||||
|
||||
// 调用 OpenAI API
|
||||
callOpenAI(
|
||||
currentKey.value, // 替换为你的 API 密钥
|
||||
currentModel.value, // 使用的模型
|
||||
mode.value === 'single' ? 1 : generationCount.value, // 请求次数
|
||||
originalText.value // 原始文本
|
||||
)
|
||||
try {
|
||||
// 获取 baseUrl
|
||||
const baseUrl = await window.mainApi.invoke('getBaseUrl')
|
||||
|
||||
// 监听部分结果
|
||||
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
|
||||
if (!rewrittenText.value[index]) {
|
||||
rewrittenText.value[index] = ''
|
||||
}
|
||||
rewrittenText.value[index] += content
|
||||
})
|
||||
|
||||
// 监听部分结果
|
||||
window.mainApi.on('openai-partial-response', (_, { index, content }) => {
|
||||
// console.log(`请求 ${index + 1} 的部分结果:`, content);
|
||||
// 将部分结果渲染到页面
|
||||
if (!rewrittenText.value[index]) {
|
||||
rewrittenText.value[index] = ''
|
||||
}
|
||||
rewrittenText.value[index] += content
|
||||
})
|
||||
|
||||
// 监听最终结果
|
||||
window.mainApi
|
||||
.invoke(
|
||||
// 调用 API
|
||||
const results = await window.mainApi.invoke(
|
||||
'call-openai',
|
||||
baseUrl.value,
|
||||
currentKey.value,
|
||||
currentModel.value,
|
||||
mode.value === 'single' ? 1 : generationCount.value,
|
||||
originalText.value
|
||||
baseUrl, // 使用获取到的 baseUrl
|
||||
key,
|
||||
model,
|
||||
count,
|
||||
text
|
||||
)
|
||||
.then((results) => {
|
||||
console.log('所有请求的最终结果:', results)
|
||||
// 将最终结果渲染到页面
|
||||
results.forEach(({ index, response }) => {
|
||||
rewrittenText.value[index] = response
|
||||
})
|
||||
isGenerating.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('调用 API 失败:', error)
|
||||
showToast('调用 API 失败,请稍后重试')
|
||||
isGenerating.value = false
|
||||
|
||||
results.forEach(({ index, response }) => {
|
||||
rewrittenText.value[index] = response
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调用 API 失败:', error)
|
||||
showToast('调用 API 失败,请稍后重试')
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyText = (text, index) => {
|
||||
@ -342,6 +192,15 @@ const showToast = (message) => {
|
||||
}, 2000)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
// 新增:切换布局模式
|
||||
const toggleLayout = (newLayout) => {
|
||||
layoutMode.value = newLayout
|
||||
// 可选:保存用户偏好
|
||||
window.mainApi.invoke('store-set', 'layoutMode', newLayout).catch((err) => {
|
||||
console.error('保存布局偏好失败:', err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -354,7 +213,8 @@ const showToast = (message) => {
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
/* background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); */
|
||||
background: linear-gradient(135deg, #f0f4fd 0%, #d4e0f7 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -434,26 +294,11 @@ const showToast = (message) => {
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
max-width: 1800px;
|
||||
height: calc(100vh - 40px);
|
||||
height: calc(100vh - 80px); /* Reduced height to make room for footer */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 左侧输入区域 */
|
||||
.input-section {
|
||||
flex: 0 0 40%;
|
||||
max-width: 600px;
|
||||
min-width: 320px;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 10px; /* Add margin to avoid content being hidden by footer */
|
||||
/* padding-bottom: 10px; */
|
||||
}
|
||||
|
||||
/* 右侧结果区域 */
|
||||
@ -754,6 +599,12 @@ const showToast = (message) => {
|
||||
/* 新增:网格布局中的项目样式 */
|
||||
.result-block.grid-item {
|
||||
height: 250px;
|
||||
transition: height 0.5s ease;
|
||||
}
|
||||
|
||||
.result-block.grid-item.expanded-result {
|
||||
height: auto;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.result-block:hover {
|
||||
@ -1030,12 +881,6 @@ const showToast = (message) => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.result-section,
|
||||
.empty-result-section {
|
||||
width: 100%;
|
||||
@ -1052,16 +897,10 @@ const showToast = (message) => {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 网格布局在平板上调整 */
|
||||
.results-list.grid-layout {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.text-rewriter-layout {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input-section,
|
||||
.result-section,
|
||||
.empty-result-section {
|
||||
padding: 20px;
|
||||
@ -1077,17 +916,6 @@ const showToast = (message) => {
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* .header-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
} */
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@ -1095,7 +923,6 @@ const showToast = (message) => {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.input-section,
|
||||
.result-section,
|
||||
.empty-result-section {
|
||||
padding: 16px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user