初版
This commit is contained in:
parent
ff5d601bc1
commit
674c45324b
@ -19,11 +19,12 @@
|
||||
},
|
||||
"plugins": ["vue"],
|
||||
"rules": {
|
||||
"vue/attributes-order": 0,
|
||||
"arrow-parens": 0,
|
||||
"generator-star-spacing": 0,
|
||||
"no-case-declarations": 0,
|
||||
"array-callback-return": 0,
|
||||
"no-trailing-spaces": 1,
|
||||
"no-trailing-spaces": 0,
|
||||
"no-control-regex": 0,
|
||||
"no-useless-constructor": 0,
|
||||
"node/no-deprecated-api": 0
|
||||
|
||||
@ -101,6 +101,7 @@ baseConfig.files = [
|
||||
(Required) The files and folders listed below should not be included in the build.
|
||||
*/
|
||||
'dist/**/*',
|
||||
'!node_modules/**/*',
|
||||
'!dist/main/index.dev.js',
|
||||
'!docs/**/*',
|
||||
'!tests/**/*',
|
||||
|
||||
3757
lingtropy-client/package-lock.json
generated
3757
lingtropy-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -43,13 +43,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"lingtropy": "file:",
|
||||
"axios": "^1.8.3",
|
||||
"electron-store": "^10.0.1",
|
||||
"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",
|
||||
|
||||
@ -1,9 +1,25 @@
|
||||
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
|
||||
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
|
||||
* */
|
||||
|
||||
// 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 {
|
||||
static initialize(): void {
|
||||
// Get application version
|
||||
@ -11,6 +27,104 @@ export default class IPCs {
|
||||
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
|
||||
ipcMain.on('msgOpenExternalLink', async (event: IpcMainEvent, url: string) => {
|
||||
await shell.openExternal(url)
|
||||
|
||||
@ -5,12 +5,12 @@ import {
|
||||
BrowserWindowConstructorOptions
|
||||
} from 'electron'
|
||||
import Constants, { TrayOptions } from './utils/Constants'
|
||||
import IPCs from './IPCs'
|
||||
import IPCs from './IPCs.js'
|
||||
import { createTray, hideWindow, showWindow } from './tray.ts'
|
||||
|
||||
const options = {
|
||||
width: Constants.IS_DEV_ENV ? 1500 : 1200,
|
||||
height: 650,
|
||||
width: Constants.IS_DEV_ENV ? 1500 : 1250,
|
||||
height: 850,
|
||||
tray: {
|
||||
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here
|
||||
enabled: true,
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'
|
||||
|
||||
// Whitelist of valid channels used for IPC communication (Send message from Renderer to Main)
|
||||
const mainAvailChannels: string[] = ['msgRequestGetVersion', 'msgOpenExternalLink', 'msgOpenFile']
|
||||
const rendererAvailChannels: string[] = []
|
||||
const mainAvailChannels: 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', {
|
||||
send: (channel: string, ...data: any[]): void => {
|
||||
|
||||
492
lingtropy-client/src/renderer/components/HeaderComponent.vue
Normal file
492
lingtropy-client/src/renderer/components/HeaderComponent.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
1
lingtropy-client/src/renderer/constants/constants.js
Normal file
1
lingtropy-client/src/renderer/constants/constants.js
Normal file
@ -0,0 +1 @@
|
||||
export const ARTICLE_MAX_COUNT = 50 // 生成文章数量的上限
|
||||
BIN
lingtropy-client/src/renderer/public/placeholder-logo.png
Normal file
BIN
lingtropy-client/src/renderer/public/placeholder-logo.png
Normal file
Binary file not shown.
@ -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 |
BIN
lingtropy-client/src/renderer/public/placeholder-user.jpg
Normal file
BIN
lingtropy-client/src/renderer/public/placeholder-user.jpg
Normal file
Binary file not shown.
BIN
lingtropy-client/src/renderer/public/placeholder.jpg
Normal file
BIN
lingtropy-client/src/renderer/public/placeholder.jpg
Normal file
Binary file not shown.
1
lingtropy-client/src/renderer/public/placeholder.svg
Normal file
1
lingtropy-client/src/renderer/public/placeholder.svg
Normal 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
Loading…
Reference in New Issue
Block a user