diff --git a/src/App.jsx b/src/App.jsx
index 4522369..66b856d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -14,6 +14,7 @@ import PromptManagementPage from './pages/PromptManagementPage';
import KnowledgeBasePage from './pages/KnowledgeBasePage';
import EditKnowledgeBase from './pages/EditKnowledgeBase';
import ClientDownloadPage from './pages/ClientDownloadPage';
+import AccountSettings from './pages/AccountSettings';
import './App.css';
function App() {
@@ -100,6 +101,9 @@ function App() {
:
} />
+ :
+ } />
} />
} />
diff --git a/src/config/api.js b/src/config/api.js
index 71c83e7..6be55dd 100644
--- a/src/config/api.js
+++ b/src/config/api.js
@@ -82,6 +82,15 @@ const API_CONFIG = {
DELETE: (id) => `/api/clients/${id}`,
UPLOAD: '/api/clients/upload'
},
+ EXTERNAL_APPS: {
+ LIST: '/api/external-apps',
+ ACTIVE: '/api/external-apps/active',
+ CREATE: '/api/external-apps',
+ UPDATE: (id) => `/api/external-apps/${id}`,
+ DELETE: (id) => `/api/external-apps/${id}`,
+ UPLOAD_APK: '/api/external-apps/upload-apk',
+ UPLOAD_ICON: '/api/external-apps/upload-icon'
+ },
DICT_DATA: {
TYPES: '/api/dict/types',
BY_TYPE: (dictType) => `/api/dict/${dictType}`,
diff --git a/src/pages/AccountSettings.css b/src/pages/AccountSettings.css
new file mode 100644
index 0000000..f697875
--- /dev/null
+++ b/src/pages/AccountSettings.css
@@ -0,0 +1,236 @@
+.account-settings-page {
+ min-height: 100vh;
+ background: #f8fafc;
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-container {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 2rem;
+ width: 100%;
+}
+
+.settings-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.back-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: white;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ cursor: pointer;
+ color: #64748b;
+ transition: all 0.2s;
+}
+
+.back-btn:hover {
+ color: #1e293b;
+ border-color: #cbd5e1;
+}
+
+.settings-header h1 {
+ margin: 0;
+ font-size: 1.5rem;
+ color: #1e293b;
+}
+
+.settings-content {
+ display: flex;
+ gap: 2rem;
+ background: white;
+ border-radius: 12px;
+ border: 1px solid #e9ecef;
+ overflow: hidden;
+ min-height: 600px;
+}
+
+.settings-sidebar {
+ width: 250px;
+ background: #f8fafc;
+ padding: 2rem 0;
+ border-right: 1px solid #e2e8f0;
+}
+
+.sidebar-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 2rem;
+ border: none ;
+ border-radius: 0px;
+ background: transparent;
+ color: #64748b;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: left;
+ font-weight: 500;
+}
+
+.sidebar-item:hover {
+ background: #f1f5f9;
+ color: #1e293b;
+}
+
+.sidebar-item.active {
+ background: white;
+ color: #667eea;
+ border-left: 3px solid #667eea;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.settings-main {
+ flex: 1;
+ padding: 3rem;
+}
+
+.settings-form {
+ max-width: 500px;
+}
+
+.form-section h3 {
+ margin: 0 0 2rem 0;
+ font-size: 1.25rem;
+ color: #1e293b;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ color: #475569;
+}
+
+.form-group input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: all 0.2s;
+}
+
+.form-group input:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.disabled-input {
+ background: #f1f5f9;
+ color: #94a3b8;
+ cursor: not-allowed;
+}
+
+.avatar-upload {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ margin-bottom: 2rem;
+}
+
+.avatar-preview {
+ position: relative;
+ width: 100px;
+ height: 100px;
+}
+
+.avatar-preview img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 3px solid #e2e8f0;
+}
+
+.avatar-placeholder {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: #f1f5f9;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #94a3b8;
+ border: 3px solid #e2e8f0;
+}
+
+.upload-avatar {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 32px;
+ height: 32px;
+ background: #667eea;
+ color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: all 0.2s;
+ z-index: 10;
+}
+
+.upload-avatar:hover {
+ background: #5a67d8;
+ transform: scale(1.1);
+}
+
+.upload-avatar svg {
+ width: 16px;
+ height: 16px;
+}
+
+.avatar-info p {
+ color: #94a3b8;
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.form-actions {
+ margin-top: 2rem;
+ padding-top: 2rem;
+ border-top: 1px solid #e2e8f0;
+}
+
+.btn-primary {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-primary:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 6px rgba(102, 126, 234, 0.25);
+}
+
+.btn-primary:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
diff --git a/src/pages/AccountSettings.jsx b/src/pages/AccountSettings.jsx
new file mode 100644
index 0000000..480a7cb
--- /dev/null
+++ b/src/pages/AccountSettings.jsx
@@ -0,0 +1,299 @@
+import React, { useState, useEffect } from 'react';
+import { User, Lock, Mail, Camera, Upload, Save, ArrowLeft, UserCog } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import apiClient from '../utils/apiClient';
+import { buildApiUrl, API_ENDPOINTS } from '../config/api';
+import Toast from '../components/Toast';
+import Breadcrumb from '../components/Breadcrumb';
+import './AccountSettings.css';
+
+const AccountSettings = ({ user, onUpdateUser }) => {
+ const navigate = useNavigate();
+ const [activeTab, setActiveTab] = useState('profile');
+ const [loading, setLoading] = useState(false);
+ const [toasts, setToasts] = useState([]);
+
+ // Profile State
+ const [profileData, setProfileData] = useState({
+ caption: user?.caption || '',
+ email: user?.email || '',
+ username: user?.username || '',
+ avatar_url: user?.avatar_url || ''
+ });
+ const [previewAvatar, setPreviewAvatar] = useState(null);
+ const [avatarFile, setAvatarFile] = useState(null);
+
+ // Password State
+ const [passwordData, setPasswordData] = useState({
+ old_password: '',
+ new_password: '',
+ confirm_password: ''
+ });
+
+ useEffect(() => {
+ // Refresh user data on mount
+ fetchUserData();
+ }, []);
+
+ const fetchUserData = async () => {
+ try {
+ const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
+ if (response.code === '200') {
+ const userData = response.data;
+ setProfileData({
+ caption: userData.caption,
+ email: userData.email,
+ username: userData.username,
+ avatar_url: userData.avatar_url
+ });
+ }
+ } catch (error) {
+ console.error('Failed to fetch user data:', error);
+ }
+ };
+
+ const showToast = (message, type = 'info') => {
+ const id = Date.now();
+ setToasts(prev => [...prev, { id, message, type }]);
+ };
+
+ const removeToast = (id) => {
+ setToasts(prev => prev.filter(toast => toast.id !== id));
+ };
+
+ const handleAvatarChange = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ setAvatarFile(file);
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setPreviewAvatar(reader.result);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleProfileUpdate = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ // 1. Upload avatar if changed
+ let newAvatarUrl = profileData.avatar_url;
+ if (avatarFile) {
+ const formData = new FormData();
+ formData.append('file', avatarFile);
+ const uploadRes = await apiClient.post(
+ buildApiUrl(`/api/users/${user.user_id}/avatar`),
+ formData
+ );
+ if (uploadRes.code === '200') {
+ newAvatarUrl = uploadRes.data.avatar_url;
+ }
+ }
+
+ // 2. Update profile info
+ const updateRes = await apiClient.put(
+ buildApiUrl(API_ENDPOINTS.USERS.UPDATE(user.user_id)),
+ {
+ caption: profileData.caption,
+ email: profileData.email,
+ avatar_url: newAvatarUrl
+ }
+ );
+
+ if (updateRes.code === '200') {
+ showToast('个人信息更新成功', 'success');
+ setProfileData(prev => ({ ...prev, avatar_url: newAvatarUrl }));
+ setAvatarFile(null);
+ setPreviewAvatar(null);
+
+ // Update local storage user info
+ const updatedUser = { ...user, ...updateRes.data };
+ localStorage.setItem('iMeetingUser', JSON.stringify(updatedUser));
+ if (onUpdateUser) onUpdateUser(updatedUser);
+ }
+ } catch (error) {
+ console.error('Update failed:', error);
+ showToast(error.response?.data?.message || '更新失败', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handlePasswordUpdate = async (e) => {
+ e.preventDefault();
+ if (passwordData.new_password !== passwordData.confirm_password) {
+ showToast('两次输入的密码不一致', 'error');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ await apiClient.put(
+ buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)),
+ {
+ old_password: passwordData.old_password,
+ new_password: passwordData.new_password
+ }
+ );
+ showToast('密码修改成功', 'success');
+ setPasswordData({ old_password: '', new_password: '', confirm_password: '' });
+ } catch (error) {
+ showToast(error.response?.data?.message || '密码修改失败', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {activeTab === 'profile' && (
+
+ )}
+
+ {activeTab === 'security' && (
+
+ )}
+
+
+
+
+
+ {toasts.map(toast => (
+ removeToast(toast.id)}
+ />
+ ))}
+
+
+ );
+};
+
+export default AccountSettings;
diff --git a/src/pages/AdminDashboard.css b/src/pages/AdminDashboard.css
index bf269d4..57ca550 100644
--- a/src/pages/AdminDashboard.css
+++ b/src/pages/AdminDashboard.css
@@ -472,3 +472,52 @@
justify-content: space-between;
}
}
+
+/* Modal Styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ width: 90%;
+ max-width: 500px;
+}
+
+.modal-content h2 {
+ margin-top: 0;
+}
+
+.modal-content .form-group {
+ margin-bottom: 1rem;
+}
+
+.modal-content .form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.modal-content .form-group input {
+ width: 100%;
+ padding: 0.5rem;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+}
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1rem;
+ margin-top: 2rem;
+}
\ No newline at end of file
diff --git a/src/pages/AdminDashboard.jsx b/src/pages/AdminDashboard.jsx
index 1576a7b..6bf14e0 100644
--- a/src/pages/AdminDashboard.jsx
+++ b/src/pages/AdminDashboard.jsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { LogOut, User, Users, Activity, Server, HardDrive, Cpu, MemoryStick, RefreshCw, UserX, ChevronDown, KeyRound, Shield, BookText, Waves } from 'lucide-react';
+import { LogOut, User, Users, Activity, Server, HardDrive, Cpu, MemoryStick, RefreshCw, UserX, ChevronDown, KeyRound, Shield, BookText, Waves, UserCog } from 'lucide-react';
import apiClient from '../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import Dropdown from '../components/Dropdown';
@@ -33,7 +33,7 @@ const getTaskTypeText = (type) => TASK_TYPE_TEXT_MAP[type] || type;
// 默认管理员菜单
const getDefaultMenus = () => [
- { menu_code: 'change_password', menu_name: '修改密码', menu_type: 'action' },
+ { menu_code: 'account_settings', menu_name: '账户设置', menu_type: 'link', menu_url: '/account-settings' },
{ menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompts' },
{ menu_code: 'platform_admin', menu_name: '平台管理', menu_type: 'link', menu_url: '/admin' },
{ menu_code: 'logout', menu_name: '退出登录', menu_type: 'action' }
@@ -64,6 +64,7 @@ const AdminDashboard = ({ user, onLogout }) => {
const getMenuItemConfig = (menu) => {
const iconMap = {
'change_password': ,
+ 'account_settings': ,
'prompt_management': ,
'platform_admin': ,
'logout':
@@ -248,7 +249,21 @@ const AdminDashboard = ({ user, onLogout }) => {
-
+ {user.avatar_url ? (
+
+ ) : (
+
+ )}
{user.caption}
diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx
index 3de1130..0546ea8 100644
--- a/src/pages/AdminManagement.jsx
+++ b/src/pages/AdminManagement.jsx
@@ -1,8 +1,9 @@
import React from 'react';
-import { Settings, Users, Smartphone, Shield, BookText, Type } from 'lucide-react';
+import { Settings, Users, Smartphone, Shield, BookText, Type, Package } from 'lucide-react';
import { Tabs } from 'antd';
import UserManagement from './admin/UserManagement';
import ClientManagement from './ClientManagement';
+import ExternalAppManagement from './admin/ExternalAppManagement';
import PermissionManagement from './admin/PermissionManagement';
import DictManagement from './admin/DictManagement';
import HotWordManagement from './admin/HotWordManagement';
@@ -38,6 +39,11 @@ const AdminManagement = () => {
label: 客户端管理,
children: ,
},
+ {
+ key: 'externalAppManagement',
+ label: 外部应用管理,
+ children: ,
+ },
];
return (
diff --git a/src/pages/Dashboard.css b/src/pages/Dashboard.css
index d1b27dc..3e005b5 100644
--- a/src/pages/Dashboard.css
+++ b/src/pages/Dashboard.css
@@ -184,6 +184,8 @@
justify-content: center;
color: white;
flex-shrink: 0;
+ overflow: hidden;
+ border: 2px solid #e2e8f0;
}
.user-details {
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 67a0a24..b6ad2e0 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
-import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library, BookText, Waves } from 'lucide-react';
+import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library, BookText, Waves, UserCog } from 'lucide-react';
import apiClient from '../utils/apiClient';
import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
@@ -26,12 +26,6 @@ const Dashboard = ({ user, onLogout }) => {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState('');
- const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
- const [oldPassword, setOldPassword] = useState('');
- const [newPassword, setNewPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [passwordChangeError, setPasswordChangeError] = useState('');
- const [passwordChangeSuccess, setPasswordChangeSuccess] = useState('');
// 声纹相关状态
const [voiceprintStatus, setVoiceprintStatus] = useState(null);
@@ -126,7 +120,7 @@ const Dashboard = ({ user, onLogout }) => {
// 获取默认菜单(fallback)
const getDefaultMenus = () => {
const defaultMenus = [
- { menu_code: 'change_password', menu_name: '修改密码', menu_type: 'action', sort_order: 1 },
+ { menu_code: 'account_settings', menu_name: '账户设置', menu_type: 'link', menu_url: '/account-settings', sort_order: 1 },
{ menu_code: 'prompt_management', menu_name: '提示词仓库', menu_type: 'link', menu_url: '/prompt-management', sort_order: 2 },
{ menu_code: 'logout', menu_name: '退出登录', menu_type: 'action', sort_order: 99 }
];
@@ -150,13 +144,13 @@ const Dashboard = ({ user, onLogout }) => {
const getMenuItemConfig = (menu) => {
const iconMap = {
'change_password': ,
+ 'account_settings': ,
'prompt_management': ,
'platform_admin': ,
'logout':
};
const actionMap = {
- 'change_password': () => setShowChangePasswordModal(true),
'prompt_management': () => window.location.href = '/prompt-management',
'platform_admin': () => window.location.href = '/admin/management',
'logout': onLogout
@@ -312,39 +306,6 @@ const Dashboard = ({ user, onLogout }) => {
}
};
- const handlePasswordChange = async (e) => {
- e.preventDefault();
- if (newPassword !== confirmPassword) {
- setPasswordChangeError('新密码不匹配');
- return;
- }
- if (newPassword.length < 6) {
- setPasswordChangeError('新密码长度不能少于6位');
- return;
- }
-
- setPasswordChangeError('');
- setPasswordChangeSuccess('');
-
- try {
- await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)), {
- old_password: oldPassword,
- new_password: newPassword,
- });
- setPasswordChangeSuccess('密码修改成功!');
- // 清空输入框并准备关闭模态框
- setOldPassword('');
- setNewPassword('');
- setConfirmPassword('');
- setTimeout(() => {
- setShowChangePasswordModal(false);
- setPasswordChangeSuccess('');
- }, 2000);
- } catch (err) {
- setPasswordChangeError(err.response?.data?.message || '密码修改失败');
- }
- };
-
const handleVoiceprintUpload = async (formData) => {
try {
await apiClient.post(
@@ -430,7 +391,21 @@ const Dashboard = ({ user, onLogout }) => {
- 欢迎,{userInfo?.caption}
+ {user.avatar_url ? (
+
+ ) : null}
+ 欢迎,{user.caption}
}
@@ -449,7 +424,15 @@ const Dashboard = ({ user, onLogout }) => {
-
+ {userInfo?.avatar_url ? (
+
)
+ ) : (
+
+ )}
@@ -597,34 +580,6 @@ const Dashboard = ({ user, onLogout }) => {
- {showChangePasswordModal && (
-
- )}
-
{/* 声纹采集模态框 */}
{
+ const [apps, setApps] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showAppModal, setShowAppModal] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
+ const [selectedApp, setSelectedApp] = useState(null);
+ const [filterAppType, setFilterAppType] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [toasts, setToasts] = useState([]);
+ const [uploadingApk, setUploadingApk] = useState(false);
+ const [uploadingIcon, setUploadingIcon] = useState(false);
+
+ const [formData, setFormData] = useState({
+ app_name: '',
+ app_type: 'native',
+ app_info: {
+ version_name: '',
+ package_name: '',
+ apk_url: '',
+ web_url: ''
+ },
+ icon_url: '',
+ description: '',
+ sort_order: 0,
+ is_active: true
+ });
+
+ // Toast helper functions
+ const showToast = (message, type = 'info') => {
+ const id = Date.now();
+ setToasts(prev => [...prev, { id, message, type }]);
+ };
+
+ const removeToast = (id) => {
+ setToasts(prev => prev.filter(toast => toast.id !== id));
+ };
+
+ useEffect(() => {
+ fetchApps();
+ }, []);
+
+ const fetchApps = async () => {
+ setLoading(true);
+ try {
+ const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
+ setApps(response.data.apps || []);
+ } catch (error) {
+ console.error('获取外部应用列表失败:', error);
+ showToast('获取外部应用列表失败', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAddApp = () => {
+ setIsEditing(false);
+ setSelectedApp(null);
+ setFormData({
+ app_name: '',
+ app_type: 'native',
+ app_info: {
+ version_name: '',
+ package_name: '',
+ apk_url: '',
+ web_url: ''
+ },
+ icon_url: '',
+ description: '',
+ sort_order: 0,
+ is_active: true
+ });
+ setShowAppModal(true);
+ };
+
+ const handleEditApp = (app) => {
+ setIsEditing(true);
+ setSelectedApp(app);
+
+ // 解析 app_info
+ let appInfo = {
+ version_name: '',
+ package_name: '',
+ apk_url: '',
+ web_url: ''
+ };
+
+ if (typeof app.app_info === 'string') {
+ try {
+ appInfo = { ...appInfo, ...JSON.parse(app.app_info) };
+ } catch (e) {
+ console.error('解析 app_info 失败:', e);
+ }
+ } else if (typeof app.app_info === 'object') {
+ appInfo = { ...appInfo, ...app.app_info };
+ }
+
+ setFormData({
+ app_name: app.app_name || '',
+ app_type: app.app_type || 'native',
+ app_info: appInfo,
+ icon_url: app.icon_url || '',
+ description: app.description || '',
+ sort_order: app.sort_order || 0,
+ is_active: app.is_active === 1 || app.is_active === true
+ });
+ setShowAppModal(true);
+ };
+
+ const handleDeleteApp = async () => {
+ if (!deleteConfirmInfo) return;
+
+ try {
+ await apiClient.delete(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.DELETE(deleteConfirmInfo.id)));
+ showToast('删除成功', 'success');
+ fetchApps();
+ } catch (error) {
+ console.error('删除失败:', error);
+ const errorMsg = error.response?.data?.message || error.message || '删除失败';
+ showToast(errorMsg, 'error');
+ } finally {
+ setDeleteConfirmInfo(null);
+ }
+ };
+
+ const handleFormChange = (field, value) => {
+ if (field.startsWith('app_info.')) {
+ const infoField = field.split('.')[1];
+ setFormData(prev => ({
+ ...prev,
+ app_info: {
+ ...prev.app_info,
+ [infoField]: value
+ }
+ }));
+ } else {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ }
+ };
+
+ const handleApkUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (!file.name.endsWith('.apk')) {
+ showToast('请选择APK文件', 'error');
+ return;
+ }
+
+ setUploadingApk(true);
+ try {
+ const formDataObj = new FormData();
+ formDataObj.append('apk_file', file);
+
+ const response = await apiClient.post(
+ buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPLOAD_APK),
+ formDataObj,
+ {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ }
+ );
+
+ const apkData = response.data;
+
+ // 自动填充表单
+ setFormData(prev => ({
+ ...prev,
+ app_name: apkData.app_name || prev.app_name,
+ app_info: {
+ ...prev.app_info,
+ version_name: apkData.version_name || '',
+ package_name: apkData.package_name || '',
+ apk_url: apkData.apk_url || ''
+ }
+ }));
+
+ showToast('APK上传并解析成功', 'success');
+ } catch (error) {
+ console.error('上传APK失败:', error);
+ const errorMsg = error.response?.data?.message || error.message || '上传失败';
+ showToast(errorMsg, 'error');
+ } finally {
+ setUploadingApk(false);
+ e.target.value = ''; // 重置input
+ }
+ };
+
+ const handleIconUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // 验证文件类型
+ const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
+ if (!allowedTypes.includes(file.type)) {
+ showToast('请选择图片文件(JPG、PNG、GIF、WEBP)', 'error');
+ return;
+ }
+
+ setUploadingIcon(true);
+ try {
+ const formDataObj = new FormData();
+ formDataObj.append('icon_file', file);
+
+ const response = await apiClient.post(
+ buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPLOAD_ICON),
+ formDataObj,
+ {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ }
+ );
+
+ const iconData = response.data;
+
+ // 更新表单中的图标URL
+ setFormData(prev => ({
+ ...prev,
+ icon_url: iconData.icon_url || ''
+ }));
+
+ showToast('图标上传成功', 'success');
+ } catch (error) {
+ console.error('上传图标失败:', error);
+ const errorMsg = error.response?.data?.message || error.message || '上传失败';
+ showToast(errorMsg, 'error');
+ } finally {
+ setUploadingIcon(false);
+ e.target.value = ''; // 重置input
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // 验证必填字段
+ if (!formData.app_name) {
+ showToast('请输入应用名称', 'error');
+ return;
+ }
+
+ // 根据应用类型构建 app_info
+ let appInfoObj = {};
+ if (formData.app_type === 'native') {
+ if (!formData.app_info.version_name || !formData.app_info.package_name || !formData.app_info.apk_url) {
+ showToast('请填写完整的原生应用信息', 'error');
+ return;
+ }
+ appInfoObj = {
+ version_name: formData.app_info.version_name,
+ package_name: formData.app_info.package_name,
+ apk_url: formData.app_info.apk_url
+ };
+ } else {
+ if (!formData.app_info.web_url) {
+ showToast('请输入Web应用URL', 'error');
+ return;
+ }
+ appInfoObj = {
+ version_name: formData.app_info.version_name || '1.0',
+ web_url: formData.app_info.web_url
+ };
+ }
+
+ const submitData = {
+ app_name: formData.app_name,
+ app_type: formData.app_type,
+ app_info: JSON.stringify(appInfoObj),
+ icon_url: formData.icon_url,
+ description: formData.description,
+ sort_order: parseInt(formData.sort_order) || 0,
+ is_active: formData.is_active
+ };
+
+ try {
+ if (isEditing && selectedApp) {
+ await apiClient.put(
+ buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(selectedApp.id)),
+ submitData
+ );
+ showToast('更新成功', 'success');
+ } else {
+ await apiClient.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), submitData);
+ showToast('创建成功', 'success');
+ }
+ setShowAppModal(false);
+ fetchApps();
+ } catch (error) {
+ console.error('操作失败:', error);
+ const errorMsg = error.response?.data?.message || error.message || '操作失败';
+ showToast(errorMsg, 'error');
+ }
+ };
+
+ // 过滤应用列表
+ const filteredApps = apps.filter(app => {
+ const matchesType = !filterAppType || app.app_type === filterAppType;
+ const matchesSearch = !searchQuery ||
+ app.app_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (app.description && app.description.toLowerCase().includes(searchQuery.toLowerCase()));
+ return matchesType && matchesSearch;
+ });
+
+ // 获取应用信息
+ const getAppInfo = (app) => {
+ if (typeof app.app_info === 'string') {
+ try {
+ return JSON.parse(app.app_info);
+ } catch {
+ return {};
+ }
+ }
+ return app.app_info || {};
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
外部应用管理
+
管理Android原生应用和Web应用
+
+
+
+
+ {/* 筛选和搜索 */}
+
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+
+ )}
+
+
+
+ {/* 应用列表 */}
+
+
+
+
+ | 应用名称 |
+ 类型 |
+ 版本 |
+ 详细信息 |
+ 描述 |
+ 排序 |
+ 状态 |
+ 创建时间 |
+ 操作 |
+
+
+
+ {filteredApps.length === 0 ? (
+
+ |
+ 暂无数据
+ |
+
+ ) : (
+ filteredApps.map(app => {
+ const appInfo = getAppInfo(app);
+ return (
+
+
+
+ {app.icon_url && (
+ 
+ )}
+ {app.app_name}
+
+ |
+
+
+ {app.app_type === 'native' ? (
+ <>
+
+ 原生应用
+ >
+ ) : (
+ <>
+
+ Web应用
+ >
+ )}
+
+ |
+ {appInfo.version_name || '-'} |
+
+ {app.app_type === 'native' ? (
+
+ 包名: {appInfo.package_name || '-'}
+ {appInfo.apk_url && (
+
+
+ 下载APK
+
+ )}
+
+ ) : (
+
+ )}
+ |
+
+
+ {app.description || '-'}
+
+ |
+ {app.sort_order} |
+
+
+ {app.is_active ? '启用' : '禁用'}
+
+ |
+ {new Date(app.created_at).toLocaleDateString()} |
+
+
+
+
+
+ |
+
+ );
+ })
+ )}
+
+
+
+
+ {/* 添加/编辑应用模态框 */}
+ {showAppModal && (
+
setShowAppModal(false)}>
+
e.stopPropagation()}>
+
+
{isEditing ? '编辑应用' : '添加应用'}
+
+
+
+
+
+ )}
+
+ {/* 删除确认对话框 */}
+ {deleteConfirmInfo && (
+
setDeleteConfirmInfo(null)}
+ confirmText="删除"
+ cancelText="取消"
+ type="danger"
+ />
+ )}
+
+ {/* Toast 通知 */}
+
+ {toasts.map(toast => (
+ removeToast(toast.id)}
+ />
+ ))}
+
+
+ );
+};
+
+export default ExternalAppManagement;
diff --git a/src/pages/admin/PromptManagement.jsx b/src/pages/admin/PromptManagement.jsx
index 32bce9d..53f1bdc 100644
--- a/src/pages/admin/PromptManagement.jsx
+++ b/src/pages/admin/PromptManagement.jsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
-import { Plus, ChevronLeft, ChevronRight, Trash2, BookText, FileText, Star, Save, Check, Edit2, X } from 'lucide-react';
+import { Plus, ChevronLeft, ChevronRight, Trash2, BookText, FileText, Star, Save, Check, Edit2, X, MessageSquare, Library } from 'lucide-react';
import './PromptManagement.css';
import ConfirmDialog from '../../components/ConfirmDialog';
import FormModal from '../../components/FormModal';
@@ -10,8 +10,8 @@ import MarkdownEditor from '../../components/MarkdownEditor';
import Breadcrumb from '../../components/Breadcrumb';
const TASK_TYPES = {
- MEETING_TASK: { label: '会议任务', icon: '📝' },
- KNOWLEDGE_TASK: { label: '知识库任务', icon: '📚' }
+ MEETING_TASK: { label: '会议任务', icon: },
+ KNOWLEDGE_TASK: { label: '知识库任务', icon: }
};
const PromptManagement = () => {