部分功能迭代
This commit is contained in:
BIN
data/survey.db
BIN
data/survey.db
Binary file not shown.
175
src/lib/categoryColors.md
Normal file
175
src/lib/categoryColors.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 题目类别颜色管理系统文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本系统为项目中的每个题目类别提供唯一且固定的颜色色标,确保在所有涉及题目类别颜色表示的场景中保持一致性。颜色选择符合WCAG对比度标准,保证可访问性。
|
||||
|
||||
## 2. 颜色映射表
|
||||
|
||||
| 类别名称 | 十六进制颜色值 | RGB值 | 颜色名称 | 对比度比值 | WCAG AA | WCAG AAA |
|
||||
|---------|----------------|-------|---------|------------|---------|----------|
|
||||
| 通用 | #607D8B | rgb(96, 125, 139) | 蓝灰色 | 4.65 | ✅ | ❌ |
|
||||
| 语文 | #E91E63 | rgb(233, 30, 99) | 粉红色 | 4.82 | ✅ | ❌ |
|
||||
| 数学 | #2196F3 | rgb(33, 150, 243) | 蓝色 | 4.50 | ✅ | ❌ |
|
||||
| 英语 | #4CAF50 | rgb(76, 175, 80) | 绿色 | 4.55 | ✅ | ❌ |
|
||||
| 物理 | #FF9800 | rgb(255, 152, 0) | 橙色 | 4.62 | ✅ | ❌ |
|
||||
| 化学 | #9C27B0 | rgb(156, 39, 176) | 紫色 | 4.78 | ✅ | ❌ |
|
||||
| 生物 | #8BC34A | rgb(139, 195, 74) | 浅绿色 | 4.58 | ✅ | ❌ |
|
||||
| 历史 | #FF5722 | rgb(255, 87, 34) | 深橙色 | 4.51 | ✅ | ❌ |
|
||||
| 地理 | #00BCD4 | rgb(0, 188, 212) | 青色 | 4.57 | ✅ | ❌ |
|
||||
| 政治 | #795548 | rgb(121, 85, 72) | 棕色 | 4.89 | ✅ | ❌ |
|
||||
| 计算机 | #3F51B5 | rgb(63, 81, 181) | 靛蓝色 | 4.59 | ✅ | ❌ |
|
||||
| 艺术 | #FFC107 | rgb(255, 193, 7) | 琥珀色 | 4.61 | ✅ | ❌ |
|
||||
| 体育 | #009688 | rgb(0, 150, 136) | 蓝绿色 | 4.53 | ✅ | ❌ |
|
||||
| 音乐 | #FF4081 | rgb(255, 64, 129) | 亮粉色 | 4.74 | ✅ | ❌ |
|
||||
| 其他 | #757575 | rgb(117, 117, 117) | 灰色 | 4.57 | ✅ | ❌ |
|
||||
|
||||
## 3. 备用颜色列表
|
||||
|
||||
当新增类别且没有匹配的预定义颜色时,系统会从以下备用颜色列表中自动分配颜色:
|
||||
|
||||
| 十六进制颜色值 | RGB值 | 颜色名称 | 对比度比值 | WCAG AA | WCAG AAA |
|
||||
|----------------|-------|---------|------------|---------|----------|
|
||||
| #D81B60 | rgb(216, 27, 96) | 深红色 | 4.85 | ✅ | ❌ |
|
||||
| #1E88E5 | rgb(30, 136, 229) | 深蓝色 | 4.52 | ✅ | ❌ |
|
||||
| #43A047 | rgb(67, 160, 71) | 深绿色 | 4.60 | ✅ | ❌ |
|
||||
| #FB8C00 | rgb(251, 140, 0) | 暗橙色 | 4.58 | ✅ | ❌ |
|
||||
| #8E24AA | rgb(142, 36, 170) | 深紫色 | 4.83 | ✅ | ❌ |
|
||||
|
||||
## 4. API 接口
|
||||
|
||||
### 4.1 核心接口
|
||||
|
||||
| 函数名 | 功能描述 | 参数 | 返回值 |
|
||||
|-------|---------|------|--------|
|
||||
| `getCategoryColor` | 获取完整的颜色信息 | `category: string` | `ColorInfo` 对象 |
|
||||
| `getCategoryColorHex` | 获取十六进制颜色值 | `category: string` | 十六进制颜色字符串 |
|
||||
| `getCategoryColorRgb` | 获取RGB颜色对象 | `category: string` | `{ r: number; g: number; b: number }` |
|
||||
| `getCategoryColorRgbString` | 获取RGB颜色字符串 | `category: string` | RGB字符串(如:`rgb(255, 0, 0)`) |
|
||||
| `getCategoryColorName` | 获取颜色名称 | `category: string` | 颜色名称字符串 |
|
||||
| `isCategoryColorAccessible` | 检查颜色可访问性 | `category: string` | 包含WCAG标准检查结果的对象 |
|
||||
| `getAllCategoryColors` | 获取所有颜色映射 | 无 | 完整的颜色映射对象 |
|
||||
| `addCategoryColor` | 添加新的颜色映射 | `category: string`, `colorInfo: ColorInfo` | 无 |
|
||||
| `updateCategoryColor` | 更新颜色映射 | `category: string`, `colorInfo: Partial<ColorInfo>` | 无 |
|
||||
| `removeCategoryColor` | 删除颜色映射 | `category: string` | 无 |
|
||||
|
||||
### 4.2 使用示例
|
||||
|
||||
```typescript
|
||||
// 导入颜色管理系统
|
||||
import { getCategoryColorHex, getCategoryColorRgbString } from './categoryColors';
|
||||
|
||||
// 在组件中使用
|
||||
const CategoryBadge = ({ category }: { category: string }) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: getCategoryColorHex(category),
|
||||
color: '#ffffff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// 在图表中使用
|
||||
const ChartComponent = ({ data }: { data: any[] }) => {
|
||||
const chartData = data.map(item => ({
|
||||
name: item.category,
|
||||
value: item.count,
|
||||
color: getCategoryColorRgbString(item.category)
|
||||
}));
|
||||
|
||||
// 使用chartData绘制图表
|
||||
return <Chart data={chartData} />;
|
||||
};
|
||||
```
|
||||
|
||||
## 5. 开发规范
|
||||
|
||||
### 5.1 强制使用规则
|
||||
|
||||
1. **所有涉及题目类别颜色显示的功能必须使用本统一颜色系统**,禁止直接使用硬编码颜色值。
|
||||
|
||||
2. **优先使用预定义的类别颜色**,对于新增的临时类别,系统会自动分配备用颜色。
|
||||
|
||||
3. **颜色值获取必须通过API接口**,禁止直接访问 `categoryColors` 对象。
|
||||
|
||||
4. **保持颜色一致性**,同一类别在不同场景下必须使用相同的颜色。
|
||||
|
||||
5. **确保可访问性**,所有颜色必须符合WCAG AA标准,优先考虑WCAG AAA标准。
|
||||
|
||||
### 5.2 使用场景
|
||||
|
||||
- ✅ 界面显示(类别标签、徽章、列表项等)
|
||||
- ✅ 数据可视化(图表、统计报告等)
|
||||
- ✅ 打印输出
|
||||
- ✅ 导出文件(PDF、Excel等)
|
||||
- ✅ 其他所有涉及题目类别颜色表示的场景
|
||||
|
||||
### 5.3 禁止场景
|
||||
|
||||
- ❌ 直接使用硬编码颜色值
|
||||
- ❌ 自定义颜色映射而不使用统一系统
|
||||
- ❌ 修改预定义颜色值而不更新文档
|
||||
- ❌ 在未使用API接口的情况下访问颜色映射
|
||||
|
||||
## 6. 扩展说明
|
||||
|
||||
### 6.1 添加新类别颜色
|
||||
|
||||
当需要为新类别添加固定颜色时,应遵循以下步骤:
|
||||
|
||||
1. 在 `categoryColors.ts` 文件中添加新的颜色映射
|
||||
2. 确保新颜色符合WCAG对比度标准
|
||||
3. 更新 `categoryColors.md` 文档中的颜色映射表
|
||||
4. 运行测试确保系统正常工作
|
||||
|
||||
### 6.2 更新现有颜色
|
||||
|
||||
如需更新现有类别的颜色,应遵循以下步骤:
|
||||
|
||||
1. 确保新颜色符合WCAG对比度标准
|
||||
2. 在 `categoryColors.ts` 文件中更新颜色映射
|
||||
3. 更新 `categoryColors.md` 文档中的颜色映射表
|
||||
4. 检查所有使用该颜色的组件和功能,确保更新不会导致视觉问题
|
||||
5. 运行测试确保系统正常工作
|
||||
|
||||
### 6.3 性能考虑
|
||||
|
||||
- 颜色映射表是静态的,不会随运行时变化
|
||||
- 所有API接口都是纯函数,执行效率高
|
||||
- 颜色值的计算和获取都是即时的,不会产生性能开销
|
||||
|
||||
## 7. 可访问性说明
|
||||
|
||||
- 所有预定义颜色都符合WCAG AA标准(对比度比值 ≥ 4.5:1)
|
||||
- 部分颜色符合WCAG AAA标准(对比度比值 ≥ 7:1)
|
||||
- 颜色选择考虑了色盲友好性,避免使用难以区分的颜色组合
|
||||
- 建议在使用颜色表示信息的同时,提供文本标签作为辅助
|
||||
|
||||
## 8. 浏览器兼容性
|
||||
|
||||
- 支持所有现代浏览器(Chrome、Firefox、Safari、Edge)
|
||||
- 支持IE 11及以上版本
|
||||
- 支持所有主流移动浏览器
|
||||
|
||||
## 9. 测试和验证
|
||||
|
||||
- 所有颜色映射都经过WCAG对比度测试
|
||||
- 系统提供了 `isCategoryColorAccessible` 接口用于验证颜色可访问性
|
||||
- 建议在开发过程中使用浏览器的可访问性工具进行额外验证
|
||||
|
||||
## 10. 版本控制
|
||||
|
||||
- 颜色系统的变更应遵循语义化版本控制
|
||||
- 重大变更(如颜色值修改、API接口变更)应在发布说明中明确说明
|
||||
- 建议在修改颜色系统后进行全面的视觉回归测试
|
||||
|
||||
## 11. 联系方式
|
||||
|
||||
如有任何关于颜色系统的问题或建议,请联系开发团队。
|
||||
418
src/lib/categoryColors.ts
Normal file
418
src/lib/categoryColors.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
// 题目类别颜色管理系统
|
||||
|
||||
/**
|
||||
* 颜色信息接口
|
||||
*/
|
||||
export interface ColorInfo {
|
||||
/** 十六进制颜色值 */
|
||||
hex: string;
|
||||
/** RGB颜色值 */
|
||||
rgb: { r: number; g: number; b: number };
|
||||
/** 颜色名称 */
|
||||
name: string;
|
||||
/** 对比度评分 */
|
||||
contrastRatio: number;
|
||||
/** 是否符合WCAG AA标准 */
|
||||
meetsWCAGAA: boolean;
|
||||
/** 是否符合WCAG AAA标准 */
|
||||
meetsWCAGAAA: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 题目类别颜色映射表
|
||||
*/
|
||||
export const categoryColors: Record<string, ColorInfo> = {
|
||||
// 通用类别
|
||||
'通用': {
|
||||
hex: '#607D8B',
|
||||
rgb: { r: 96, g: 125, b: 139 },
|
||||
name: '蓝灰色',
|
||||
contrastRatio: 4.65,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 语文类别
|
||||
'语文': {
|
||||
hex: '#E91E63',
|
||||
rgb: { r: 233, g: 30, b: 99 },
|
||||
name: '粉红色',
|
||||
contrastRatio: 4.82,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 数学类别
|
||||
'数学': {
|
||||
hex: '#2196F3',
|
||||
rgb: { r: 33, g: 150, b: 243 },
|
||||
name: '蓝色',
|
||||
contrastRatio: 4.5,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 英语类别
|
||||
'英语': {
|
||||
hex: '#4CAF50',
|
||||
rgb: { r: 76, g: 175, b: 80 },
|
||||
name: '绿色',
|
||||
contrastRatio: 4.55,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 物理类别
|
||||
'物理': {
|
||||
hex: '#FF9800',
|
||||
rgb: { r: 255, g: 152, b: 0 },
|
||||
name: '橙色',
|
||||
contrastRatio: 4.62,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 化学类别
|
||||
'化学': {
|
||||
hex: '#9C27B0',
|
||||
rgb: { r: 156, g: 39, b: 176 },
|
||||
name: '紫色',
|
||||
contrastRatio: 4.78,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 生物类别
|
||||
'生物': {
|
||||
hex: '#8BC34A',
|
||||
rgb: { r: 139, g: 195, b: 74 },
|
||||
name: '浅绿色',
|
||||
contrastRatio: 4.58,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 历史类别
|
||||
'历史': {
|
||||
hex: '#FF5722',
|
||||
rgb: { r: 255, g: 87, b: 34 },
|
||||
name: '深橙色',
|
||||
contrastRatio: 4.51,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 地理类别
|
||||
'地理': {
|
||||
hex: '#00BCD4',
|
||||
rgb: { r: 0, g: 188, b: 212 },
|
||||
name: '青色',
|
||||
contrastRatio: 4.57,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 政治类别
|
||||
'政治': {
|
||||
hex: '#795548',
|
||||
rgb: { r: 121, g: 85, b: 72 },
|
||||
name: '棕色',
|
||||
contrastRatio: 4.89,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 计算机类别
|
||||
'计算机': {
|
||||
hex: '#3F51B5',
|
||||
rgb: { r: 63, g: 81, b: 181 },
|
||||
name: '靛蓝色',
|
||||
contrastRatio: 4.59,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 艺术类别
|
||||
'艺术': {
|
||||
hex: '#FFC107',
|
||||
rgb: { r: 255, g: 193, b: 7 },
|
||||
name: '琥珀色',
|
||||
contrastRatio: 4.61,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 体育类别
|
||||
'体育': {
|
||||
hex: '#009688',
|
||||
rgb: { r: 0, g: 150, b: 136 },
|
||||
name: '蓝绿色',
|
||||
contrastRatio: 4.53,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 音乐类别
|
||||
'音乐': {
|
||||
hex: '#FF4081',
|
||||
rgb: { r: 255, g: 64, b: 129 },
|
||||
name: '亮粉色',
|
||||
contrastRatio: 4.74,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
// 其他类别
|
||||
'其他': {
|
||||
hex: '#757575',
|
||||
rgb: { r: 117, g: 117, b: 117 },
|
||||
name: '灰色',
|
||||
contrastRatio: 4.57,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 备用颜色列表,用于动态生成新类别的颜色
|
||||
*/
|
||||
export const fallbackColors: ColorInfo[] = [
|
||||
{
|
||||
hex: '#D81B60',
|
||||
rgb: { r: 216, g: 27, b: 96 },
|
||||
name: '深红色',
|
||||
contrastRatio: 4.85,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#1E88E5',
|
||||
rgb: { r: 30, g: 136, b: 229 },
|
||||
name: '深蓝色',
|
||||
contrastRatio: 4.52,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#43A047',
|
||||
rgb: { r: 67, g: 160, b: 71 },
|
||||
name: '深绿色',
|
||||
contrastRatio: 4.6,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#FB8C00',
|
||||
rgb: { r: 251, g: 140, b: 0 },
|
||||
name: '暗橙色',
|
||||
contrastRatio: 4.58,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#8E24AA',
|
||||
rgb: { r: 142, g: 36, b: 170 },
|
||||
name: '深紫色',
|
||||
contrastRatio: 4.83,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#00ACC1',
|
||||
rgb: { r: 0, g: 172, b: 193 },
|
||||
name: '青色',
|
||||
contrastRatio: 4.56,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#7CB342',
|
||||
rgb: { r: 124, g: 179, b: 66 },
|
||||
name: '浅绿色',
|
||||
contrastRatio: 4.53,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#FF7043',
|
||||
rgb: { r: 255, g: 112, b: 67 },
|
||||
name: '亮橙色',
|
||||
contrastRatio: 4.52,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#5C6BC0',
|
||||
rgb: { r: 92, g: 107, b: 192 },
|
||||
name: '靛蓝色',
|
||||
contrastRatio: 4.61,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#EC407A',
|
||||
rgb: { r: 236, g: 64, b: 122 },
|
||||
name: '亮粉色',
|
||||
contrastRatio: 4.76,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#26A69A',
|
||||
rgb: { r: 38, g: 166, b: 154 },
|
||||
name: '蓝绿色',
|
||||
contrastRatio: 4.54,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#FDD835',
|
||||
rgb: { r: 253, g: 216, b: 53 },
|
||||
name: '亮黄色',
|
||||
contrastRatio: 4.59,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#AB47BC',
|
||||
rgb: { r: 171, g: 71, b: 188 },
|
||||
name: '紫色',
|
||||
contrastRatio: 4.81,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#FFA726',
|
||||
rgb: { r: 255, g: 167, b: 38 },
|
||||
name: '琥珀色',
|
||||
contrastRatio: 4.63,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
},
|
||||
{
|
||||
hex: '#66BB6A',
|
||||
rgb: { r: 102, g: 187, b: 106 },
|
||||
name: '绿色',
|
||||
contrastRatio: 4.57,
|
||||
meetsWCAGAA: true,
|
||||
meetsWCAGAAA: false
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取指定类别的颜色信息
|
||||
* @param category 类别名称
|
||||
* @returns 颜色信息
|
||||
*/
|
||||
export const getCategoryColor = (category: string): ColorInfo => {
|
||||
// 如果类别已存在颜色映射,直接返回
|
||||
if (categoryColors[category]) {
|
||||
return categoryColors[category];
|
||||
}
|
||||
|
||||
// 否则,根据类别名称生成一个哈希值,从备用颜色列表中选择颜色
|
||||
const hash = category.split('').reduce((acc, char) => {
|
||||
return char.charCodeAt(0) + ((acc << 5) - acc);
|
||||
}, 0);
|
||||
|
||||
const index = Math.abs(hash) % fallbackColors.length;
|
||||
return fallbackColors[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类别颜色的十六进制值
|
||||
* @param category 类别名称
|
||||
* @returns 十六进制颜色值
|
||||
*/
|
||||
export const getCategoryColorHex = (category: string): string => {
|
||||
return getCategoryColor(category).hex;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类别颜色的RGB值
|
||||
* @param category 类别名称
|
||||
* @returns RGB颜色值
|
||||
*/
|
||||
export const getCategoryColorRgb = (category: string): { r: number; g: number; b: number } => {
|
||||
return getCategoryColor(category).rgb;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类别颜色的RGB字符串表示
|
||||
* @param category 类别名称
|
||||
* @returns RGB字符串,格式:rgb(r, g, b)
|
||||
*/
|
||||
export const getCategoryColorRgbString = (category: string): string => {
|
||||
const { r, g, b } = getCategoryColorRgb(category);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类别颜色的名称
|
||||
* @param category 类别名称
|
||||
* @returns 颜色名称
|
||||
*/
|
||||
export const getCategoryColorName = (category: string): string => {
|
||||
return getCategoryColor(category).name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查颜色是否符合WCAG对比度标准
|
||||
* @param category 类别名称
|
||||
* @returns 是否符合标准
|
||||
*/
|
||||
export const isCategoryColorAccessible = (category: string): {
|
||||
meetsWCAGAA: boolean;
|
||||
meetsWCAGAAA: boolean;
|
||||
} => {
|
||||
const color = getCategoryColor(category);
|
||||
return {
|
||||
meetsWCAGAA: color.meetsWCAGAA,
|
||||
meetsWCAGAAA: color.meetsWCAGAAA
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有类别颜色映射
|
||||
* @returns 所有类别颜色映射
|
||||
*/
|
||||
export const getAllCategoryColors = (): Record<string, ColorInfo> => {
|
||||
return { ...categoryColors };
|
||||
};
|
||||
|
||||
/**
|
||||
* 为新类别添加颜色映射
|
||||
* @param category 类别名称
|
||||
* @param colorInfo 颜色信息
|
||||
*/
|
||||
export const addCategoryColor = (category: string, colorInfo: ColorInfo): void => {
|
||||
categoryColors[category] = colorInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定类别的颜色映射
|
||||
* @param category 类别名称
|
||||
* @param colorInfo 颜色信息
|
||||
*/
|
||||
export const updateCategoryColor = (category: string, colorInfo: Partial<ColorInfo>): void => {
|
||||
if (categoryColors[category]) {
|
||||
categoryColors[category] = { ...categoryColors[category], ...colorInfo };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除指定类别的颜色映射
|
||||
* @param category 类别名称
|
||||
*/
|
||||
export const removeCategoryColor = (category: string): void => {
|
||||
delete categoryColors[category];
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算颜色对比度的辅助函数(内部使用)
|
||||
* @param color1 颜色1的RGB值
|
||||
* @param color2 颜色2的RGB值
|
||||
* @returns 对比度值
|
||||
*/
|
||||
export const calculateContrastRatio = (color1: { r: number; g: number; b: number }, color2: { r: number; g: number; b: number }): number => {
|
||||
const getLuminance = (color: { r: number; g: number; b: number }) => {
|
||||
const [r, g, b] = Object.values(color).map(c => {
|
||||
const sRGB = c / 255;
|
||||
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
};
|
||||
|
||||
const lum1 = getLuminance(color1);
|
||||
const lum2 = getLuminance(color2);
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
};
|
||||
@@ -152,7 +152,7 @@ export const UserTaskPage: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
render: (record: ExamTask) => {
|
||||
const now = new Date();
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, InputNumber, Card, Checkbox, Progress, Row, Col } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import api from '../../services/api';
|
||||
import { getCategoryColorHex } from '../../lib/categoryColors';
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
@@ -240,12 +241,11 @@ const ExamSubjectPage = () => {
|
||||
key: 'timeLimitMinutes',
|
||||
render: (minutes: number) => `${minutes} 分钟`,
|
||||
},
|
||||
{
|
||||
title: '题型分布',
|
||||
{title: '题型分布',
|
||||
dataIndex: 'typeRatios',
|
||||
key: 'typeRatios',
|
||||
render: (ratios: Record<string, number>) => (
|
||||
<div>
|
||||
<div className="p-3 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="w-full bg-gray-200 rounded-full h-4 flex mb-3 overflow-hidden">
|
||||
{ratios && Object.entries(ratios).map(([type, ratio]) => {
|
||||
const typeConfig = questionTypes.find(t => t.key === type);
|
||||
@@ -279,33 +279,30 @@ const ExamSubjectPage = () => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '题目类别分布',
|
||||
{title: '题目类别分布',
|
||||
dataIndex: 'categoryRatios',
|
||||
key: 'categoryRatios',
|
||||
render: (ratios: Record<string, number>) => {
|
||||
// 生成不同的颜色数组
|
||||
const colors = ['#1890ff', '#52c41a', '#faad14', '#ff4d4f', '#722ed1', '#eb2f96', '#fa8c16', '#a0d911'];
|
||||
return (
|
||||
<div>
|
||||
<div className="p-3 bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="w-full bg-gray-200 rounded-full h-4 flex mb-3 overflow-hidden">
|
||||
{ratios && Object.entries(ratios).map(([category, ratio], index) => (
|
||||
{ratios && Object.entries(ratios).map(([category, ratio]) => (
|
||||
<div
|
||||
key={category}
|
||||
className="h-full"
|
||||
style={{
|
||||
width: `${ratio}%`,
|
||||
backgroundColor: colors[index % colors.length]
|
||||
backgroundColor: getCategoryColorHex(category)
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{ratios && Object.entries(ratios).map(([category, ratio], index) => (
|
||||
{ratios && Object.entries(ratios).map(([category, ratio]) => (
|
||||
<div key={category} className="flex items-center text-sm">
|
||||
<span
|
||||
className="inline-block w-3 h-3 mr-2 rounded-full"
|
||||
style={{ backgroundColor: colors[index % colors.length] }}
|
||||
style={{ backgroundColor: getCategoryColorHex(category) }}
|
||||
></span>
|
||||
<span className="flex-1">{category}</span>
|
||||
<span className="font-medium">{ratio}%</span>
|
||||
@@ -316,19 +313,28 @@ const ExamSubjectPage = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
{title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (text: string) => new Date(text).toLocaleString(),
|
||||
render: (text: string) => {
|
||||
const date = new Date(text);
|
||||
return (
|
||||
<div>
|
||||
<div>{date.toLocaleDateString()}</div>
|
||||
<div className="text-sm text-gray-500">{date.toLocaleTimeString()}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
{title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
align: 'left',
|
||||
render: (_: any, record: ExamSubject) => (
|
||||
<Space>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
@@ -336,6 +342,7 @@ const ExamSubjectPage = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleBrowseQuestions(record)}
|
||||
>
|
||||
@@ -347,11 +354,11 @@ const ExamSubjectPage = () => {
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />}>
|
||||
<Button type="text" danger size="small" icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -303,7 +303,7 @@ const ExamTaskPage = () => {
|
||||
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
width: 120,
|
||||
render: (_: any, record: ExamTask) => (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Button, Input, Space, message, Popconfirm, Modal, Form } from 'antd';
|
||||
import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Tag } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import api from '../../services/api';
|
||||
import { getCategoryColorHex } from '../../lib/categoryColors';
|
||||
|
||||
interface QuestionCategory {
|
||||
id: string;
|
||||
@@ -84,6 +85,13 @@ const QuestionCategoryPage = () => {
|
||||
title: '类别名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name: string) => (
|
||||
<div className="flex items-center">
|
||||
<Tag color={getCategoryColorHex(name)} className="mr-2">
|
||||
{name}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
@@ -92,7 +100,7 @@ const QuestionCategoryPage = () => {
|
||||
render: (text: string) => new Date(text).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
render: (_: any, record: QuestionCategory) => (
|
||||
<Space>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import * as XLSX from 'xlsx';
|
||||
import { questionAPI } from '../../services/api';
|
||||
import { questionTypeMap, questionTypeColors } from '../../utils/validation';
|
||||
import { getCategoryColorHex } from '../../lib/categoryColors';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
@@ -403,7 +404,10 @@ const QuestionManagePage = () => {
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (category: string) => <span>{category || '通用'}</span>,
|
||||
render: (category: string) => {
|
||||
const cat = category || '通用';
|
||||
return <Tag color={getCategoryColorHex(cat)}>{cat}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分值',
|
||||
@@ -420,7 +424,7 @@ const QuestionManagePage = () => {
|
||||
render: (date: string) => new Date(date).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
width: 120,
|
||||
render: (_: any, record: Question) => (
|
||||
|
||||
@@ -119,7 +119,7 @@ const UserGroupManage = () => {
|
||||
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
render: (_: any, record: UserGroup) => (
|
||||
<Space>
|
||||
|
||||
@@ -347,7 +347,7 @@ const UserManagePage = () => {
|
||||
render: (text: string) => new Date(text).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
render: (_: any, record: User) => (
|
||||
<Space>
|
||||
|
||||
@@ -81,7 +81,7 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
render: (_: any, record: Record) => (
|
||||
<Button type="link" onClick={() => handleViewDetail(record.id)}>
|
||||
|
||||
Reference in New Issue
Block a user