From 2edf393b1507f97385bef1427ace3135ff959755 Mon Sep 17 00:00:00 2001 From: canglan Date: Wed, 6 May 2026 17:25:06 +0800 Subject: [PATCH] =?UTF-8?q?v1.0.4=E4=BF=AE=E5=A4=8D=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Views/chat.php | 112 +------------------- app/Views/config.php | 214 +------------------------------------- app/Views/login.php | 57 +---------- assets/css/style.css | 122 ++++++++++++++++++++++ assets/js/chat.js | 106 +++++++++++++++++++ assets/js/config.js | 203 ++++++++++++++++++++++++++++++++++++ assets/js/install.js | 201 ++++++++++++++++++++++++++++++++++++ assets/js/login.js | 50 +++++++++ public/install.php | 238 +++++-------------------------------------- 9 files changed, 717 insertions(+), 586 deletions(-) create mode 100644 assets/js/config.js create mode 100644 assets/js/install.js create mode 100644 assets/js/login.js diff --git a/app/Views/chat.php b/app/Views/chat.php index 9f2ad22..8be832a 100644 --- a/app/Views/chat.php +++ b/app/Views/chat.php @@ -31,15 +31,15 @@ - 思考 + 思考 - - + + 设置 @@ -56,7 +56,7 @@
- @@ -65,107 +65,3 @@
- - diff --git a/app/Views/config.php b/app/Views/config.php index 1d475c6..786f18a 100644 --- a/app/Views/config.php +++ b/app/Views/config.php @@ -1,11 +1,11 @@

- + 系统配置

- + 返回聊天
@@ -16,7 +16,7 @@
- +
@@ -25,7 +25,7 @@
-

添加自定义人格

+

添加自定义人格

@@ -48,208 +48,4 @@
- + diff --git a/app/Views/login.php b/app/Views/login.php index f521755..3ce1b1d 100644 --- a/app/Views/login.php +++ b/app/Views/login.php @@ -10,7 +10,7 @@

AI Chat

