🔄Update: T3/前端初步构建
This commit is contained in:
Generated
+7
@@ -8,6 +8,7 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1686,6 +1687,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+78
-1
@@ -1,3 +1,80 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<h1>AI API 配置与对话工具</h1>
|
||||
<p>快速配置主流厂商API并进行对话测试</p>
|
||||
</header>
|
||||
|
||||
</template>
|
||||
<main class="app-main">
|
||||
<!-- 配置区域 -->
|
||||
<section class="config-section">
|
||||
<h2>厂商配置</h2>
|
||||
<ConfigPanel />
|
||||
</section>
|
||||
|
||||
<!-- 对话区域 -->
|
||||
<section class="chat-section">
|
||||
<h2>AI 对话</h2>
|
||||
<ChatPanel />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ConfigPanel from './components/ConfigPanel.vue'
|
||||
import ChatPanel from './components/ChatPanel.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app-header p {
|
||||
margin: 10px 0 0 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.config-section, .chat-section {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config-section h2, .chat-section h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+111
-28
@@ -1,34 +1,117 @@
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* 重置默认样式 */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
border-color: #86b7fe;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #0069d9;
|
||||
border-color: #0062cc;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
|
||||
.app-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div class="chat-panel">
|
||||
<!-- 厂商选择器 -->
|
||||
<div class="form-group">
|
||||
<label for="chat-platform">选择厂商</label>
|
||||
<select id="chat-platform" v-model="selectedPlatform" :disabled="!configuredPlatforms.length">
|
||||
<option value="">请选择厂商</option>
|
||||
<option v-for="platform in configuredPlatforms" :key="platform.id" :value="platform.id">
|
||||
{{ platform.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 对话输入框 -->
|
||||
<div class="form-group">
|
||||
<label for="user-input">输入问题</label>
|
||||
<textarea
|
||||
id="user-input"
|
||||
v-model="userInput"
|
||||
placeholder="请输入您的问题..."
|
||||
:disabled="!selectedPlatform || isSending"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<div class="form-group">
|
||||
<button @click="sendMessage" :disabled="!canSend || isSending" class="send-btn">
|
||||
{{ isSending ? '发送中...' : '发送' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 响应显示区域 -->
|
||||
<div v-if="responseMessage" class="response-area">
|
||||
<h3>AI 响应</h3>
|
||||
<div class="response-content" v-html="responseMessage"></div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息显示 -->
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="!configuredPlatforms.length" class="info-message">
|
||||
<p>请先在配置页面添加厂商配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { decrypt } from '../utils/encryption'
|
||||
|
||||
// 响应式数据
|
||||
const userInput = ref('')
|
||||
const selectedPlatform = ref('')
|
||||
const responseMessage = ref('')
|
||||
const errorMessage = ref('')
|
||||
const isSending = ref(false)
|
||||
const configuredPlatforms = ref([])
|
||||
|
||||
// 计算属性
|
||||
const canSend = computed(() => {
|
||||
return selectedPlatform.value && userInput.value.trim() !== ''
|
||||
})
|
||||
|
||||
// 从localStorage加载已配置的厂商
|
||||
const loadConfiguredPlatforms = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('ai-api-configs')
|
||||
if (stored) {
|
||||
const configs = JSON.parse(stored)
|
||||
configuredPlatforms.value = configs.map(config => {
|
||||
// 这里需要根据实际的平台配置来构建完整信息
|
||||
// 为简化实现,我们只保留必要的信息
|
||||
return {
|
||||
id: config.id,
|
||||
name: getPlatformName(config.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取平台名称
|
||||
const getPlatformName = (platformId) => {
|
||||
const platforms = [
|
||||
{ id: 'openai', name: 'OpenAI' },
|
||||
{ id: 'anthropic', name: 'Anthropic' },
|
||||
{ id: 'google', name: 'Google (Gemini)' },
|
||||
{ id: 'qwen', name: '通义千问(阿里云)' },
|
||||
{ id: 'wenxin', name: '文心一言(百度)' }
|
||||
]
|
||||
const platform = platforms.find(p => p.id === platformId)
|
||||
return platform ? platform.name : platformId
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!canSend.value) return
|
||||
|
||||
isSending.value = true
|
||||
responseMessage.value = ''
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// 获取选中的平台配置
|
||||
const platformConfig = configuredPlatforms.value.find(p => p.id === selectedPlatform.value)
|
||||
if (!platformConfig) {
|
||||
throw new Error('未找到厂商配置')
|
||||
}
|
||||
|
||||
// 从localStorage获取密钥
|
||||
const stored = localStorage.getItem('ai-api-configs')
|
||||
if (!stored) {
|
||||
throw new Error('未找到厂商配置')
|
||||
}
|
||||
|
||||
const configs = JSON.parse(stored)
|
||||
const config = configs.find(c => c.id === selectedPlatform.value)
|
||||
if (!config) {
|
||||
throw new Error('未找到厂商配置')
|
||||
}
|
||||
|
||||
// 解密密钥
|
||||
const apiKey = decrypt(config.apiKey)
|
||||
|
||||
// 调用API
|
||||
const result = await callAIPlatform(selectedPlatform.value, apiKey, config.endpoint, userInput.value)
|
||||
|
||||
// 显示响应
|
||||
responseMessage.value = result
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
errorMessage.value = error.message || '发送消息失败,请重试'
|
||||
} finally {
|
||||
isSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 调用AI平台API
|
||||
const callAIPlatform = async (platformId, apiKey, endpoint, message) => {
|
||||
// 根据不同平台调用不同的API
|
||||
switch (platformId) {
|
||||
case 'openai':
|
||||
return await callOpenAI(apiKey, endpoint, message)
|
||||
case 'anthropic':
|
||||
return await callAnthropic(apiKey, endpoint, message)
|
||||
case 'google':
|
||||
return await callGoogle(apiKey, endpoint, message)
|
||||
case 'qwen':
|
||||
return await callQwen(apiKey, endpoint, message)
|
||||
case 'wenxin':
|
||||
return await callWenxin(apiKey, endpoint, message)
|
||||
default:
|
||||
throw new Error(`不支持的厂商: ${platformId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用OpenAI API
|
||||
const callOpenAI = async (apiKey, endpoint, message) => {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [{ role: 'user', content: message }],
|
||||
temperature: 0.7
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(`OpenAI API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.choices[0]?.message?.content || '未获取到响应内容'
|
||||
}
|
||||
|
||||
// 调用Anthropic API
|
||||
const callAnthropic = async (apiKey, endpoint, message) => {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
messages: [{ role: 'user', content: message }],
|
||||
max_tokens: 1024
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(`Anthropic API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.content[0]?.text || '未获取到响应内容'
|
||||
}
|
||||
|
||||
// 调用Google Gemini API
|
||||
const callGoogle = async (apiKey, endpoint, message) => {
|
||||
const response = await fetch(`${endpoint}?key=${apiKey}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{
|
||||
parts: [
|
||||
{ text: message }
|
||||
]
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(`Google API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.candidates[0]?.content?.parts[0]?.text || '未获取到响应内容'
|
||||
}
|
||||
|
||||
// 调用通义千问API
|
||||
const callQwen = async (apiKey, endpoint, message) => {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'X-DashScope-Scene': 'chat'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'qwen-plus',
|
||||
input: {
|
||||
messages: [{ role: 'user', content: message }]
|
||||
},
|
||||
parameters: {
|
||||
result_format: 'message'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(`通义千问 API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.output?.choices[0]?.message?.content || '未获取到响应内容'
|
||||
}
|
||||
|
||||
// 调用文心一言API
|
||||
const callWenxin = async (apiKey, endpoint, message) => {
|
||||
const response = await fetch(`${endpoint}?access_token=${apiKey}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: [{ role: 'user', content: message }]
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(`文心一言 API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.result || '未获取到响应内容'
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadConfiguredPlatforms()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group select:disabled,
|
||||
.form-group textarea:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 12px 20px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.response-area {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.response-area h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 10px;
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 15px;
|
||||
background-color: #e7f3ff;
|
||||
color: #004085;
|
||||
border: 1px solid #b8daff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-message p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="config-panel">
|
||||
<!-- 厂商选择 -->
|
||||
<div class="form-group">
|
||||
<label for="platform">选择厂商</label>
|
||||
<select id="platform" v-model="selectedPlatform" @change="onPlatformChange">
|
||||
<option value="">请选择厂商</option>
|
||||
<option v-for="platform in platforms" :key="platform.id" :value="platform.id">
|
||||
{{ platform.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 密钥输入 -->
|
||||
<div class="form-group">
|
||||
<label for="api-key">API 密钥</label>
|
||||
<input
|
||||
id="api-key"
|
||||
v-model="apiKey"
|
||||
type="password"
|
||||
:placeholder="currentPlatform?.keyPlaceholder || '请输入API密钥'"
|
||||
:disabled="!selectedPlatform"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 接入点输入 -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint">接入点</label>
|
||||
<input
|
||||
id="endpoint"
|
||||
v-model="endpoint"
|
||||
type="text"
|
||||
:placeholder="currentPlatform?.endpoint || '请输入接入点'"
|
||||
:disabled="!selectedPlatform"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="form-group">
|
||||
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 验证状态 -->
|
||||
<div v-if="validationMessage" class="validation-message" :class="validationClass">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 已配置的厂商列表 -->
|
||||
<div v-if="configuredPlatforms.length > 0" class="configured-list">
|
||||
<h3>已配置的厂商</h3>
|
||||
<div class="platform-item" v-for="platform in configuredPlatforms" :key="platform.id">
|
||||
<span>{{ platform.name }}</span>
|
||||
<button @click="removeConfig(platform.id)" class="remove-btn">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { encrypt, decrypt } from '../utils/encryption'
|
||||
|
||||
// 厂商配置预设
|
||||
const platforms = [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
endpoint: 'https://api.openai.com/v1/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
endpoint: 'https://api.anthropic.com/v1/messages',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google (Gemini)',
|
||||
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
|
||||
keyPlaceholder: 'AIza...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'qwen',
|
||||
name: '通义千问(阿里云)',
|
||||
endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'wenxin',
|
||||
name: '文心一言(百度)',
|
||||
endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot',
|
||||
keyPlaceholder: 'your_api_key',
|
||||
keyType: 'api_key'
|
||||
}
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const selectedPlatform = ref('')
|
||||
const apiKey = ref('')
|
||||
const endpoint = ref('')
|
||||
const validationMessage = ref('')
|
||||
const validationClass = ref('')
|
||||
const configuredPlatforms = ref([])
|
||||
|
||||
// 计算属性
|
||||
const currentPlatform = computed(() => {
|
||||
if (!selectedPlatform.value) return null
|
||||
return platforms.find(p => p.id === selectedPlatform.value)
|
||||
})
|
||||
|
||||
const canSave = computed(() => {
|
||||
return selectedPlatform.value && apiKey.value && endpoint.value
|
||||
})
|
||||
|
||||
// 从localStorage加载已配置的厂商
|
||||
const loadConfiguredPlatforms = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('ai-api-configs')
|
||||
if (stored) {
|
||||
const configs = JSON.parse(stored)
|
||||
configuredPlatforms.value = configs.map(config => {
|
||||
const platform = platforms.find(p => p.id === config.id)
|
||||
return {
|
||||
...platform,
|
||||
...config
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = () => {
|
||||
if (!canSave.value) {
|
||||
showValidation('请填写所有必填字段', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密钥格式
|
||||
if (!validateApiKey()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 加密密钥
|
||||
const encryptedKey = encrypt(apiKey.value)
|
||||
|
||||
// 构造配置对象
|
||||
const config = {
|
||||
id: selectedPlatform.value,
|
||||
apiKey: encryptedKey,
|
||||
endpoint: endpoint.value
|
||||
}
|
||||
|
||||
// 保存到localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('ai-api-configs')
|
||||
let configs = stored ? JSON.parse(stored) : []
|
||||
|
||||
// 检查是否已存在该厂商配置
|
||||
const existingIndex = configs.findIndex(c => c.id === selectedPlatform.value)
|
||||
if (existingIndex !== -1) {
|
||||
configs[existingIndex] = config
|
||||
} else {
|
||||
configs.push(config)
|
||||
}
|
||||
|
||||
localStorage.setItem('ai-api-configs', JSON.stringify(configs))
|
||||
|
||||
// 更新已配置列表
|
||||
loadConfiguredPlatforms()
|
||||
|
||||
// 清空表单
|
||||
resetForm()
|
||||
|
||||
showValidation('配置保存成功', 'success')
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
showValidation('保存配置失败,请重试', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密钥格式
|
||||
const validateApiKey = () => {
|
||||
const key = apiKey.value.trim()
|
||||
const platform = currentPlatform.value
|
||||
|
||||
if (!platform) return true
|
||||
|
||||
// 根据厂商类型验证密钥格式
|
||||
if (platform.id === 'openai' || platform.id === 'qwen' || platform.id === 'anthropic') {
|
||||
if (!key.startsWith('sk-')) {
|
||||
showValidation('OpenAI/通义千问密钥应以 "sk-" 开头', 'error')
|
||||
return false
|
||||
}
|
||||
} else if (platform.id === 'google') {
|
||||
if (!key.startsWith('AIza')) {
|
||||
showValidation('Google密钥应以 "AIza" 开头', 'error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 显示验证消息
|
||||
const showValidation = (message, type) => {
|
||||
validationMessage.value = message
|
||||
validationClass.value = type
|
||||
setTimeout(() => {
|
||||
validationMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
selectedPlatform.value = ''
|
||||
apiKey.value = ''
|
||||
endpoint.value = ''
|
||||
}
|
||||
|
||||
// 平台变更
|
||||
const onPlatformChange = () => {
|
||||
if (selectedPlatform.value) {
|
||||
const platform = platforms.find(p => p.id === selectedPlatform.value)
|
||||
if (platform) {
|
||||
endpoint.value = platform.endpoint
|
||||
}
|
||||
} else {
|
||||
endpoint.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const removeConfig = (platformId) => {
|
||||
try {
|
||||
const stored = localStorage.getItem('ai-api-configs')
|
||||
if (stored) {
|
||||
const configs = JSON.parse(stored)
|
||||
const filtered = configs.filter(c => c.id !== platformId)
|
||||
localStorage.setItem('ai-api-configs', JSON.stringify(filtered))
|
||||
loadConfiguredPlatforms()
|
||||
showValidation('配置已删除', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
showValidation('删除配置失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadConfiguredPlatforms()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group select:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
padding: 12px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.save-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.validation-message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.validation-message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.configured-list {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.configured-list h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.platform-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 5px 10px;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
</style>
|
||||
@@ -2,5 +2,9 @@ import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
// 将CryptoJS挂载到全局,供组件使用
|
||||
const app = createApp(App)
|
||||
app.config.globalProperties.$crypto = CryptoJS
|
||||
app.mount('#app')
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// AES-256 加密/解密工具
|
||||
// 注意:这是一个简化版本,实际生产环境应使用更安全的加密库
|
||||
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
// 密钥(在实际应用中应使用更安全的方式管理)
|
||||
const SECRET_KEY = 'ai-api-tool-secret-key-2025'
|
||||
|
||||
/**
|
||||
* 加密函数
|
||||
* @param {string} text - 要加密的文本
|
||||
* @returns {string} - 加密后的文本
|
||||
*/
|
||||
export function encrypt(text) {
|
||||
try {
|
||||
return CryptoJS.AES.encrypt(text, SECRET_KEY).toString()
|
||||
} catch (error) {
|
||||
console.error('加密失败:', error)
|
||||
throw new Error('加密失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密函数
|
||||
* @param {string} encryptedText - 要解密的文本
|
||||
* @returns {string} - 解密后的文本
|
||||
*/
|
||||
export function decrypt(encryptedText) {
|
||||
try {
|
||||
const bytes = CryptoJS.AES.decrypt(encryptedText, SECRET_KEY)
|
||||
return bytes.toString(CryptoJS.enc.Utf8)
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error)
|
||||
throw new Error('解密失败')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user