Compare commits

..

6 Commits

Author SHA1 Message Date
a67b0262d4 feat: 移除管理台模型配置列表关键信息列
简化界面展示,提升安全性:
- 移除配置列表表格中的关键信息列
- 移除生效配置卡片中的关键信息展示
- 更新规格文档,移除列表页密钥掩码要求
2026-03-30 09:50:21 +08:00
9d30c5e21f fix: 技能详情页返回按钮样式统一为 page-back-btn 2026-03-28 18:33:05 +08:00
f46f26fe27 feat: 工作台对话列表添加删除功能
- 对话卡片 Hover 时显示删除按钮
- 点击删除按钮显示确认弹窗
- 删除按钮垂直居中对齐
2026-03-28 18:04:21 +08:00
fb9833663c fix: Modal 弹窗高度约束,防止内容溢出视窗
- 添加 max-height 限制,确保弹窗始终在视窗内
- header/footer 固定不压缩,body 内容可滚动
- 兼容 iOS Safari 的 dvh 单位
2026-03-28 17:35:54 +08:00
ae75c82505 refactor: 简化 dailyVersioning 插件实现 2026-03-28 13:03:50 +08:00
46016b0786 feat: 构建输出按日期命名
添加 dailyVersioning Vite 插件,将构建输出从 index.html 改为 grandclaw-archtype-YYYYMMDD.html 格式
2026-03-28 12:59:45 +08:00
10 changed files with 224 additions and 18 deletions

View File

@@ -9,8 +9,8 @@
#### Scenario: 查看配置列表页
- **WHEN** 管理员进入模型配置管理页面
- **THEN** 系统展示当前生效配置卡片(包含名称、类型、关键信息
- **AND** 系统展示配置列表表格(包含名称、类型、关键信息摘要、状态、操作按钮)
- **THEN** 系统展示当前生效配置卡片(包含名称、类型)
- **AND** 系统展示配置列表表格(包含名称、类型、状态、操作按钮)
#### Scenario: 区分配置状态
- **WHEN** 配置列表中有多个配置
@@ -103,11 +103,7 @@
- **AND** 新类型自动在类型选择器和表单中生效
### Requirement: 密钥字段掩码显示
系统 SHALL 对所有敏感字段API 密钥、App Secret 等)使用掩码显示。
#### Scenario: 列表页密钥掩码
- **WHEN** 配置列表展示配置信息
- **THEN** API 密钥、App Secret 字段显示为掩码格式(如"sk-****xxxx"
系统 SHALL 在配置表单页对所有敏感字段API 密钥、App Secret 等)使用掩码显示。
#### Scenario: 表单页密钥掩码
- **WHEN** 编辑配置时表单显示已保存的密钥

View File