- +
@@ -20,59 +20,8 @@
- +
- + diff --git a/assets/css/style.css b/assets/css/style.css index 119d6ac..eb852e9 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -649,3 +649,125 @@ body { font-size: 22px; } } + +/* ======================================== + 工具类 + ======================================== */ +.hidden { display: none !important; } +.ml-auto { margin-left: auto; } +.mt-sm { margin-top: 12px; } +.mt-md { margin-top: 16px; } +.mt-lg { margin-top: 24px; } +.full-width { width: 100%; } +.text-muted { color: var(--text-secondary); } +.text-xs { font-size: 12px; } +.text-center { text-align: center; } +.col-flex-2 { flex: 2; } +.col-flex-1 { flex: 1; } + +/* 内联 SVG 图标对齐 */ +.icon-inline { + vertical-align: -2px; + margin-right: 4px; +} + +.icon-inline-md { + vertical-align: -4px; + margin-right: 8px; +} + +/* ======================================== + 聊天页工具栏组件 + ======================================== */ +.toolbar-think-label { + font-size: 12px; + color: var(--text-secondary); +} + +.upload-btn { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; +} + +.settings-link { + margin-left: auto; +} + +/* ======================================== + 配置页组件 + ======================================== */ +.config-add-btn { + margin-top: 12px; +} + +.config-section h3 { + margin-top: 20px; + margin-bottom: 16px; + font-size: 15px; + font-weight: 600; +} + +.config-empty { + color: var(--text-secondary); +} + +.config-provider-card { + background: var(--bg-secondary); + padding: 16px; + border-radius: var(--radius); + margin-bottom: 12px; +} + +.config-provider-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.config-provider-header strong { + font-size: 15px; +} + +.config-provider-header .toggle-switch { + margin-right: 8px; +} + +.personality-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--bg-secondary); + border-radius: var(--radius); + margin-bottom: 8px; +} + +.personality-icon { + margin-right: 4px; +} + +.personality-badge { + font-size: 12px; + margin-left: 8px; +} + +.personality-badge.preset { + color: var(--warning); +} + +.personality-badge.custom { + color: var(--success); +} + +/* 固定定位消息提示(配置页用) */ +.toast-message { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + min-width: 200px; +} diff --git a/assets/js/chat.js b/assets/js/chat.js index 63a4ee5..5ffbfbb 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -306,3 +306,109 @@ const ChatManager = { return div.innerHTML; } }; + +// ======================================== +// 聊天页面初始化 +// ======================================== + +// 切换侧边栏 +function toggleSidebar() { + document.getElementById('sidebar').classList.toggle('collapsed'); +} + +// 供应商/模型数据缓存 +let providersData = []; +let personalitiesData = []; + +// 加载供应商配置 +async function loadProviders() { + try { + const res = await api.get('/config'); + providersData = res.data.providers || []; + const select = document.getElementById('providerSelect'); + select.innerHTML = ''; + providersData.forEach((p, i) => { + if (p.enabled) { + const option = document.createElement('option'); + option.value = i; + option.textContent = p.name; + select.appendChild(option); + } + }); + // 如果只有一个供应商,自动选择 + if (providersData.filter(p => p.enabled).length === 1) { + select.value = providersData.findIndex(p => p.enabled); + onProviderChange(); + } + } catch (err) { + console.error('加载供应商配置失败:', err); + } +} + +// 供应商切换时更新模型列表 +function onProviderChange() { + const index = document.getElementById('providerSelect').value; + const modelSelect = document.getElementById('modelSelect'); + modelSelect.innerHTML = ''; + + if (index !== '' && providersData[index]) { + const provider = providersData[index]; + (provider.models || []).forEach(m => { + const option = document.createElement('option'); + option.value = m; + option.textContent = m; + modelSelect.appendChild(option); + }); + // 如果只有一个模型,自动选择 + if (provider.models && provider.models.length === 1) { + modelSelect.value = provider.models[0]; + } + // 如果有默认模型,自动选择 + if (provider.defaultModel) { + modelSelect.value = provider.defaultModel; + } + } +} + +// 加载人格列表 +async function loadPersonalities() { + try { + const res = await api.get('/personalities'); + personalitiesData = res.data || []; + const select = document.getElementById('personalitySelect'); + select.innerHTML = ''; + personalitiesData.forEach(p => { + const option = document.createElement('option'); + option.value = p.id; + option.textContent = (p.icon ? p.icon + ' ' : '') + p.name; + select.appendChild(option); + }); + } catch (err) { + console.error('加载人格列表失败:', err); + } +} + +// 页面初始化 +(async function() { + // 检查认证 + const token = Storage.getToken(); + if (!token) { + window.location.href = '/login.php'; + return; + } + + // 初始化聊天管理器 + ChatManager.init(); + + // 加载数据 + await Promise.all([ + loadProviders(), + loadPersonalities(), + SessionManager.loadSessions() + ]); + + // 如果有会话,自动选择第一个 + if (SessionManager.sessions.length > 0) { + await SessionManager.switchSession(SessionManager.sessions[0].id); + } +})(); diff --git a/assets/js/config.js b/assets/js/config.js new file mode 100644 index 0000000..84e7d35 --- /dev/null +++ b/assets/js/config.js @@ -0,0 +1,203 @@ +const ConfigPage = { + providers: [], + personalities: [], + + async init() { + // 检查认证和管理员权限 + const token = Storage.getToken(); + if (!token) { + window.location.href = '/login.php'; + return; + } + + try { + // 验证管理员权限 + const userRes = await api.get('/auth/me'); + if (userRes.data.role !== 'admin') { + window.location.href = '/chat.php'; + return; + } + } catch (err) { + window.location.href = '/login.php'; + return; + } + + await this.loadProviders(); + await this.loadPersonalities(); + }, + + async loadProviders() { + try { + const res = await api.get('/config'); + this.providers = res.data.providers || []; + this.renderProviders(); + } catch (err) { + this.showMessage('加载供应商配置失败: ' + err.message, 'error'); + } + }, + + renderProviders() { + const list = document.getElementById('providerList'); + if (this.providers.length === 0) { + list.innerHTML = '

暂无供应商配置

