docs(claude): restructure documentation management and add feedback system

- Restructure §8 from "文档沉淀规则" to "文档管理规则" with 4 subsections
  - Add docs/ structure with features/ and knowledge-base/ directories
  - Add feature documentation template with 7 sections (概述/设计初衷/技术设计/预期作用/实际效果/演化路线/头脑风暴)
  - Add feature update trigger matrix (新增/修改/完成/问题/反馈)
  - Add documentation quality checklist
- Add §16
This commit is contained in:
iven
2026-03-16 13:54:03 +08:00
parent 8e630882c7
commit adfd7024df
44 changed files with 10491 additions and 248 deletions

View File

@@ -0,0 +1,345 @@
import { useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
AlertTriangle,
Wifi,
Shield,
Clock,
Settings,
AlertCircle,
ChevronDown,
ChevronUp,
Copy,
CheckCircle,
ExternalLink,
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { Button } from './Button';
import {
AppError,
ErrorCategory,
classifyError,
formatErrorForClipboard,
getErrorIcon as getIconByCategory,
getErrorColor as getColorByCategory,
} from '../../lib/error-types';
import { reportError } from '../../lib/error-handling';
// === Props ===
export interface ErrorAlertProps {
error: AppError | string | Error | null;
onDismiss?: () => void;
onRetry?: () => void;
showTechnicalDetails?: boolean;
className?: string;
compact?: boolean;
}
interface ErrorAlertState {
showDetails: boolean;
copied: boolean;
}
// === Category Configuration ===
const CATEGORY_CONFIG: Record<ErrorCategory, {
icon: typeof Wifi | typeof Shield | typeof Clock | typeof Settings | typeof AlertCircle | typeof AlertTriangle;
color: string;
bgColor: string;
label: string;
}> = {
network: {
icon: Wifi,
color: 'text-orange-500',
bgColor: 'bg-orange-50 dark:bg-orange-900/20',
label: 'Network',
},
auth: {
icon: Shield,
color: 'text-red-500',
bgColor: 'bg-red-50 dark:bg-red-900/20',
label: 'Authentication',
},
permission: {
icon: Shield,
color: 'text-purple-500',
bgColor: 'bg-purple-50 dark:bg-purple-900/20',
label: 'Permission',
},
validation: {
icon: AlertCircle,
color: 'text-yellow-600',
bgColor: 'bg-yellow-50 dark:bg-yellow-900/20',
label: 'Validation',
},
timeout: {
icon: Clock,
color: 'text-amber-500',
bgColor: 'bg-amber-50 dark:bg-amber-900/20',
label: 'Timeout',
},
server: {
icon: AlertTriangle,
color: 'text-red-500',
bgColor: 'bg-red-50 dark:bg-red-900/20',
label: 'Server',
},
client: {
icon: AlertCircle,
color: 'text-blue-500',
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
label: 'Client',
},
config: {
icon: Settings,
color: 'text-gray-500',
bgColor: 'bg-gray-50 dark:bg-gray-900/20',
label: 'Configuration',
},
system: {
icon: AlertTriangle,
color: 'text-red-600',
bgColor: 'bg-red-50 dark:bg-red-900/20',
label: 'System',
},
};
/**
* Get icon component for error category
*/
export function getIconByCategory(category: ErrorCategory) typeof Wifi | typeof Shield | typeof Clock | typeof Settings | typeof AlertCircle | typeof AlertTriangle {
return CATEGORY_CONFIG[category]?. CATEGORY_CONFIG[category].icon : AlertCircle;
}
/**
* Get color class for error category
*/
export function getColorByCategory(category: ErrorCategory) string {
return CATEGORY_CONFIG[category]?. CATEGORY_CONFIG[category].color : 'text-gray-500';
}
/**
* ErrorAlert Component
*
* Displays detailed error information with recovery suggestions,
* technical details, and action buttons.
*/
export function ErrorAlert({
error: errorProp,
onDismiss,
onRetry,
showTechnicalDetails = true,
className,
compact = false,
}: ErrorAlertProps) {
const [state, setState] = useState<ErrorAlertState>({
showDetails: false,
copied: false,
});
// Normalize error input
const appError = typeof error === 'string'
? classifyError(new Error(error))
: error instanceof Error
? classifyError(error)
: error;
const {
category,
title,
message,
technicalDetails,
recoverable,
recoverySteps,
timestamp,
} = appError;
const config = CATEGORY_CONFIG[category] || CATEGORY_CONFIG.system!;
const IconComponent = config.icon;
const handleCopyDetails = useCallback(async () => {
const text = formatErrorForClipboard(appError);
try {
await navigator.clipboard.writeText(text);
setState({ copied: true });
setTimeout(() => setState({ copied: false }), 2000);
} catch (err) {
console.error('Failed to copy error details:', err);
}
}, [appError]);
const handleReport = useCallback(() => {
reportError(appError.originalError || appError, {
errorId: appError.id,
category: appError.category,
title: appError.title,
message: appError.message,
timestamp: appError.timestamp.toISOString(),
});
}, [appError]);
const toggleDetails = useCallback(() => {
setState((prev) => ({ showDetails: !prev.showDetails }));
}, []);
const handleRetry = useCallback(() => {
onRetry?.();
}, [onRetry]);
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={cn(
'rounded-lg border overflow-hidden',
config.bgColor,
'border-gray-200 dark:border-gray-700',
className
)}
>
{/* Header */}
<div className="flex items-start gap-3 p-3 bg-white/50 dark:bg-gray-800/50">
<div className={cn('p-2 rounded-lg', config.bgColor)}>
<IconComponent className={cn('w-5 h-5', config.color)} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className={cn('text-xs font-medium', config.color)}>
{config.label}
</span>
<span className="text-xs text-gray-400">
{timestamp.toLocaleTimeString()}
</span>
</div>
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mt-1">
{title}
</h4>
</div>
{onDismiss && (
<button
onClick={onDismiss}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1"
aria-label="Dismiss"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Body */}
<div className="px-3 pb-2">
<p className={cn(
'text-gray-700 dark:text-gray-300',
compact ? 'text-sm line-clamp-2' : 'text-sm'
)}>
{message}
</p>
{/* Recovery Steps */}
{recoverySteps.length > 0 && !compact && (
<div className="mt-3 space-y-2">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
Recovery Suggestions
</p>
<ul className="space-y-1">
{recoverySteps.slice(0, 3).map((step, index) => (
<li key={index} className="text-xs text-gray-600 dark:text-gray-400 flex items-start gap-2">
<span className="text-gray-400">-</span>
{step.description}
{step.action && step.label && (
<button
onClick={step.action}
className="text-blue-500 hover:text-blue-600 ml-1"
>
{step.label}
</button>
)}
</li>
))}
</ul>
</div>
)}
{/* Technical Details Toggle */}
{showTechnicalDetails && technicalDetails && !compact && (
<div className="mt-2">
<button
onClick={toggleDetails}
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
{state.showDetails ? (
<ChevronUp className="w-3 h-3" />
) : (
<ChevronDown className="w-3 h-3" />
)}
Technical Details
</button>
<AnimatePresence>
{state.showDetails && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<pre className="mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs text-gray-600 dark:text-gray-400 overflow-x-auto whitespace-pre-wrap break-all">
{technicalDetails}
</pre>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between gap-2 p-3 pt-2 border-t border-gray-100 dark:border-gray-700 bg-white/30 dark:bg-gray-800/30">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleCopyDetails}
className="text-xs"
>
{state.copied ? (
<>
<CheckCircle className="w-3 h-3 mr-1" />
Copied
</>
) : (
<>
<Copy className="w-3 h-3 mr-1" />
Copy
</>
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleReport}
className="text-xs"
>
<ExternalLink className="w-3 h-3 mr-1" />
Report
</Button>
</div>
{recoverable && onRetry && (
<Button
variant="primary"
size="sm"
onClick={handleRetry}
className="text-xs"
>
Try Again
</Button>
)}
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,179 @@
import { Component, ReactNode, ErrorInfo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, RefreshCcw, Bug, Home } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Button } from './Button';
import { reportError } from '../../lib/error-handling';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onReset?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* ErrorBoundary Component
*
* Catches React rendering errors and displays a friendly error screen
* with recovery options and error reporting.
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): ErrorInfo {
return {
componentStack: error.stack || 'No stack trace available',
errorName: error.name || 'Unknown Error',
errorMessage: error.message || 'An unexpected error occurred',
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const { onError } = this.props;
// Call optional error handler
if (onError) {
onError(error, errorInfo);
}
// Update state to show error UI
this.setState({
hasError: true,
error,
errorInfo: {
componentStack: errorInfo.componentStack,
errorName: errorInfo.errorName || error.name || 'Unknown Error',
errorMessage: errorInfo.errorMessage || error.message || 'An unexpected error occurred',
},
});
}
handleReset = () => {
const { onReset } = this.props;
// Reset error state
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
// Call optional reset handler
if (onReset) {
onReset();
}
};
handleReport = () => {
const { error, errorInfo } = this.state;
if (error && errorInfo) {
reportError(error, {
componentStack: errorInfo.componentStack,
errorName: errorInfo.errorName,
errorMessage: errorInfo.errorMessage,
});
}
};
handleGoHome = () => {
// Navigate to home/main view
window.location.href = '/';
};
render() {
const { children, fallback } = this.props;
const { hasError, error, errorInfo } = this.state;
if (hasError && error) {
// Use custom fallback if provided
if (fallback) {
return fallback;
}
// Default error UI
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md w-full bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden"
>
{/* Error Icon */}
<div className="flex items-center justify-center w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full mx-4">
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
{/* Content */}
<div className="p-6 text-center">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Something went wrong
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{errorInfo?.errorMessage || error.message || 'An unexpected error occurred'}
</p>
{/* Error Details */}
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg text-left">
<p className="text-xs text-gray-500 dark:text-gray-400 font-mono">
{errorInfo?.errorName || 'Unknown Error'}
</p>
</div>
{/* Actions */}
<div className="flex flex-col gap-2 mt-6">
<Button
variant="primary"
size="sm"
onClick={this.handleReset}
className="w-full"
>
<RefreshC className="w-4 h-4 mr-2" />
Try Again
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={this.handleReport}
className="flex-1"
>
<Bug className="w-4 h-4 mr-2" />
Report Issue
</Button>
<Button
variant="ghost"
size="sm"
onClick={this.handleGoHome}
className="flex-1"
>
<Home className="w-4 h-4 mr-2" />
Go Home
</Button>
</div>
</div>
</motion.div>
</div>
);
}
return children;
}
}
}