@@ -0,0 +1,26 @@
### Requirement: 构建输出文件命名
系统 SHALL 在执行 `pnpm build` 时,将输出文件命名为 `grandclaw-archtype-YYYYMMDD.html` 格式,其中 YYYYMMDD 为构建当天的本地日期。
#### Scenario: 标准构建输出
- **WHEN** 执行 `pnpm build` 命令
- **THEN** dist 目录中生成文件名为 `grandclaw-archtype-YYYYMMDD.html` 的单文件 HTML
- **AND** 文件名为当天本地日期(如 2026年3月28日 构建则输出 `grandclaw-archtype-20260328.html`
#### Scenario: 同日多次构建
- **WHEN** 同一天内多次执行 `pnpm build`
- **THEN** 每次构建输出覆盖同一天的文件
- **AND** dist 目录中只存在一个文件
#### Scenario: 跨天构建
- **WHEN** 不同日期执行 `pnpm build`
- **THEN** 每次构建生成当天日期命名的文件
- **AND** 历史文件被清空Vite 默认行为emptyOutDir: true
### Requirement: 零额外依赖
构建输出命名功能 SHALL 通过自定义 Vite 插件实现,不引入额外的 npm 依赖。
#### Scenario: 依赖检查
- **WHEN** 查看 package.json 的 dependencies 和 devDependencies
- **THEN** 不存在为文件命名功能新增的依赖项

View File

@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: 对话列表项支持删除操作
对话列表中的每一项 SHALL 提供删除功能,允许用户移除不需要的对话记录。
#### Scenario: Hover 显示删除按钮
- **WHEN** 用户将鼠标悬停在对话卡片上
- **THEN** 系统 SHALL 在卡片右侧显示删除图标按钮
#### Scenario: 默认隐藏删除按钮
- **WHEN** 用户未将鼠标悬停在对话卡片上
- **THEN** 系统 SHALL 隐藏删除按钮,保持界面简洁
### Requirement: 删除操作需确认
系统 SHALL 在执行删除前显示确认弹窗,防止用户误操作。
#### Scenario: 点击删除按钮显示确认弹窗
- **WHEN** 用户点击删除按钮
- **THEN** 系统 SHALL 显示确认弹窗,包含对话标题和确认/取消按钮
#### Scenario: 确认删除
- **WHEN** 用户在确认弹窗中点击"确定"按钮
- **THEN** 系统 SHALL 关闭弹窗并模拟删除操作
#### Scenario: 取消删除
- **WHEN** 用户在确认弹窗中点击"取消"按钮或点击遮罩层
- **THEN** 系统 SHALL 关闭弹窗,不执行删除操作
### Requirement: 删除按钮样式符合设计规范
删除按钮的样式 SHALL 符合项目现有的设计规范。
#### Scenario: 删除按钮默认样式
- **WHEN** 删除按钮显示时
- **THEN** 按钮 SHALL 使用灰色图标,不干扰主要内容
#### Scenario: 删除按钮 Hover 样式
- **WHEN** 用户将鼠标悬停在删除按钮上
- **THEN** 按钮 SHALL 变为红色,提示危险操作

View File

@@ -0,0 +1,49 @@
## Purpose
定义 Modal 弹窗的高度约束机制,确保弹窗在各种视窗尺寸下完整显示,并提供良好的内容滚动体验。
## Requirements
### Requirement: Modal 最大高度限制
Modal 弹窗 SHALL 设置最大高度,确保弹窗始终在视窗内完整显示。
#### Scenario: 视窗高度充足
- **WHEN** 浏览器视窗高度大于弹窗内容高度
- **THEN** 弹窗高度自适应内容,不显示滚动条
#### Scenario: 视窗高度不足
- **WHEN** 浏览器视窗高度小于弹窗内容高度
- **THEN** 弹窗最大高度为 `calc(100dvh - 48px)`(兼容旧浏览器使用 `calc(100vh - 48px)` fallback
- **THEN** 弹窗上下各预留 24px 边距
### Requirement: Modal 标题栏固定可见
Modal 标题栏(`.modal-header`SHALL 始终可见,不被内容挤压或遮挡。
#### Scenario: 内容超出滚动
- **WHEN** Modal body 内容超出高度限制
- **THEN** 标题栏固定在弹窗顶部,高度不变
- **THEN** 标题栏不参与滚动
### Requirement: Modal 底部栏固定可见
Modal 底部栏(`.modal-footer`如存在SHALL 始终可见,不被内容挤压或遮挡。
#### Scenario: 内容超出滚动且有 footer
- **WHEN** Modal body 内容超出高度限制且存在 footer
- **THEN** footer 固定在弹窗底部,高度不变
- **THEN** footer 不参与滚动
### Requirement: Modal body 内容可滚动
Modal body`.modal-body`SHALL 在内容超出时支持滚动。
#### Scenario: 内容超出时滚动
- **WHEN** Modal body 内容高度超过可用空间
- **THEN** body 显示纵向滚动条
- **THEN** 用户可滚动查看全部内容
#### Scenario: 内容未超出时不滚动
- **WHEN** Modal body 内容高度未超过可用空间
- **THEN** body 不显示滚动条

View File

@@ -1,25 +1,40 @@
import { useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { FiPlus, FiClock, FiList, FiUsers, FiBox } from 'react-icons/fi';
import { FiTrash2 } from 'react-icons/fi';
import { FaPuzzlePiece } from 'react-icons/fa';
import Layout from '../Layout.jsx';
import SidebarNavItem from './SidebarNavItem.jsx';
import Modal from '../common/Modal.jsx';
import api from '../../services/api.js';
function ConsoleLayout() {
const location = useLocation();
const navigate = useNavigate();
const [deleteTarget, setDeleteTarget] = useState(null);
// 从 URL 提取当前 scene
const sceneMatch = location.pathname.match(/\/console\/chat\/(.+)$/);
const currentScene = sceneMatch ? sceneMatch[1] : null;
// 判断是否在 chat 页面(需要全宽布局)
const isChatPage = location.pathname === '/console/chat' ||
location.pathname.startsWith('/console/chat/');
// sidebar 高亮判断
const isPathActive = (basePath) => location.pathname.startsWith(basePath);
const handleDeleteClick = (e, conv) => {
e.stopPropagation();
setDeleteTarget(conv);
};
const handleDeleteConfirm = () => {
console.log('删除对话:', deleteTarget?.id);
setDeleteTarget(null);
};
const handleDeleteCancel = () => {
setDeleteTarget(null);
};
const sidebar = (
<>
<div className="chat-sidebar-header">
@@ -35,10 +50,21 @@ function ConsoleLayout() {
<div
key={conv.id}
className={`conversation-item ${conv.scene === currentScene ? 'active' : ''}`}
onClick={() => navigate(`/console/chat/${conv.scene}`)}
>
<div className="conversation-title">{conv.title}</div>
<div className="conversation-time">{conv.time}</div>
<div
className="conversation-item__content"
onClick={() => navigate(`/console/chat/${conv.scene}`)}
>
<div className="conversation-title">{conv.title}</div>
<div className="conversation-time">{conv.time}</div>
</div>
<button
className="conversation-item__delete"
onClick={(e) => handleDeleteClick(e, conv)}
title="删除对话"
>
<FiTrash2 size={14} />
</button>
</div>
))}
</div>
@@ -82,6 +108,14 @@ function ConsoleLayout() {
onClick={() => navigate('/console/projects')}
/>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
>
确定要删除对话{deleteTarget?.title}
</Modal>
</>
);

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FiPlus } from 'react-icons/fi';
import { api } from '../../services/api.js';
import { MODEL_CONFIG_TYPES, getConfigSummary } from '../../data/configTypes.js';
import { MODEL_CONFIG_TYPES } from '../../data/configTypes.js';
import Modal from '../../components/common/Modal.jsx';
function ModelConfigsPage() {
@@ -57,7 +57,6 @@ function ModelConfigsPage() {
<tr>
<th>配置名称</th>
<th>配置类型</th>
<th>关键信息</th>
<th>状态</th>
<th className="col-actions">操作</th>
</tr>
@@ -67,7 +66,6 @@ function ModelConfigsPage() {
<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>

View File

@@ -15,7 +15,7 @@ function SkillDetailPage() {
return (
<>
<div className="dev-back-btn" onClick={() => navigate('/console/skills')} style={{ marginBottom: '16px' }}>
<div className="page-back-btn" onClick={() => navigate('/console/skills')}>
<FiChevronLeft /> 返回技能市场
</div>
{cv ? (

View File

@@ -25,6 +25,10 @@
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.16);
width: 420px;
max-width: 90vw;
max-height: calc(100vh - 48px);
max-height: calc(100dvh - 48px);
display: flex;
flex-direction: column;
animation: modal-in 0.2s ease-out;
}
@@ -47,6 +51,7 @@
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border-2);
flex-shrink: 0;
}
.modal-title {
@@ -79,6 +84,9 @@
font-size: $font-size-base;
color: var(--color-text-2);
line-height: 1.6;
flex: 1;
min-height: 0;
overflow-y: auto;
}
// Element: footer
@@ -89,4 +97,5 @@
gap: 8px;
padding: 12px 20px;
border-top: 1px solid var(--color-border-2);
flex-shrink: 0;
}

View File

@@ -143,6 +143,9 @@
}
.conversation-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-radius: var(--radius-md);
cursor: pointer;
@@ -153,6 +156,10 @@
&:hover {
background: var(--color-bg-1);
border-color: var(--color-border-2);
.conversation-item__delete {
opacity: 1;
}
}
&.active {
@@ -162,6 +169,30 @@
}
}
.conversation-item__content {
flex: 1;
min-width: 0;
}
.conversation-item__delete {
flex-shrink: 0;
padding: 4px;
margin-left: 8px;
background: transparent;
border: none;
color: var(--color-text-3);
cursor: pointer;
opacity: 0;
transition: all var(--transition);
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: var(--color-danger);
}
}
.conversation-title {
font-size: 14px;
margin-bottom: 4px;

View File

@@ -1,9 +1,24 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteSingleFile } from 'vite-plugin-singlefile'
import { renameSync } from 'node:fs'
import { join } from 'node:path'
function dailyVersioning() {
return {
name: 'daily-versioning',
enforce: 'post',
writeBundle(options, bundle) {
if (!bundle['index.html']) return
const dir = options.dir || 'dist'
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')
renameSync(join(dir, 'index.html'), join(dir, `grandclaw-archtype-${date}.html`))
}
}
}
// https://vite.dev/config/
export default defineConfig({
base: './',
plugins: [react(), viteSingleFile()],
plugins: [react(), viteSingleFile(), dailyVersioning()],
})