'; + return; + } + + list.innerHTML = this.providers.map((p, i) => ` +
+
+ ${this.escapeHtml(p.name)} +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `).join(''); + }, + + addProvider() { + this.providers.push({ + name: '新供应商', + apiUrl: '', + apiKey: '', + models: [], + type: 'newapi', + enabled: true + }); + this.renderProviders(); + this.saveProviders(); + }, + + updateProvider(index, field, value) { + this.providers[index][field] = value; + this.saveProviders(); + }, + + updateProviderModels(index, value) { + this.providers[index].models = value.split(',').map(m => m.trim()).filter(m => m); + this.saveProviders(); + }, + + toggleProvider(index, enabled) { + this.providers[index].enabled = enabled; + this.saveProviders(); + }, + + deleteProvider(index) { + if (!confirm('确定要删除供应商 "' + this.providers[index].name + '" 吗?')) return; + this.providers.splice(index, 1); + this.renderProviders(); + this.saveProviders(); + }, + + async saveProviders() { + try { + await api.put('/config', { providers: this.providers }); + this.showMessage('供应商配置已保存', 'success'); + } catch (err) { + this.showMessage('保存失败: ' + err.message, 'error'); + } + }, + + async loadPersonalities() { + try { + const res = await api.get('/personalities'); + this.personalities = res.data || []; + this.renderPersonalities(); + } catch (err) { + this.showMessage('加载人格列表失败: ' + err.message, 'error'); + } + }, + + renderPersonalities() { + const list = document.getElementById('personalityList'); + if (this.personalities.length === 0) { + list.innerHTML = '

暂无人格

'; + return; + } + + list.innerHTML = this.personalities.map(p => ` +
+
+ ${p.icon ? '' + this.escapeHtml(p.icon) + '' : ''}${this.escapeHtml(p.name)} + ${p.is_preset ? '预设' : '自定义'} +
+ ${!p.is_preset ? '' : ''} +
+ `).join(''); + }, + + async createPersonality() { + const name = document.getElementById('newPersonalityName').value.trim(); + const prompt = document.getElementById('newPersonalityPrompt').value.trim(); + const icon = document.getElementById('newPersonalityIcon').value.trim(); + const description = document.getElementById('newPersonalityDesc').value.trim(); + + if (!name || !prompt) { + this.showMessage('名称和提示词不能为空', 'error'); + return; + } + + try { + await api.post('/personalities', { name, prompt, icon, description }); + this.showMessage('人格创建成功', 'success'); + // 清空表单 + document.getElementById('newPersonalityName').value = ''; + document.getElementById('newPersonalityPrompt').value = ''; + document.getElementById('newPersonalityIcon').value = ''; + document.getElementById('newPersonalityDesc').value = ''; + await this.loadPersonalities(); + } catch (err) { + this.showMessage('创建失败: ' + err.message, 'error'); + } + }, + + async deletePersonality(id) { + if (!confirm('确定要删除这个人格吗?')) return; + try { + await api.delete('/personalities/' + id); + this.showMessage('人格已删除', 'success'); + await this.loadPersonalities(); + } catch (err) { + this.showMessage('删除失败: ' + err.message, 'error'); + } + }, + + showMessage(text, type) { + const el = document.getElementById('configMessage'); + el.innerHTML = `
${this.escapeHtml(text)}
`; + setTimeout(() => { el.innerHTML = ''; }, 3000); + }, + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +}; + +ConfigPage.init(); diff --git a/assets/js/install.js b/assets/js/install.js new file mode 100644 index 0000000..167a72d --- /dev/null +++ b/assets/js/install.js @@ -0,0 +1,201 @@ +const InstallWizard = { + currentStep: 1, + totalSteps: 5, + + init() { + this.checkEnv(); + this.addProvider(); // 默认添加一个供应商表单 + }, + + checkEnv() { + const checks = document.getElementById('envCheckData'); + if (!checks) return; + + const data = JSON.parse(checks.textContent); + + const container = document.getElementById('envChecks'); + container.innerHTML = data.map(c => ` +
+ ${c.name} + + ${c.pass + ? ' 通过' + : ' 未通过' + } + +
+ `).join(''); + }, + + nextStep() { + if (this.currentStep === 4) { + // 验证管理员密码 + const pwd = document.getElementById('adminPassword').value; + const confirm = document.getElementById('adminPasswordConfirm').value; + const username = document.getElementById('adminUsername').value; + if (!username) { alert('请输入管理员用户名'); return; } + if (pwd.length < 6) { alert('密码至少6位'); return; } + if (pwd !== confirm) { alert('两次密码不一致'); return; } + } + if (this.currentStep < this.totalSteps) { + this.currentStep++; + this.updateSteps(); + } + }, + + prevStep() { + if (this.currentStep > 1) { + this.currentStep--; + this.updateSteps(); + } + }, + + updateSteps() { + document.querySelectorAll('.step-content').forEach((el, i) => { + el.classList.toggle('active', i + 1 === this.currentStep); + }); + document.querySelectorAll('.step-indicator .step').forEach((el, i) => { + el.classList.remove('active', 'completed'); + if (i + 1 === this.currentStep) el.classList.add('active'); + if (i + 1 < this.currentStep) el.classList.add('completed'); + }); + }, + + async testDb() { + const result = document.getElementById('dbTestResult'); + try { + const response = await fetch('/api/install/test-db', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + host: document.getElementById('dbHost').value, + port: parseInt(document.getElementById('dbPort').value), + user: document.getElementById('dbUser').value, + password: document.getElementById('dbPassword').value, + database: document.getElementById('dbName').value + }) + }); + const data = await response.json(); + result.innerHTML = `
${data.message}
`; + } catch (err) { + result.innerHTML = '
连接失败: ' + err.message + '
'; + } + }, + + addProvider() { + const list = document.getElementById('providerList'); + const index = list.children.length; + const html = ` +
+
+ 供应商 #${index + 1} + ${index > 0 ? '' : ''} +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ `; + list.insertAdjacentHTML('beforeend', html); + }, + + generateSecret() { + const chars = '0123456789abcdef'; + let result = ''; + for (let i = 0; i < 64; i++) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; + }, + + async runInstall() { + const btn = document.getElementById('installBtn'); + const result = document.getElementById('installResult'); + + // 收集供应商数据 + const providers = []; + document.querySelectorAll('.provider-item').forEach(item => { + const models = item.querySelector('.provider-models').value.split(',').map(m => m.trim()).filter(m => m); + providers.push({ + name: item.querySelector('.provider-name').value, + apiUrl: item.querySelector('.provider-url').value, + apiKey: item.querySelector('.provider-key').value, + models: models, + type: item.querySelector('.provider-type').value, + enabled: true + }); + }); + + if (providers.length === 0 || !providers[0].name || !providers[0].apiKey) { + alert('请至少配置一个完整的供应商'); + return; + } + + btn.disabled = true; + btn.textContent = '安装中...'; + + const setupData = { + username: document.getElementById('adminUsername').value, + password: document.getElementById('adminPassword').value, + dbConfig: { + host: document.getElementById('dbHost').value, + port: parseInt(document.getElementById('dbPort').value), + user: document.getElementById('dbUser').value, + password: document.getElementById('dbPassword').value, + database: document.getElementById('dbName').value + }, + appConfig: { + jwtSecret: document.getElementById('jwtSecret').value || undefined, + jwtExpiry: parseInt(document.getElementById('jwtExpiry').value) || 86400 + }, + providers: providers + }; + + try { + const response = await fetch('/api/install/setup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(setupData) + }); + const data = await response.json(); + + if (data.success) { + result.innerHTML = '
安装成功!正在跳转到登录页...
'; + setTimeout(() => { window.location.href = '/login.php'; }, 2000); + } else { + result.innerHTML = '
安装失败: ' + data.message + '
'; + btn.disabled = false; + btn.textContent = '完成安装'; + } + } catch (err) { + result.innerHTML = '
安装失败: ' + err.message + '
'; + btn.disabled = false; + btn.textContent = '完成安装'; + } + } +}; + +InstallWizard.init(); diff --git a/assets/js/login.js b/assets/js/login.js new file mode 100644 index 0000000..f40cd2e --- /dev/null +++ b/assets/js/login.js @@ -0,0 +1,50 @@ +// 页面加载时检查是否已登录 +(function() { + const token = localStorage.getItem('token'); + if (token) { + window.location.href = '/chat.php'; + } +})(); + +// 登录表单提交 +document.getElementById('loginForm').addEventListener('submit', async function(e) { + e.preventDefault(); + const btn = document.getElementById('loginBtn'); + const errorEl = document.getElementById('loginError'); + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value; + + if (!username || !password) { + errorEl.textContent = '请输入用户名和密码'; + errorEl.classList.remove('hidden'); + return; + } + + btn.disabled = true; + btn.textContent = '登录中...'; + errorEl.classList.add('hidden'); + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (data.success) { + localStorage.setItem('token', data.data.token); + window.location.href = '/chat.php'; + } else { + errorEl.textContent = data.message || '登录失败'; + errorEl.classList.remove('hidden'); + } + } catch (err) { + errorEl.textContent = '网络错误,请稍后重试'; + errorEl.classList.remove('hidden'); + } finally { + btn.disabled = false; + btn.textContent = '登录'; + } +}); diff --git a/public/install.php b/public/install.php index 6dfad90..a5955da 100644 --- a/public/install.php +++ b/public/install.php @@ -5,6 +5,15 @@ if (file_exists($configFile)) { header('Location: /login.php'); exit; } + +// 环境检查数据(JSON 格式供 JS 使用) +$envChecks = json_encode([ + ['name' => 'PHP 版本 >= 8.0', 'pass' => version_compare(PHP_VERSION, '8.0.0', '>=')], + ['name' => 'PDO 扩展', 'pass' => extension_loaded('pdo')], + ['name' => 'cURL 扩展', 'pass' => extension_loaded('curl')], + ['name' => 'uploads/ 目录可写', 'pass' => is_writable(__DIR__ . '/../uploads')], + ['name' => 'config/ 目录可写', 'pass' => is_writable(__DIR__ . '/../config')], +]); ?> @@ -15,6 +24,9 @@ if (file_exists($configFile)) { + + +
@@ -75,11 +87,11 @@ if (file_exists($configFile)) {

填写 MySQL 数据库连接信息

-
+
-
+
@@ -89,11 +101,11 @@ if (file_exists($configFile)) {
-
+
-
+
@@ -102,7 +114,7 @@ if (file_exists($configFile)) {
@@ -144,11 +156,11 @@ if (file_exists($configFile)) {
-
+
-
+
@@ -168,11 +180,11 @@ if (file_exists($configFile)) {
- -
+
@@ -185,210 +197,6 @@ if (file_exists($configFile)) {
- +