From b00d75de8ad17db1d6778881b6891fad4b2ad574 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 26 Mar 2026 11:14:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8F=B0=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持配置类型注册表机制(basic、zhisuan) - 配置列表展示(生效中/未生效状态区分) - 新增/编辑配置表单,支持动态字段渲染 - 生效中配置不可编辑/删除限制 - 配置类型创建后不可修改 - 密钥掩码显示与显示/隐藏切换 - 操作二次确认弹窗(设为默认、删除) --- README.md | 17 +- openspec/config.yaml | 1 + openspec/specs/admin-model-config/spec.md | 115 +++++++++++ src/constants/pages.js | 2 + src/constants/storageKeys.js | 1 + src/data/adminData.js | 64 +++++++ src/data/configTypes.js | 93 +++++++++ src/pages/AdminPage.jsx | 27 ++- src/pages/admin/AddModelConfigPage.jsx | 221 ++++++++++++++++++++++ src/pages/admin/ModelConfigsPage.jsx | 149 +++++++++++++++ src/services/api.js | 93 ++++++++- src/styles/global.scss | 158 ++++++++++++++++ 12 files changed, 931 insertions(+), 10 deletions(-) create mode 100644 openspec/specs/admin-model-config/spec.md create mode 100644 src/data/configTypes.js create mode 100644 src/pages/admin/AddModelConfigPage.jsx create mode 100644 src/pages/admin/ModelConfigsPage.jsx diff --git a/README.md b/README.md index cf51744..01b7d2a 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ grandclaw-archtype/ │ │ ├── members.js # 成员数据 │ │ ├── skills.js # 技能数据 │ │ ├── tasks.js # 定时任务数据 -│ │ └── adminData.js # 管理台数据(部门/用户/项目/总览/日志) +│ │ ├── adminData.js # 管理台数据(部门/用户/项目/总览/日志/模型配置) +│ │ └── configTypes.js # 模型配置类型注册表 │ ├── pages/ # 页面组件 │ │ ├── HomePage.jsx # 首页 │ │ ├── LoginPage.jsx # 登录页面 @@ -89,7 +90,9 @@ grandclaw-archtype/ │ │ │ ├── AddUserPage.jsx # 新增/编辑用户 │ │ │ ├── AdminProjectsPage.jsx # 项目管理 │ │ │ ├── AddProjectPage.jsx # 新增/编辑项目 -│ │ │ └── AdminLogsPage.jsx # 全局日志查询 +│ │ │ ├── AdminLogsPage.jsx # 全局日志查询 +│ │ │ ├── ModelConfigsPage.jsx # 模型配置列表 +│ │ │ └── AddModelConfigPage.jsx # 新增/编辑模型配置 │ │ ├── console/ # 工作台子页面 │ │ │ ├── ChatPage.jsx # 聊天页面 │ │ │ ├── SkillsPage.jsx # 技能市场 @@ -178,10 +181,11 @@ pnpm build ### 4. 管理台(Admin) - **运营总览**:平台运营指标卡片(用户总数、部门数量、项目数量、今日调用)、异常/待办事项提醒、最近操作日志 -- **审核管理**:版本审核列表与详情、下架审核列表与详情(NEW) +- **审核管理**:版本审核列表与详情、下架审核列表与详情 - **部门管理**:部门列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认 - **用户管理**:用户列表,支持搜索筛选(关键词/部门/状态)、新增、编辑、启用/禁用、删除确认,角色区分(管理员/开发者/成员) - **项目管理**:项目列表,支持搜索筛选、新增、编辑、启用/禁用、删除确认 +- **模型配置**:管理平台的默认模型接入配置,支持多组配置(OpenAI兼容接口、智算管理平台等类型),可切换生效配置,生效中配置不可编辑/删除,配置类型创建后不可修改 - **日志查询**:全局系统日志查询,支持多维度筛选(关键词、用户、部门、类型、状态、时间范围) ### 5. 开发台(Developer) @@ -579,7 +583,7 @@ const members = api.members.list(); - `api.developer` - 开发台数据(总览、技能、分类、文档) - `api.members` - 项目成员 - `api.tasks` - 定时任务 -- `api.admin` - 管理台(总览、部门、用户、项目、全局日志) +- `api.admin` - 管理台(总览、部门、用户、项目、模型配置、全局日志) ## 数据模拟 @@ -591,7 +595,8 @@ const members = api.members.list(); - `developerData.js`:开发台数据,包含我的技能(含图标、版本审核状态、hasPendingReview标识)、技能分类、开发者总览、开发文档 - `logs.js`:操作日志数据(成功/失败/警告状态) - `tasks.js`:定时任务数据(包含任务配置和执行日志) -- `adminData.js`:管理台数据(部门列表、用户列表、项目列表、总览指标、全局日志、可选项数据) +- `adminData.js`:管理台数据(部门列表、用户列表、项目列表、模型配置列表、总览指标、全局日志、可选项数据) +- `configTypes.js`:模型配置类型注册表(OpenAI兼容接口、智算管理平台等类型定义) - `members.js`:项目成员数据 ## 构建和部署 @@ -674,4 +679,4 @@ export default defineConfig({ 审核审批流程的详细说明请查看:[docs/审核流程.md](docs/审核流程.md) -*最后更新:2026-03-21* +*最后更新:2026-03-26* diff --git a/openspec/config.yaml b/openspec/config.yaml index 7f6218a..7c2b47e 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -5,6 +5,7 @@ context: | - 纯前端展示原型项目(非功能原型),无后端交互,供内部开发人员参考UI界面使用,目标在于展示页面布局、样式和组件能力 - 允许轻量级交互展示(如表单验证、弹框),状态展示策略:不重叠的状态通过静态数据驱动展示,重叠/覆盖类状态(弹框、下拉、抽屉等)允许简单交互切换 - 示例数据应精心设计,展示不同的页面元素状态 + - 新增代码要遵循原有代码的设计风格和模式 - 不引入UI库,使用当前SCSS样式方案 - 使用pnpm作为包管理器,javascript作为开发语言,不引入typescript或eslint - 不构建测试,使用pnpm build验证打包即可,AI禁止运行pnpm dev(会挂起流程) diff --git a/openspec/specs/admin-model-config/spec.md b/openspec/specs/admin-model-config/spec.md new file mode 100644 index 0000000..82daddb --- /dev/null +++ b/openspec/specs/admin-model-config/spec.md @@ -0,0 +1,115 @@ +## Purpose + +管理台模型配置管理功能,支持管理员管理多组 AI 模型接入配置,并可选择其中一组作为平台默认配置生效。 + +## Requirements + +### Requirement: 配置列表展示 +系统 SHALL 在管理台展示模型配置列表,包括当前生效配置卡片和所有配置表格。 + +#### Scenario: 查看配置列表页 +- **WHEN** 管理员进入模型配置管理页面 +- **THEN** 系统展示当前生效配置卡片(包含名称、类型、关键信息) +- **AND** 系统展示配置列表表格(包含名称、类型、关键信息摘要、状态、操作按钮) + +#### Scenario: 区分配置状态 +- **WHEN** 配置列表中有多个配置 +- **THEN** 当前生效配置在表格中显示"生效中"状态标签 +- **AND** 其他配置显示"未生效"状态标签 + +### Requirement: 设为默认配置 +系统 SHALL 允许管理员将非生效配置设为平台默认配置,操作需二次确认。 + +#### Scenario: 成功切换默认配置 +- **WHEN** 管理员点击某未生效配置的"设为默认"按钮 +- **THEN** 系统显示二次确认弹窗 +- **AND** 管理员确认后,该配置变为生效状态 +- **AND** 原生效配置变为未生效状态 + +#### Scenario: 取消切换默认配置 +- **WHEN** 管理员点击"设为默认"按钮后 +- **AND** 在二次确认弹窗中点击取消 +- **THEN** 系统关闭弹窗,配置状态保持不变 + +### Requirement: 删除配置 +系统 SHALL 允许管理员删除非生效的配置,删除前需二次确认。 + +#### Scenario: 成功删除配置 +- **WHEN** 管理员点击某未生效配置的"删除"按钮 +- **THEN** 系统显示二次确认弹窗 +- **AND** 管理员确认后,该配置从列表中移除 + +#### Scenario: 无法删除生效配置 +- **WHEN** 某配置是当前生效配置 +- **THEN** 其删除按钮被禁用 +- **AND** 鼠标悬停时显示提示"生效中的配置不可删除" + +### Requirement: 新增配置 +系统 SHALL 提供独立页面供管理员新增模型配置,支持选择配置类型并填写对应字段。 + +#### Scenario: 成功新增基础类型配置 +- **WHEN** 管理员点击"新增配置"按钮进入新增页面 +- **AND** 选择配置类型为"OpenAI 兼容接口" +- **AND** 填写配置名称、API 地址、API 密钥、模型名称 +- **AND** 填写常用参数(Temperature、Max Tokens、Top P) +- **AND** 点击保存 +- **THEN** 系统创建新配置并返回列表页 +- **AND** 新配置默认状态为"未生效" + +#### Scenario: 成功新增智算平台配置 +- **WHEN** 管理员在新增页面选择配置类型为"智算管理平台" +- **THEN** 表单动态切换为智算平台字段(API 地址、App ID、App Secret) +- **AND** 管理员填写完成后保存 +- **THEN** 系统创建新配置并返回列表页 + +#### Scenario: 新增时切换类型清空字段 +- **WHEN** 管理员在新增页面已填写某类型的部分字段 +- **AND** 切换配置类型为另一种类型 +- **THEN** 系统清空已填写的类型特定字段 +- **AND** 保留通用字段(配置名称) + +#### Scenario: 新增配置表单验证失败 +- **WHEN** 管理员提交表单时未填写必填项 +- **THEN** 系统高亮显示未填写的必填字段 +- **AND** 阻止表单提交 + +### Requirement: 编辑配置 +系统 SHALL 提供独立页面供管理员编辑现有配置,仅允许编辑非生效配置。 + +#### Scenario: 成功编辑配置 +- **WHEN** 管理员点击某未生效配置的"编辑"按钮 +- **THEN** 系统进入编辑页面,预填充该配置的当前值 +- **AND** 配置类型字段显示为只读 +- **AND** 管理员修改其他字段后保存 +- **THEN** 系统更新配置并返回列表页 + +#### Scenario: 无法编辑生效配置 +- **WHEN** 某配置是当前生效配置 +- **THEN** 其编辑按钮被禁用 +- **AND** 鼠标悬停时显示提示"生效中的配置不可编辑" + +#### Scenario: 编辑时类型字段只读 +- **WHEN** 管理员在编辑配置页面 +- **THEN** 配置类型选择器被禁用 +- **AND** 显示提示"配置类型不可修改" + +### Requirement: 配置类型注册表 +系统 SHALL 使用可扩展的注册表机制定义配置类型,每种类型包含独立的字段定义和验证规则。 + +#### Scenario: 扩展新配置类型 +- **WHEN** 需要添加新的配置类型(如"阿里云百炼") +- **THEN** 开发者只需在注册表中添加类型定义 +- **AND** 无需修改配置表单页面的核心逻辑 +- **AND** 新类型自动在类型选择器和表单中生效 + +### Requirement: 密钥字段掩码显示 +系统 SHALL 对所有敏感字段(API 密钥、App Secret 等)使用掩码显示。 + +#### Scenario: 列表页密钥掩码 +- **WHEN** 配置列表展示配置信息 +- **THEN** API 密钥、App Secret 字段显示为掩码格式(如"sk-****xxxx") + +#### Scenario: 表单页密钥掩码 +- **WHEN** 编辑配置时表单显示已保存的密钥 +- **THEN** 密钥输入框显示为掩码格式 +- **AND** 管理员可点击显示/隐藏按钮切换明文/密文 diff --git a/src/constants/pages.js b/src/constants/pages.js index 5e7bfc8..fbc2cd4 100644 --- a/src/constants/pages.js +++ b/src/constants/pages.js @@ -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 }, }; /** diff --git a/src/constants/storageKeys.js b/src/constants/storageKeys.js index c73bd76..2aa4b1b 100644 --- a/src/constants/storageKeys.js +++ b/src/constants/storageKeys.js @@ -13,6 +13,7 @@ export const CONSOLE_KEYS = { */ export const ADMIN_KEYS = { CURRENT_PAGE: 'admin_currentPage', + MODEL_CONFIG_EDIT_DATA: 'admin_modelConfigEditData', }; /** diff --git a/src/data/adminData.js b/src/data/adminData.js index 7a0854d..af00a9f 100644 --- a/src/data/adminData.js +++ b/src/data/adminData.js @@ -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' + } + } +]; diff --git a/src/data/configTypes.js b/src/data/configTypes.js new file mode 100644 index 0000000..0b1a22e --- /dev/null +++ b/src/data/configTypes.js @@ -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); +}; diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx index 3a9beed..9de2763 100644 --- a/src/pages/AdminPage.jsx +++ b/src/pages/AdminPage.jsx @@ -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 navigateTo('addModelConfig')} + onEdit={(config) => navigateTo('addModelConfig', config)} + />; + case 'addModelConfig': + return navigateTo('modelConfigs')} + editData={editData} + />; default: return
Page not found
; } }; 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" /> + } + label="模型配置" + active={currentPage === 'modelConfigs' || currentPage === 'addModelConfig'} + onClick={() => navigateTo('modelConfigs')} + itemClassName="admin-nav-item" + iconClassName="admin-nav-icon" + textClassName="admin-nav-text" + /> {}} diff --git a/src/pages/admin/AddModelConfigPage.jsx b/src/pages/admin/AddModelConfigPage.jsx new file mode 100644 index 0000000..cf8b2a5 --- /dev/null +++ b/src/pages/admin/AddModelConfigPage.jsx @@ -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 ( +
+
+
{isEdit ? '编辑配置' : '新增配置'}
+
+
+ {/* 基础信息 */} +
+ + setConfigName(e.target.value)} + /> +
+ +
+ + + {isEdit && ( +
+ 配置类型不可修改 +
+ )} +
+ + {/* 类型特定配置 */} +
+
+ {MODEL_CONFIG_TYPES[configType]?.label} 配置 +
+ + {currentFields.map(field => ( +
+ + + {field.type === 'password' ? ( +
+ handleFieldChange(field.key, e.target.value)} + /> + +
+ ) : field.type === 'number' ? ( + handleFieldChange(field.key, e.target.value)} + min={field.min} + max={field.max} + step={field.step} + /> + ) : ( + handleFieldChange(field.key, e.target.value)} + /> + )} +
+ ))} +
+ + {/* 操作按钮 */} +
+ + +
+
+
+ ); +} + +export default AddModelConfigPage; diff --git a/src/pages/admin/ModelConfigsPage.jsx b/src/pages/admin/ModelConfigsPage.jsx new file mode 100644 index 0000000..7232ebc --- /dev/null +++ b/src/pages/admin/ModelConfigsPage.jsx @@ -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 ( +
+ {/* 配置列表 */} +
+
+
配置列表
+ +
+
+
+ + + + + + + + + + + + {configs.map(config => ( + + + + + + + + ))} + +
配置名称配置类型关键信息状态操作
{config.name}{MODEL_CONFIG_TYPES[config.type]?.label || config.type}{getConfigSummary(config)} + {config.isActive ? ( + 生效中 + ) : ( + 未生效 + )} + +
+ {!config.isActive && ( + + )} + + +
+
+
+
+
+ + {/* 设为默认确认弹窗 */} + { + setShowSetActiveModal(false); + setSelectedConfig(null); + }} + confirmText="确认切换" + cancelText="取消" + > +

确定将"{selectedConfig?.name}"设为平台默认模型配置吗?

+

切换后,原生效配置将变为备用状态。

+
+ + {/* 删除确认弹窗 */} + { + setShowDeleteModal(false); + setSelectedConfig(null); + }} + confirmText="删除" + cancelText="取消" + > +

确定要删除配置"{selectedConfig?.name}"吗?

+

此操作不可恢复。

+
+
+ ); +} + +export default ModelConfigsPage; diff --git a/src/services/api.js b/src/services/api.js index 3401d23..e6b3a53 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -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; + }, + }, }; /** diff --git a/src/styles/global.scss b/src/styles/global.scss index 04b8aa7..2c99fa0 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -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; +}