feat: 新增管理台模型配置管理功能
- 支持配置类型注册表机制(basic、zhisuan) - 配置列表展示(生效中/未生效状态区分) - 新增/编辑配置表单,支持动态字段渲染 - 生效中配置不可编辑/删除限制 - 配置类型创建后不可修改 - 密钥掩码显示与显示/隐藏切换 - 操作二次确认弹窗(设为默认、删除)
This commit is contained in:
@@ -24,12 +24,14 @@ export const ADMIN_PAGES = {
|
||||
departments: { title: '部门管理', icon: 'FiBarChart2' },
|
||||
users: { title: '用户管理', icon: 'FiUsers' },
|
||||
projects: { title: '项目管理', icon: 'FiList' },
|
||||
modelConfigs: { title: '模型配置', icon: 'FiSettings' },
|
||||
adminLogs: { title: '日志查询', icon: 'FiActivity' },
|
||||
reviewList: { title: '审核管理', icon: 'FiCheckCircle' },
|
||||
reviewDetail: { title: '审核详情', icon: null },
|
||||
addDepartment: { title: '新增部门', icon: null },
|
||||
addUser: { title: '新增用户', icon: null },
|
||||
addProject: { title: '新增项目', icon: null },
|
||||
addModelConfig: { title: '新增配置', icon: null },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ export const CONSOLE_KEYS = {
|
||||
*/
|
||||
export const ADMIN_KEYS = {
|
||||
CURRENT_PAGE: 'admin_currentPage',
|
||||
MODEL_CONFIG_EDIT_DATA: 'admin_modelConfigEditData',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,6 +52,8 @@ export const adminOverview = {
|
||||
};
|
||||
|
||||
export const adminLogs = [
|
||||
{ time: '2026-03-26 15:30:22', user: '管理员', department: '系统管理部', type: '配置变更', action: '切换默认模型配置', status: '成功', detail: '从"智算平台生产环境"切换至"阿里云百炼主账号"' },
|
||||
{ time: '2026-03-26 10:15:33', user: '张三', department: 'AI 产品部', type: '配置变更', action: '新增模型配置', status: '成功', detail: '新增配置"智算平台测试环境"' },
|
||||
{ time: '2026-03-19 16:42:33', user: '张三', department: 'AI 产品部', type: '实例操作', action: '启动实例', status: '成功', detail: '实例 teleclaw-zhangsan 启动成功' },
|
||||
{ time: '2026-03-19 15:28:17', user: '张三', department: 'AI 产品部', type: '技能', action: '调用 代码生成助手', status: '成功', detail: 'Token 消耗: 1,234' },
|
||||
{ time: '2026-03-19 14:55:02', user: '李四', department: '技术研发部', type: '文件上传', action: '上传数据文件', status: '成功', detail: '文件 sales_2026_q1.xlsx 上传完成' },
|
||||
@@ -86,3 +88,65 @@ export const availableDepartments = [
|
||||
{ id: 5, name: '测试部', description: '负责产品质量保障', head: '周九', memberCount: 5 },
|
||||
{ id: 6, name: '客户服务部', description: '负责客户支持与服务', head: '吴十', memberCount: 12 }
|
||||
];
|
||||
|
||||
// 模型配置数据
|
||||
export const modelConfigs = [
|
||||
{
|
||||
id: 'cfg_001',
|
||||
name: '阿里云百炼主账号',
|
||||
type: 'basic',
|
||||
isActive: true,
|
||||
createdAt: '2026-03-20T10:30:00',
|
||||
updatedAt: '2026-03-26T14:30:00',
|
||||
basic: {
|
||||
apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
apiKey: 'sk-ds-abc123xyz789',
|
||||
modelName: 'qwen-max',
|
||||
temperature: 0.7,
|
||||
maxTokens: 4096,
|
||||
topP: 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cfg_002',
|
||||
name: '智算平台生产环境',
|
||||
type: 'zhisuan',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-25T09:00:00',
|
||||
updatedAt: '2026-03-25T09:00:00',
|
||||
zhisuan: {
|
||||
apiUrl: 'https://zhisuan.internal.company.com/api/v1',
|
||||
appId: 'app_prod_001',
|
||||
appSecret: 'secret_prod_xyz123abc'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cfg_003',
|
||||
name: 'DeepSeek 备用',
|
||||
type: 'basic',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-24T16:00:00',
|
||||
updatedAt: '2026-03-24T16:00:00',
|
||||
basic: {
|
||||
apiUrl: 'https://api.deepseek.com/v1',
|
||||
apiKey: 'sk-ds-deepseek456',
|
||||
modelName: 'deepseek-chat',
|
||||
temperature: 0.5,
|
||||
maxTokens: 8192,
|
||||
topP: 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cfg_004',
|
||||
name: '智算平台测试环境',
|
||||
type: 'zhisuan',
|
||||
isActive: false,
|
||||
createdAt: '2026-03-23T11:00:00',
|
||||
updatedAt: '2026-03-23T11:00:00',
|
||||
zhisuan: {
|
||||
apiUrl: 'https://zhisuan-test.internal.company.com/api/v1',
|
||||
appId: 'app_test_001',
|
||||
appSecret: 'secret_test_abc789xyz'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
93
src/data/configTypes.js
Normal file
93
src/data/configTypes.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 模型配置类型注册表
|
||||
* 定义支持的配置类型及其字段元数据
|
||||
*/
|
||||
|
||||
export const MODEL_CONFIG_TYPES = {
|
||||
basic: {
|
||||
key: 'basic',
|
||||
label: 'OpenAI 兼容接口',
|
||||
description: '支持标准的 OpenAI API 格式',
|
||||
fields: [
|
||||
{ key: 'apiUrl', label: 'API 地址', type: 'url', required: true },
|
||||
{ key: 'apiKey', label: 'API 密钥', type: 'password', required: true },
|
||||
{ key: 'modelName', label: '模型名称', type: 'text', required: true },
|
||||
{ key: 'temperature', label: 'Temperature', type: 'number', min: 0, max: 2, step: 0.1, default: 0.7 },
|
||||
{ key: 'maxTokens', label: 'Max Tokens', type: 'number', min: 1, max: 128000, step: 1, default: 4096 },
|
||||
{ key: 'topP', label: 'Top P', type: 'number', min: 0, max: 1, step: 0.1, default: 0.9 },
|
||||
]
|
||||
},
|
||||
zhisuan: {
|
||||
key: 'zhisuan',
|
||||
label: '智算管理平台',
|
||||
description: '内部智算平台接入',
|
||||
fields: [
|
||||
{ key: 'apiUrl', label: 'API 地址', type: 'url', required: true },
|
||||
{ key: 'appId', label: 'App ID', type: 'text', required: true },
|
||||
{ key: 'appSecret', label: 'App Secret', type: 'password', required: true },
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有配置类型列表
|
||||
* @returns {Array} 配置类型列表
|
||||
*/
|
||||
export const getConfigTypeList = () => {
|
||||
return Object.values(MODEL_CONFIG_TYPES).map(type => ({
|
||||
key: type.key,
|
||||
label: type.label,
|
||||
description: type.description
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 key 获取配置类型定义
|
||||
* @param {string} key - 配置类型 key
|
||||
* @returns {Object|undefined} 配置类型定义
|
||||
*/
|
||||
export const getConfigTypeByKey = (key) => {
|
||||
return MODEL_CONFIG_TYPES[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取配置类型的字段定义
|
||||
* @param {string} key - 配置类型 key
|
||||
* @returns {Array} 字段定义数组
|
||||
*/
|
||||
export const getConfigFields = (key) => {
|
||||
const configType = MODEL_CONFIG_TYPES[key];
|
||||
return configType ? configType.fields : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成配置摘要信息
|
||||
* @param {Object} config - 配置对象
|
||||
* @returns {string} 摘要字符串
|
||||
*/
|
||||
export const getConfigSummary = (config) => {
|
||||
if (!config) return '-';
|
||||
|
||||
const type = MODEL_CONFIG_TYPES[config.type];
|
||||
if (!type) return '-';
|
||||
|
||||
if (config.type === 'basic') {
|
||||
return config.basic?.modelName || '-';
|
||||
}
|
||||
|
||||
if (config.type === 'zhisuan') {
|
||||
return '智算平台接入';
|
||||
}
|
||||
|
||||
return '-';
|
||||
};
|
||||
|
||||
/**
|
||||
* 掩码显示敏感信息
|
||||
* @param {string} value - 原始值
|
||||
* @returns {string} 掩码后的值
|
||||
*/
|
||||
export const maskSensitiveValue = (value) => {
|
||||
if (!value || value.length < 8) return '****';
|
||||
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity } from 'react-icons/fi';
|
||||
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity, FiSettings } from 'react-icons/fi';
|
||||
import Layout from '../components/Layout.jsx';
|
||||
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
|
||||
import SidebarUser from '../components/layout/SidebarUser.jsx';
|
||||
@@ -18,6 +18,8 @@ import AddProjectPage from './admin/AddProjectPage.jsx';
|
||||
import AdminLogsPage from './admin/AdminLogsPage.jsx';
|
||||
import ConsoleReviewListPage from './console/ConsoleReviewListPage.jsx';
|
||||
import ConsoleReviewDetailPage from './console/ConsoleReviewDetailPage.jsx';
|
||||
import ModelConfigsPage from './admin/ModelConfigsPage.jsx';
|
||||
import AddModelConfigPage from './admin/AddModelConfigPage.jsx';
|
||||
|
||||
function AdminPage() {
|
||||
const location = useLocation();
|
||||
@@ -94,15 +96,25 @@ function AdminPage() {
|
||||
onBack={() => navigateTo('projects')}
|
||||
editData={editData}
|
||||
/>;
|
||||
case 'modelConfigs':
|
||||
return <ModelConfigsPage
|
||||
onAdd={() => navigateTo('addModelConfig')}
|
||||
onEdit={(config) => navigateTo('addModelConfig', config)}
|
||||
/>;
|
||||
case 'addModelConfig':
|
||||
return <AddModelConfigPage
|
||||
onBack={() => navigateTo('modelConfigs')}
|
||||
editData={editData}
|
||||
/>;
|
||||
default:
|
||||
return <div>Page not found</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const getPageTitle = () => {
|
||||
if (editData && (currentPage === 'addDepartment' || currentPage === 'addUser' || currentPage === 'addProject')) {
|
||||
if (editData && (currentPage === 'addDepartment' || currentPage === 'addUser' || currentPage === 'addProject' || currentPage === 'addModelConfig')) {
|
||||
const prefix = '编辑';
|
||||
const nameMap = { addDepartment: '部门', addUser: '用户', addProject: '项目' };
|
||||
const nameMap = { addDepartment: '部门', addUser: '用户', addProject: '项目', addModelConfig: '配置' };
|
||||
return prefix + nameMap[currentPage];
|
||||
}
|
||||
if (currentPage === 'reviewDetail') {
|
||||
@@ -171,6 +183,15 @@ function AdminPage() {
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
<SidebarNavItem
|
||||
icon={<FiSettings />}
|
||||
label="模型配置"
|
||||
active={currentPage === 'modelConfigs' || currentPage === 'addModelConfig'}
|
||||
onClick={() => navigateTo('modelConfigs')}
|
||||
itemClassName="admin-nav-item"
|
||||
iconClassName="admin-nav-icon"
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
</nav>
|
||||
<SidebarUser
|
||||
onClick={() => {}}
|
||||
|
||||
221
src/pages/admin/AddModelConfigPage.jsx
Normal file
221
src/pages/admin/AddModelConfigPage.jsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiEye, FiEyeOff } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES, getConfigFields, getConfigTypeList } from '../../data/configTypes.js';
|
||||
|
||||
function AddModelConfigPage({ onBack, editData }) {
|
||||
const isEdit = !!editData;
|
||||
|
||||
// 基础信息
|
||||
const [configName, setConfigName] = useState('');
|
||||
const [configType, setConfigType] = useState('basic');
|
||||
|
||||
// 类型特定字段值
|
||||
const [fieldValues, setFieldValues] = useState({});
|
||||
|
||||
// 密码显示/隐藏状态
|
||||
const [showPasswords, setShowPasswords] = useState({});
|
||||
|
||||
// 初始化编辑数据
|
||||
useEffect(() => {
|
||||
if (editData) {
|
||||
setConfigName(editData.name || '');
|
||||
setConfigType(editData.type || 'basic');
|
||||
|
||||
// 加载类型特定字段值
|
||||
const typeData = editData[editData.type] || {};
|
||||
const initialValues = {};
|
||||
const fields = getConfigFields(editData.type);
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = typeData[field.key] ?? field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
} else {
|
||||
// 新增时初始化默认值
|
||||
const fields = getConfigFields('basic');
|
||||
const initialValues = {};
|
||||
fields.forEach(field => {
|
||||
initialValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(initialValues);
|
||||
}
|
||||
}, [editData]);
|
||||
|
||||
// 处理类型切换(仅新增时可用)
|
||||
const handleTypeChange = (newType) => {
|
||||
if (isEdit) return; // 编辑时不可切换类型
|
||||
|
||||
setConfigType(newType);
|
||||
|
||||
// 清空类型特定字段,保留默认值
|
||||
const fields = getConfigFields(newType);
|
||||
const newValues = {};
|
||||
fields.forEach(field => {
|
||||
newValues[field.key] = field.default ?? '';
|
||||
});
|
||||
setFieldValues(newValues);
|
||||
};
|
||||
|
||||
// 处理字段值变化
|
||||
const handleFieldChange = (key, value) => {
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 切换密码显示/隐藏
|
||||
const togglePasswordVisibility = (key) => {
|
||||
setShowPasswords(prev => ({
|
||||
...prev,
|
||||
[key]: !prev[key]
|
||||
}));
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
// 构建类型特定数据
|
||||
const typeData = {};
|
||||
const fields = getConfigFields(configType);
|
||||
fields.forEach(field => {
|
||||
let value = fieldValues[field.key];
|
||||
// 数字类型转换
|
||||
if (field.type === 'number' && value !== '') {
|
||||
value = Number(value);
|
||||
}
|
||||
typeData[field.key] = value;
|
||||
});
|
||||
|
||||
const configData = {
|
||||
name: configName.trim(),
|
||||
type: configType,
|
||||
[configType]: typeData
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
api.admin.modelConfigs.update(editData.id, configData);
|
||||
} else {
|
||||
api.admin.modelConfigs.create(configData);
|
||||
}
|
||||
|
||||
onBack();
|
||||
};
|
||||
|
||||
// 获取当前类型的字段定义
|
||||
const currentFields = getConfigFields(configType);
|
||||
const configTypeList = getConfigTypeList();
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">{isEdit ? '编辑配置' : '新增配置'}</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{/* 基础信息 */}
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="请输入配置名称"
|
||||
value={configName}
|
||||
onChange={(e) => setConfigName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label required">配置类型</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={configType}
|
||||
onChange={(e) => handleTypeChange(e.target.value)}
|
||||
disabled={isEdit}
|
||||
>
|
||||
{configTypeList.map(type => (
|
||||
<option key={type.key} value={type.key}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isEdit && (
|
||||
<div style={{ fontSize: '12px', color: '#6B7280', marginTop: '4px' }}>
|
||||
配置类型不可修改
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 类型特定配置 */}
|
||||
<div style={{ marginTop: '24px', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '14px', fontWeight: 600, color: '#111827', marginBottom: '16px' }}>
|
||||
{MODEL_CONFIG_TYPES[configType]?.label} 配置
|
||||
</div>
|
||||
|
||||
{currentFields.map(field => (
|
||||
<div key={field.key} className="form-group">
|
||||
<label className={`form-label ${field.required ? 'required' : ''}`}>
|
||||
{field.label}
|
||||
</label>
|
||||
|
||||
{field.type === 'password' ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type={showPasswords[field.key] ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
style={{ paddingRight: '40px' }}
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePasswordVisibility(field.key)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '6px',
|
||||
cursor: 'pointer',
|
||||
color: '#6B7280'
|
||||
}}
|
||||
>
|
||||
{showPasswords[field.key] ? <FiEyeOff size={16} /> : <FiEye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
) : field.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={field.type === 'url' ? 'url' : 'text'}
|
||||
className="form-control"
|
||||
placeholder={`请输入${field.label}`}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<button className="btn" onClick={onBack}>取消</button>
|
||||
<button className="btn btn-primary" onClick={handleSave}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddModelConfigPage;
|
||||
149
src/pages/admin/ModelConfigsPage.jsx
Normal file
149
src/pages/admin/ModelConfigsPage.jsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useState } from 'react';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { api } from '../../services/api.js';
|
||||
import { MODEL_CONFIG_TYPES, getConfigSummary } from '../../data/configTypes.js';
|
||||
import Modal from '../../components/common/Modal.jsx';
|
||||
|
||||
function ModelConfigsPage({ onAdd, onEdit }) {
|
||||
const [configs, setConfigs] = useState(api.admin.modelConfigs.list());
|
||||
const [showSetActiveModal, setShowSetActiveModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState(null);
|
||||
|
||||
const handleSetActiveClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowSetActiveModal(true);
|
||||
};
|
||||
|
||||
const handleSetActiveConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
api.admin.modelConfigs.setActive(selectedConfig.id);
|
||||
setConfigs([...api.admin.modelConfigs.list()]);
|
||||
}
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (config) => {
|
||||
setSelectedConfig(config);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (selectedConfig) {
|
||||
api.admin.modelConfigs.delete(selectedConfig.id);
|
||||
setConfigs([...api.admin.modelConfigs.list()]);
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="model-configs-page">
|
||||
{/* 配置列表 */}
|
||||
<div className="card">
|
||||
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="card-title">配置列表</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={onAdd}>
|
||||
<FiPlus /> 新增配置
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="table-wrapper">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置名称</th>
|
||||
<th>配置类型</th>
|
||||
<th>关键信息</th>
|
||||
<th>状态</th>
|
||||
<th style={{ width: '200px' }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{configs.map(config => (
|
||||
<tr key={config.id} className={config.isActive ? 'active-row' : ''}>
|
||||
<td><strong>{config.name}</strong></td>
|
||||
<td>{MODEL_CONFIG_TYPES[config.type]?.label || config.type}</td>
|
||||
<td>{getConfigSummary(config)}</td>
|
||||
<td>
|
||||
{config.isActive ? (
|
||||
<span className="status status-running">生效中</span>
|
||||
) : (
|
||||
<span className="status status-stopped">未生效</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ width: '200px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{!config.isActive && (
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => handleSetActiveClick(config)}
|
||||
>
|
||||
设为默认
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="text-btn text-btn-primary"
|
||||
onClick={() => onEdit(config)}
|
||||
disabled={config.isActive}
|
||||
title={config.isActive ? '生效中的配置不可编辑' : ''}
|
||||
style={config.isActive ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="text-btn text-btn-danger"
|
||||
onClick={() => handleDeleteClick(config)}
|
||||
disabled={config.isActive}
|
||||
title={config.isActive ? '生效中的配置不可删除' : ''}
|
||||
style={config.isActive ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设为默认确认弹窗 */}
|
||||
<Modal
|
||||
visible={showSetActiveModal}
|
||||
title="确认切换默认配置"
|
||||
onConfirm={handleSetActiveConfirm}
|
||||
onCancel={() => {
|
||||
setShowSetActiveModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="确认切换"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定将"{selectedConfig?.name}"设为平台默认模型配置吗?</p>
|
||||
<p>切换后,原生效配置将变为备用状态。</p>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
<Modal
|
||||
visible={showDeleteModal}
|
||||
title="确认删除"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => {
|
||||
setShowDeleteModal(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
confirmText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>确定要删除配置"{selectedConfig?.name}"吗?</p>
|
||||
<p>此操作不可恢复。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModelConfigsPage;
|
||||
@@ -10,7 +10,7 @@ import { logs } from '../data/logs.js';
|
||||
import { mySkills, skillCategories, devDocs, developerOverview } from '../data/developerData.js';
|
||||
import { projectMembers } from '../data/members.js';
|
||||
import { scheduledTasks } from '../data/tasks.js';
|
||||
import { adminDepartments, adminUsers, adminProjects, adminOverview, adminLogs } from '../data/adminData.js';
|
||||
import { adminDepartments, adminUsers, adminProjects, adminOverview, adminLogs, modelConfigs } from '../data/adminData.js';
|
||||
|
||||
/**
|
||||
* 用户相关 API
|
||||
@@ -262,6 +262,97 @@ export const adminApi = {
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 模型配置相关 API
|
||||
*/
|
||||
modelConfigs: {
|
||||
/**
|
||||
* 获取所有模型配置
|
||||
* @returns {Array} 配置列表
|
||||
*/
|
||||
list: () => modelConfigs,
|
||||
|
||||
/**
|
||||
* 根据 ID 获取模型配置
|
||||
* @param {string} id - 配置 ID
|
||||
* @returns {Object|undefined} 配置对象
|
||||
*/
|
||||
getById: (id) => modelConfigs.find(c => c.id === id),
|
||||
|
||||
/**
|
||||
* 创建新配置
|
||||
* @param {Object} data - 配置数据
|
||||
* @returns {Object} 创建的配置
|
||||
*/
|
||||
create: (data) => {
|
||||
const newConfig = {
|
||||
...data,
|
||||
id: `cfg_${String(modelConfigs.length + 1).padStart(3, '0')}`,
|
||||
isActive: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
modelConfigs.push(newConfig);
|
||||
return newConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @param {string} id - 配置 ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Object|undefined} 更新后的配置
|
||||
*/
|
||||
update: (id, data) => {
|
||||
const index = modelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return undefined;
|
||||
modelConfigs[index] = {
|
||||
...modelConfigs[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return modelConfigs[index];
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
* @param {string} id - 配置 ID
|
||||
* @returns {boolean} 是否删除成功
|
||||
*/
|
||||
delete: (id) => {
|
||||
const index = modelConfigs.findIndex(c => c.id === id);
|
||||
if (index === -1) return false;
|
||||
modelConfigs.splice(index, 1);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前生效的配置
|
||||
* @returns {Object|undefined} 生效的配置
|
||||
*/
|
||||
getActive: () => modelConfigs.find(c => c.isActive),
|
||||
|
||||
/**
|
||||
* 设为默认配置
|
||||
* @param {string} id - 配置 ID
|
||||
* @returns {Object|undefined} 新生效的配置
|
||||
*/
|
||||
setActive: (id) => {
|
||||
const target = modelConfigs.find(c => c.id === id);
|
||||
if (!target) return undefined;
|
||||
|
||||
// 将所有配置设为非生效
|
||||
modelConfigs.forEach(c => {
|
||||
c.isActive = false;
|
||||
});
|
||||
|
||||
// 将目标设为生效
|
||||
target.isActive = true;
|
||||
target.updatedAt = new Date().toISOString();
|
||||
|
||||
return target;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2878,3 +2878,161 @@ input:checked + .slider:before {
|
||||
color: var(--color-danger);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
模型配置管理页面样式
|
||||
============================================ */
|
||||
|
||||
/* 配置列表页面基础样式 */
|
||||
.model-configs-page {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 新增/编辑配置页面 */
|
||||
.add-model-config-page {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.add-model-config-page .page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.add-model-config-page .page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-model-config-page .header-spacer {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E5E7EB;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.config-form .form-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.config-form .form-section:last-of-type {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-form .form-section .section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.config-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-form .form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.config-form .form-label.required::after {
|
||||
content: ' *';
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.config-form .form-input,
|
||||
.config-form .form-select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #D1D5DB;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #111827;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.config-form .form-input:focus,
|
||||
.config-form .form-select:focus {
|
||||
outline: none;
|
||||
border-color: #3B82F6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.config-form .form-input:disabled,
|
||||
.config-form .form-select:disabled {
|
||||
background: #F3F4F6;
|
||||
color: #6B7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.config-form .form-group.has-error .form-input,
|
||||
.config-form .form-group.has-error .form-select {
|
||||
border-color: #EF4444;
|
||||
}
|
||||
|
||||
.config-form .error-message {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #EF4444;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.config-form .help-text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* 密码输入框 */
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-input-wrapper .form-input {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.password-toggle-btn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
color: #6B7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.password-toggle-btn:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 表单操作按钮 */
|
||||
.config-form .form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user