Compare commits

...

2 Commits

Author SHA1 Message Date
9336098cd9 Merge branch 'main' of https://git.devillusion.asia/mcallzbl/LingTropy 2025-03-20 23:05:32 +08:00
674c45324b 初版 2025-03-20 22:57:52 +08:00
20 changed files with 2229 additions and 5152 deletions

View File

@ -19,11 +19,12 @@
}, },
"plugins": ["vue"], "plugins": ["vue"],
"rules": { "rules": {
"vue/attributes-order": 0,
"arrow-parens": 0, "arrow-parens": 0,
"generator-star-spacing": 0, "generator-star-spacing": 0,
"no-case-declarations": 0, "no-case-declarations": 0,
"array-callback-return": 0, "array-callback-return": 0,
"no-trailing-spaces": 1, "no-trailing-spaces": 0,
"no-control-regex": 0, "no-control-regex": 0,
"no-useless-constructor": 0, "no-useless-constructor": 0,
"node/no-deprecated-api": 0 "node/no-deprecated-api": 0

View File

@ -101,6 +101,7 @@ baseConfig.files = [
(Required) The files and folders listed below should not be included in the build. (Required) The files and folders listed below should not be included in the build.
*/ */
'dist/**/*', 'dist/**/*',
'!node_modules/**/*',
'!dist/main/index.dev.js', '!dist/main/index.dev.js',
'!docs/**/*', '!docs/**/*',
'!tests/**/*', '!tests/**/*',

File diff suppressed because it is too large Load Diff

View File

@ -43,13 +43,13 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"lingtropy": "file:", "axios": "^1.8.3",
"electron-store": "^10.0.1",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.1", "vue-i18n": "^11.1.1",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuetify": "^3.7.14", "vuetify": "^3.7.14"
"vutron": "file:"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.50.1", "@playwright/test": "^1.50.1",

View File

@ -1,9 +1,25 @@
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron' import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
import Constants from './utils/Constants' 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 * IPC Communications
* */ * */
// const getBaseUrl = async () => {
// try {
// const response = await axios.get('https://aqq-jbjsjuxivc.cn-hangzhou.fcapp.run');
// console.log('获取baseUrl成功:', response.data);
// return response.data.data;
// } catch (error) {
// console.error('获取baseUrl失败:', error);
// throw error;
// }
// }
// const baseUrl = ""
export default class IPCs { export default class IPCs {
static initialize(): void { static initialize(): void {
// Get application version // Get application version
@ -11,6 +27,104 @@ export default class IPCs {
return Constants.APP_VERSION return Constants.APP_VERSION
}) })
ipcMain.handle('store-get', (event, key) => {
return store.get(key) || null
})
ipcMain.handle('store-set', (event, key, value) => {
store.set(key, value)
})
ipcMain.handle('fetch-models', async () => {
try {
const response = await axios.get('https://get-model-list-vcwjgnvcld.cn-hangzhou.fcapp.run') // 从网络获取数据
console.log('获取模型数据成功:', response.data)
return response.data.data
} catch (error) {
console.error('获取模型数据失败:', error)
throw error
}
})
ipcMain.handle('getBaseUrl', async () => {
try {
const response = await axios.get('https://aqq-jbjsjuxivc.cn-hangzhou.fcapp.run')
console.log('获取baseUrl成功:', response.data)
return response.data.data
} catch (error) {
console.error('获取baseUrl失败:', error)
throw error
}
})
// const requestParams = {
// input: '这是一个测试输入', // 输入文本
// model: 'gpt-4o', // 设置模型
// stream: false, // 是否使用流传输
// temperature: 0.8, // 设置温度
// // 其他可选参数
// max_output_tokens: 100, // 最大输出令牌数
// top_p: 0.9, // 核采样参数
// };
ipcMain.handle('call-openai', async (event, baseURL, apiKey, model, count, rawArticle) => {
try {
const client = new OpenAI({
apiKey: apiKey, // 从环境变量中获取API密钥
baseURL: baseURL // 设置代理地址
})
// 创建 count 个请求
const requests = Array.from({ length: count }, (_, index) => {
return client.chat.completions
.create({
model: model,
messages: [
{
role: 'system',
content:
'你是一个小红书文案写手,能够熟练地根据用户的输入,改写成内容相近,但表达方式不同的新文案。你的文案中需要具备吸人眼球的钩子,能够牢牢抓住用户的注意力。请直接输出新的文案,不要输出其他任何提示性词语, 以纯文本的形式输出'
},
{ role: 'user', content: rawArticle }
],
stream: true // 启用流式输出
})
.then(async (stream) => {
let fullResponse = ''
for await (const chunk of stream) {
const content = chunk.choices[0].delta.content || ''
fullResponse += content
// 实时发送每个请求的部分结果
event.sender.send('openai-partial-response', { index, content })
}
// 返回最终结果
return { index, response: fullResponse }
})
})
// 并发执行所有请求
const results = await Promise.all(requests)
// 返回所有请求的最终结果
return results
} catch (error) {
console.error('Error:', error)
throw error // 将错误传递给调用者
}
})
ipcMain.handle('get-api-key', () => {
return store.get('api-key') || null
})
ipcMain.handle('set-api-key', (_, apiKey) => {
if (!apiKey) {
store.delete('api-key')
} else {
store.set('api-key', apiKey)
}
})
// Open url via web browser // Open url via web browser
ipcMain.on('msgOpenExternalLink', async (event: IpcMainEvent, url: string) => { ipcMain.on('msgOpenExternalLink', async (event: IpcMainEvent, url: string) => {
await shell.openExternal(url) await shell.openExternal(url)

View File

@ -5,12 +5,12 @@ import {
BrowserWindowConstructorOptions BrowserWindowConstructorOptions
} from 'electron' } from 'electron'
import Constants, { TrayOptions } from './utils/Constants' import Constants, { TrayOptions } from './utils/Constants'
import IPCs from './IPCs' import IPCs from './IPCs.js'
import { createTray, hideWindow, showWindow } from './tray.ts' import { createTray, hideWindow, showWindow } from './tray.ts'
const options = { const options = {
width: Constants.IS_DEV_ENV ? 1500 : 1200, width: Constants.IS_DEV_ENV ? 1500 : 1250,
height: 650, height: 850,
tray: { tray: {
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here // all optional values from DEFAULT_TRAY_OPTIONS can de defined here
enabled: true, enabled: true,

View File

@ -1,8 +1,20 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron' import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'
// Whitelist of valid channels used for IPC communication (Send message from Renderer to Main) // Whitelist of valid channels used for IPC communication (Send message from Renderer to Main)
const mainAvailChannels: string[] = ['msgRequestGetVersion', 'msgOpenExternalLink', 'msgOpenFile'] const mainAvailChannels: string[] = [
const rendererAvailChannels: string[] = [] 'msgRequestGetVersion',
'msgOpenExternalLink',
'msgOpenFile',
'store-set',
'store-get',
'fetch-models',
'getBaseUrl',
'call-openai',
'openai-partial-response',
'get-api-key',
'set-api-key'
]
const rendererAvailChannels: string[] = ['openai-partial-response', 'get-api-key', 'set-api-key']
contextBridge.exposeInMainWorld('mainApi', { contextBridge.exposeInMainWorld('mainApi', {
send: (channel: string, ...data: any[]): void => { send: (channel: string, ...data: any[]): void => {

View File

@ -0,0 +1,492 @@
<template>
<!-- Token Setting Alert -->
<div v-if="showTokenAlert" class="token-alert">
<div class="alert-content">
<span>请设置用户令牌以继续使用</span>
<button @click="toggleSettings" class="set-token-btn">设置令牌</button>
</div>
</div>
<div class="header-section">
<div class="logo-container">
<div class="logo-icon">AI</div>
</div>
<div class="header-content">
<h1 class="title">文案生成工具</h1>
<p class="subtitle">智能改写提升文案质量</p>
</div>
<button @click="toggleSettings" class="settings-button">
<span class="settings-icon"></span>
</button>
</div>
<!-- 设置弹窗 -->
<div v-if="showSettings" class="modal-overlay">
<div class="settings-modal">
<div class="modal-header">
<h2>设置</h2>
<button @click="toggleSettings" class="close-button">×</button>
</div>
<div class="modal-content">
<div class="settings-section">
<h3>用户令牌</h3>
<div class="setting-item token-input-container">
<div class="password-input-wrapper">
<input
v-model="inputToken"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入您的用户令牌"
class="token-input"
/>
<button @click="togglePasswordVisibility" type="button" class="password-toggle-btn">
{{ showPassword ? '隐藏' : '显示' }}
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="copyright"> © 2025 沈阳泠启网络科技有限公司版权所有 </div>
<div class="button-group">
<button @click="toggleSettings" class="cancel-button">取消</button>
<button @click="saveSettings" class="save-button">保存设置</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, defineProps, watch } from 'vue'
const props = defineProps({
tokenvalue: {
type: String,
required: true
}
})
//
const inputToken = ref(props.tokenvalue)
const userToken = ref(props.tokenvalue)
const showTokenAlert = ref(false)
const showPassword = ref(false)
//
const showSettings = ref(false)
const emit = defineEmits(['update:tokenvalue'])
watch(userToken, (newValue) => {
emit('update:tokenvalue', newValue)
})
//
onMounted(async () => {
//
const savedToken = await window.mainApi.invoke('get-api-key')
if (savedToken) {
userToken.value = savedToken
} else {
//
showTokenAlert.value = true
}
})
//
const toggleSettings = () => {
showSettings.value = !showSettings.value
inputToken.value = userToken.value
showPassword.value = false //
//
if (showSettings.value) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
//
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value
}
const saveSettings = () => {
//
userToken.value = inputToken.value
window.mainApi.invoke('set-api-key', userToken.value)
//
if (userToken.value.trim()) {
showTokenAlert.value = false
} else {
showTokenAlert.value = true
}
showSettings.value = false
document.body.style.overflow = '' //
}
</script>
<style scoped>
/* 令牌相关样式 */
.token-alert {
width: 100%;
background-color: #ffe5e5;
border-radius: 8px;
padding: 10px 16px;
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
.alert-content {
display: flex;
justify-content: space-between;
align-items: center;
color: #d32f2f;
}
.set-token-btn {
background-color: #d32f2f;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
margin-left: 10px;
}
.set-token-btn:hover {
background-color: #b71c1c;
}
.token-input-container {
width: 100%;
}
/* 密码输入框样式 */
.password-input-wrapper {
position: relative;
width: 100%;
display: flex;
}
.token-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
color: #333;
padding-right: 70px; /* 为按钮留出空间 */
}
.token-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 2px rgba(46, 204, 113, 0.2);
}
.password-toggle-btn {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #666;
font-size: 12px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
background-color: #f5f5f5;
transition: all 0.2s;
white-space: nowrap;
}
.password-toggle-btn:hover {
background-color: #e0e0e0;
color: #333;
}
/* 公司版权信息 */
.copyright {
color: #666;
font-size: 14px;
margin-right: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* 以下保留原有样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
/* 确保模态框不可点击穿透 */
pointer-events: all;
}
.settings-modal {
background-color: white;
border-radius: 16px;
width: 95%;
max-width: 550px; /* 增加最大宽度 */
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
/* 确保模态框内容可以点击 */
pointer-events: auto;
position: relative;
z-index: 1001;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
transition: color 0.2s;
padding: 0 10px;
}
.close-button:hover {
color: #333;
}
.modal-content {
padding: 20px;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h3 {
font-size: 18px;
margin: 0 0 16px 0;
color: #333;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.setting-item {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
color: #333;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
flex-wrap: wrap;
gap: 10px;
}
.button-group {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.cancel-button,
.save-button {
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
min-width: 80px;
}
.cancel-button {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
color: #666;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.save-button {
background-color: #2ecc71;
border: none;
color: white;
}
.save-button:hover {
background-color: #27ae60;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Header 样式 */
.header-section {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
position: relative;
}
.header-content {
flex: 1;
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #2ecc71, #27ae60);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 20px;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
/* 设置按钮 */
.settings-button {
width: 40px;
height: 40px;
border-radius: 10px;
background-color: #f5f5f5;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.settings-button:hover {
background-color: #e0e0e0;
transform: rotate(30deg);
}
.settings-icon {
font-size: 20px;
}
@media (max-width: 768px) {
.header-section {
flex-direction: column;
text-align: center;
}
.settings-button {
position: absolute;
top: 0;
right: 0;
}
.alert-content {
flex-direction: column;
gap: 10px;
}
.modal-footer {
flex-direction: column;
gap: 16px;
}
.copyright {
margin-right: 0;
text-align: center;
max-width: 100%;
}
.button-group {
width: 100%;
justify-content: center;
}
.cancel-button,
.save-button {
flex: 1;
text-align: center;
}
}
@media (max-width: 480px) {
.settings-modal {
width: 95%;
max-width: none;
border-radius: 12px;
}
.modal-content {
padding: 15px;
}
.modal-header,
.modal-footer {
padding: 15px;
}
}
</style>

View File

@ -1,342 +0,0 @@
<template>
<div class="input-section">
<div class="header-section">
<div class="logo-container">
<div class="logo-icon">AI</div>
</div>
<div class="header-content">
<h1 class="title">文案生成工具</h1>
<p class="subtitle">智能改写提升文案质量</p>
</div>
<button @click="$emit('toggle-settings')" class="settings-button">
<span class="settings-icon"></span>
</button>
</div>
<ModelSelector
:selectedModel="selectedModel"
:availableModels="availableModels"
@update:selectedModel="$emit('update:selectedModel', $event)"
/>
<RewriteMode
:mode="mode"
:generationCount="generationCount"
@update:mode="$emit('update:mode', $event)"
@update:generationCount="$emit('update:generationCount', $event)"
/>
<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
:value="originalText"
@input="$emit('update:originalText', $event.target.value)"
class="text-input"
placeholder="请在此输入需要改写的文本..."
></textarea>
</div>
<button
:disabled="isGenerating"
@click="$emit('rewrite')"
class="rewrite-button"
>
<span v-if="isGenerating" class="loading-spinner"></span>
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
</button>
</div>
</template>
<script setup>
import ModelSelector from './ModelSelector.vue'
import RewriteMode from './RewriteMode.vue'
defineProps({
mode: String,
originalText: String,
generationCount: Number,
selectedModel: String,
availableModels: Array,
isGenerating: Boolean
})
defineEmits([
'update:mode',
'update:originalText',
'update:generationCount',
'update:selectedModel',
'rewrite',
'toggle-settings'
])
</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.9);
}
:deep(.dark-mode) .input-section {
background-color: rgba(30, 30, 46, 0.9);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
/* Logo 样式 */
.header-section {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
position: relative;
}
.header-content {
flex: 1;
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #2ecc71, #27ae60);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 20px;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
/* 设置按钮 */
.settings-button {
width: 40px;
height: 40px;
border-radius: 10px;
background-color: #f5f5f5;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.settings-button:hover {
background-color: #e0e0e0;
transform: rotate(30deg);
}
.settings-icon {
font-size: 20px;
}
:deep(.dark-mode) .settings-button {
background-color: #2d2d3a;
color: #f0f0f0;
}
:deep(.dark-mode) .settings-button:hover {
background-color: #3d3d4a;
}
/* 标题样式 */
.title {
font-size: 24px;
font-weight: 700;
margin: 0 0 4px 0;
background: linear-gradient(90deg, #2ecc71, #27ae60);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.subtitle {
color: #666;
margin: 0;
font-size: 14px;
}
:deep(.dark-mode) .subtitle {
color: #aaa;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
:deep(.dark-mode) .section-title {
color: #f0f0f0;
}
.section-icon {
font-size: 20px;
}
/* 字数统计 */
.char-count {
font-size: 14px;
color: #666;
font-weight: normal;
margin-left: auto;
background-color: #f0f0f0;
padding: 2px 8px;
border-radius: 12px;
}
:deep(.dark-mode) .char-count {
background-color: #2d2d3a;
color: #aaa;
}
/* 输入区域样式 */
.original-text-section {
margin-bottom: 24px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.text-input {
border: 1px solid #e0e0e0;
border-radius: 10px;
font-family: inherit;
font-size: 16px;
padding: 16px;
resize: none;
width: 100%;
flex-grow: 1;
min-height: 150px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: #f9f9f9;
}
.text-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
background-color: white;
}
:deep(.dark-mode) .text-input {
background-color: #2d2d3a;
border-color: #3d3d4a;
color: #f0f0f0;
}
:deep(.dark-mode) .text-input:focus {
border-color: #2ecc71;
background-color: #252533;
}
/* 按钮样式 */
.rewrite-button {
background: linear-gradient(135deg, #2ecc71, #27ae60);
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 600;
margin-top: 16px;
padding: 16px;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
.rewrite-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(46, 204, 113, 0.4);
}
.rewrite-button:active:not(:disabled) {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(46, 204, 113, 0.3);
}
.rewrite-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 1200px) {
.input-section {
flex: none;
width: 100%;
max-width: 100%;
}
}
@media (max-width: 768px) {
.input-section {
padding: 20px;
border-radius: 12px;
}
.header-section {
flex-direction: column;
text-align: center;
}
.settings-button {
position: absolute;
top: 0;
right: 0;
}
}
@media (max-width: 480px) {
.input-section {
padding: 16px;
}
}
</style>

View File

@ -1,309 +0,0 @@
<template>
<div class="result-section">
<div class="result-header-bar">
<h2 class="section-title">
<span class="section-icon"></span>
改写结果
</h2>
<div class="result-actions">
<div class="display-mode-toggle">
<button
:class="{ active: displayMode === 'list' }"
@click="$emit('change-display-mode', 'list')"
class="display-mode-button"
title="列表视图"
>
<span class="mode-icon">📃</span>
</button>
<button
:class="{ active: displayMode === 'grid' }"
@click="$emit('change-display-mode', 'grid')"
class="display-mode-button"
title="网格视图"
>
<span class="mode-icon">📊</span>
</button>
</div>
<div class="result-stats">
<span class="model-badge">{{ selectedModel }}</span>
<span class="count-badge">{{ rewrittenText.length }} 个结果</span>
</div>
</div>
</div>
<div v-if="isGenerating && rewrittenText.length === 0" class="generating-placeholder">
<div class="generating-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<p>正在使用 {{ selectedModel }} 生成改写结果...</p>
</div>
<div v-else class="results-container">
<div :class="['results-wrapper', `display-${displayMode}`]">
<ResultItem
v-for="(text, index) in rewrittenText"
:key="index"
:text="text"
:index="index"
:isExpanded="expandedResults[index]"
:isCopied="copiedIndex === index"
@toggle-expand="$emit('toggle-expand', index)"
@copy-text="$emit('copy-text', text, index)"
/>
</div>
</div>
</div>
</template>
<script setup>
import ResultItem from './ResultItem.vue'
defineProps({
rewrittenText: Array,
selectedModel: String,
isGenerating: Boolean,
expandedResults: Object,
copiedIndex: Number,
displayMode: String
})
defineEmits(['toggle-expand', 'copy-text', 'change-display-mode'])
</script>
<style scoped>
.result-section {
flex: 1;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 28px;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
}
:deep(.dark-mode) .result-section {
background-color: rgba(30, 30, 46, 0.9);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.result-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.dark-mode) .result-header-bar {
border-bottom-color: #3d3d4a;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin: 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
:deep(.dark-mode) .section-title {
color: #f0f0f0;
}
.section-icon {
font-size: 20px;
}
.result-actions {
display: flex;
align-items: center;
gap: 16px;
}
.display-mode-toggle {
display: flex;
background-color: #f0f0f0;
border-radius: 8px;
padding: 2px;
}
:deep(.dark-mode) .display-mode-toggle {
background-color: #2d2d3a;
}
.display-mode-button {
background: none;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.display-mode-button.active {
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
:deep(.dark-mode) .display-mode-button.active {
background-color: #3d3d4a;
}
.mode-icon {
font-size: 16px;
}
.result-stats {
display: flex;
gap: 12px;
}
.model-badge, .count-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.model-badge {
background-color: #e8f5e9;
color: #2e7d32;
}
:deep(.dark-mode) .model-badge {
background-color: #2e7d32;
color: #e8f5e9;
}
.count-badge {
background-color: #e3f2fd;
color: #1565c0;
}
:deep(.dark-mode) .count-badge {
background-color: #1565c0;
color: #e3f2fd;
}
/* 结果区域样式 */
.results-container {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.results-wrapper {
padding: 4px;
}
.display-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.display-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
/* 生成中占位符 */
.generating-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
color: #666;
}
:deep(.dark-mode) .generating-placeholder {
color: #aaa;
}
.generating-placeholder p {
margin-top: 20px;
font-size: 16px;
}
.generating-animation {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.dot {
width: 12px;
height: 12px;
background-color: #2ecc71;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
@media (max-width: 1200px) {
.result-section {
width: 100%;
}
}
@media (max-width: 768px) {
.result-section {
padding: 20px;
border-radius: 12px;
}
.result-header-bar {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.result-actions {
width: 100%;
justify-content: space-between;
}
.display-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.result-section {
padding: 16px;
}
.result-stats {
flex-direction: column;
gap: 8px;
}
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<div class="model-selection-section">
<h2 class="section-title">
<span class="section-icon">🤖</span>
AI 模型选择
</h2>
<div class="model-input-group">
<div ref="modelDropdown" @click="toggleModelList" class="model-dropdown">
<input
v-model="selectedModel"
type="text"
class="model-input"
placeholder="选择或输入模型名称"
@input="onModelInput"
/>
<span :class="{ 'arrow-up': showModelList }" class="dropdown-arrow"></span>
<!-- 模型列表下拉框 -->
<div v-show="showModelList" class="model-list">
<div
v-for="model in availableModels"
:key="model"
:class="{ active: model === selectedModel }"
@click="selectModel(model)"
class="model-item"
>
<span class="model-name">{{ model }}</span>
<!-- <span class="model-description">{{ model.description }}</span> -->
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
const props = defineProps({
modelvalue: {
type: String,
required: true
}
})
// AI
const availableModels = ref([])
const loading = ref(true) //
const error = ref(false)
//
const selectedModel = ref(props.modelvalue)
const showModelList = ref(false)
const modelDropdown = ref(null)
// const filteredModels = computed(() => {
// if (!selectedModel.value) return availableModels
// const searchTerm = selectedModel.value.toLowerCase()
// return availableModels.value.filter((model) => model.toLowerCase().includes(searchTerm))
// })
const emit = defineEmits(['update:modelvalue'])
// const updateParent = () => {
// emit('update:modelvalue', selectModel.value);
// };
watch(selectedModel, (newValue) => {
emit('update:modelvalue', newValue)
})
//
const toggleModelList = () => {
showModelList.value = !showModelList.value
}
//
const selectModel = (model) => {
selectedModel.value = model
showModelList.value = false
const saveSelectedModel = async () =>
await window.mainApi.invoke('store-set', 'model', selectedModel.value)
saveSelectedModel()
// updateParent()
}
// -
// const onModelInput = () => {
// //
// }
//
const handleClickOutside = (event) => {
if (modelDropdown.value && !modelDropdown.value.contains(event.target)) {
showModelList.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
const fetchModels = async () => {
try {
selectedModel.value = await window.mainApi.invoke('store-get', 'model')
const res = await window.mainApi.invoke('fetch-models')
// console.log(':', res);
availableModels.value = res
if (availableModels.value && availableModels.value.length > 0 && !selectedModel.value) {
selectedModel.value = availableModels.value[0]
} else if (!selectedModel.value) {
selectedModel.value = 'gpt-4o'
}
// console.log('availableModels:', availableModels.value);
} catch (err) {
console.error('获取模型数据失败:', err)
error.value = true //
} finally {
loading.value = false //
}
}
fetchModels()
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
/* 模型选择样式 */
.model-selection-section {
margin-bottom: 24px;
}
.model-input-group {
position: relative;
}
.model-dropdown {
position: relative;
width: 100%;
}
.model-input {
width: 100%;
padding: 12px 16px;
padding-right: 36px;
border: 1px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
background-color: #f9f9f9;
}
.model-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
background-color: white;
}
.dropdown-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #666;
pointer-events: none;
transition: transform 0.3s ease;
}
.arrow-up {
transform: translateY(-50%) rotate(180deg);
}
.model-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 10px;
margin-top: 8px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.model-item {
padding: 14px 16px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px;
transition: all 0.2s ease;
border-bottom: 1px solid #f0f0f0;
}
.model-item:last-child {
border-bottom: none;
}
.model-item:hover {
background-color: #f5f5f5;
}
.model-item.active {
background-color: #e8f5e9;
}
.model-name {
font-weight: 600;
color: #333;
}
.model-description {
font-size: 14px;
color: #666;
}
</style>

View File

@ -1,339 +0,0 @@
<template>
<div @click="$emit('close')" class="modal-overlay">
<div @click.stop class="settings-modal">
<div class="modal-header">
<h2>设置</h2>
<button @click="$emit('close')" class="close-button">×</button>
</div>
<div class="modal-content">
<div class="settings-section">
<h3>界面设置</h3>
<div class="setting-item">
<label class="setting-label">
<input v-model="localSettings.darkMode" type="checkbox">
深色模式
</label>
</div>
<div class="setting-item">
<label class="setting-label">
<input v-model="localSettings.compactMode" type="checkbox">
紧凑视图
</label>
</div>
</div>
<div class="settings-section">
<h3>生成设置</h3>
<div class="setting-item">
<label class="setting-label">温度值</label>
<div class="slider-container">
<input
v-model="localSettings.temperature"
min="0"
max="100"
type="range"
class="slider"
>
<span class="slider-value">{{ localSettings.temperature / 100 }}</span>
</div>
</div>
<div class="setting-item">
<label class="setting-label">最大长度</label>
<select v-model="localSettings.maxLength" class="select-input">
<option value="500">500 </option>
<option value="1000">1000 </option>
<option value="2000">2000 </option>
<option value="3000">3000 </option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="$emit('close')" class="cancel-button">取消</button>
<button @click="saveSettings" class="save-button">保存设置</button>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
const props = defineProps({
settings: Object
})
const emit = defineEmits(['close', 'save'])
const localSettings = reactive({
darkMode: false,
compactMode: false,
temperature: 70,
maxLength: '2000'
})
onMounted(() => {
//
Object.assign(localSettings, props.settings)
})
const saveSettings = () => {
emit('save', localSettings)
}
</script>
<style scoped>
/* 设置弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
backdrop-filter: blur(5px);
}
.settings-modal {
background-color: white;
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
:deep(.dark-mode) .settings-modal {
background-color: #2d2d3a;
color: #f0f0f0;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.dark-mode) .modal-header {
border-bottom-color: #3d3d4a;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
color: #333;
}
:deep(.dark-mode) .modal-header h2 {
color: #f0f0f0;
}
.close-button {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
transition: color 0.2s;
}
.close-button:hover {
color: #333;
}
:deep(.dark-mode) .close-button {
color: #aaa;
}
:deep(.dark-mode) .close-button:hover {
color: #f0f0f0;
}
.modal-content {
padding: 20px;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h3 {
font-size: 18px;
margin: 0 0 16px 0;
color: #333;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.dark-mode) .settings-section h3 {
color: #f0f0f0;
border-bottom-color: #3d3d4a;
}
.setting-item {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
color: #333;
}
:deep(.dark-mode) .setting-label {
color: #f0f0f0;
}
.slider-container {
display: flex;
align- .setting-label {
color: #f0f0f0;
}
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
max-width: 200px;
}
.slider {
flex: 1;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #e0e0e0;
border-radius: 3px;
outline: none;
}
:deep(.dark-mode) .slider {
background: #3d3d4a;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #2ecc71;
cursor: pointer;
transition: all 0.2s;
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.slider-value {
min-width: 40px;
text-align: center;
font-size: 14px;
color: #666;
}
:deep(.dark-mode) .slider-value {
color: #aaa;
}
.select-input {
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background-color: white;
min-width: 120px;
}
:deep(.dark-mode) .select-input {
background-color: #252533;
border-color: #3d3d4a;
color: #f0f0f0;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
}
:deep(.dark-mode) .modal-footer {
border-top-color: #3d3d4a;
}
.cancel-button, .save-button {
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.cancel-button {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
color: #666;
}
:deep(.dark-mode) .cancel-button {
background-color: #3d3d4a;
border-color: #4d4d5a;
color: #f0f0f0;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
:deep(.dark-mode) .cancel-button:hover {
background-color: #4d4d5a;
}
.save-button {
background-color: #2ecc71;
border: none;
color: white;
}
.save-button:hover {
background-color: #27ae60;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1 @@
export const ARTICLE_MAX_COUNT = 50 // 生成文章数量的上限

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff