PageSourceSearch

https://kejun.github.io/

html kejun.github.io collected 2026-10-03 09:27:07 UTC 56,159 bytes, 1,345 lines download raw bytes

1<!DOCTYPE html>
2<html lang="zh-CN">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>kejun@github:~$</title>
7    <link rel="preconnect" href="https://fonts.googleapis.com">
8    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9    <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
10
11    <!-- Three.js for 3D Models -->
12    
12<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
12
13    <!-- fflate - Required by FBXLoader -->
14    
14<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/index.js"></script>
14
15    
15<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
15
16    
16<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/FBXLoader.js"></script>
16
17    
17<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
17
18    <style>
19        * {
20            margin: 0;
21            padding: 0;
22            box-sizing: border-box;
23        }
24
25        :root {
26            --bg-color: #0d1117;
27            --terminal-bg: #161b22;
28            --text-primary: #e6edf3;
29            --text-secondary: #7d8590;
30            --accent-green: #238636;
31            --accent-blue: #58a6ff;
32            --accent-yellow: #d29922;
33            --accent-red: #f85149;
34            --accent-purple: #a371f7;
35            --accent-cyan: #39c5cf;
36            --border-color: #30363d;
37        }
38
39        body {
40            font-family: 'JetBrains Mono', 'Courier New', monospace;
41            background-color: var(--bg-color);
42            color: var(--text-primary);
43            line-height: 1.6;
44            min-height: 100vh;
45            overflow-x: hidden;
46        }
47
48        /* Terminal Window */
49        .terminal {
50            max-width: 1200px;
51            margin: 20px auto;
52            background: var(--terminal-bg);
53            border-radius: 10px;
54            border: 1px solid var(--border-color);
55            overflow: hidden;
56            box-shadow: 0 20px 50px rgba(0,0,0,0.5);
57        }
58
59        /* Terminal Header */
60        .terminal-header {
61            background: linear-gradient(180deg, #2d333b 0%, #1c2128 100%);
62            padding: 12px 16px;
63            display: flex;
64            align-items: center;
65            gap: 8px;
66            border-bottom: 1px solid var(--border-color);
67        }
68
69        .terminal-btn {
70            width: 12px;
71            height: 12px;
72            border-radius: 50%;
73            border: none;
74            cursor: pointer;
75        }
76
77        .btn-close { background: #ff5f56; }
78        .btn-minimize { background: #ffbd2e; }
79        .btn-maximize { background: #27c93f; }
80
81        .terminal-title {
82            flex: 1;
83            text-align: center;
84            font-size: 13px;
85            color: var(--text-secondary);
86        }
87
88        /* Terminal Body */
89        .terminal-body {
90            padding: 20px;
91            min-height: 600px;
92        }
93
94        /* ASCII Art */
95        .ascii-art {
96            color: var(--accent-green);
97            font-size: 11px;
98            line-height: 1.2;
99            margin-bottom: 20px;
100            white-space: pre;
101            overflow-x: auto;
102        }
103
104        /* Command Line */
105        .command-line {
106            display: flex;
107            align-items: flex-start;
108            margin-bottom: 15px;
109            flex-wrap: wrap;
110        }
111
112        .prompt {
113            color: var(--accent-green);
114            margin-right: 10px;
115            white-space: nowrap;
116        }
117
118        .command {
119            color: var(--text-primary);
120        }
121
122        .cursor {
123            display: inline-block;
124            width: 10px;
125            height: 20px;
126            background: var(--accent-green);
127            animation: blink 1s infinite;
128            vertical-align: middle;
129            margin-left: 2px;
130        }
131
132        @keyframes blink {
133            0%, 50% { opacity: 1; }
134            51%, 100% { opacity: 0; }
135        }
136
137        /* Output Sections */
138        .output {
139            margin: 20px 0;
140            padding-left: 20px;
141        }
142
143        .section-title {
144            color: var(--accent-blue);
145            font-weight: bold;
146            margin-bottom: 10px;
147            display: flex;
148            align-items: center;
149            gap: 10px;
150        }
151
152        .section-title::before {
153            content: '▶';
154            color: var(--accent-green);
155        }
156
157        /* Info Cards */
158        .info-grid {
159            display: grid;
160            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
161            gap: 20px;
162            margin: 20px 0;
163        }
164
165        .info-card {
166            background: rgba(22, 27, 34, 0.8);
167            border: 1px solid var(--border-color);
168            border-radius: 8px;
169            padding: 20px;
170            transition: all 0.3s ease;
171        }
172
173        .info-card:hover {
174            border-color: var(--accent-blue);
175            transform: translateY(-2px);
176        }
177
178        .card-header {
179            display: flex;
180            align-items: center;
181            gap: 10px;
182            margin-bottom: 15px;
183            color: var(--accent-yellow);
184        }
185
186        .card-header .icon {
187            font-size: 20px;
188        }
189
190        .card-content {
191            color: var(--text-secondary);
192        }
193
194        .card-content strong {
195            color: var(--text-primary);
196        }
197
198        /* Stats */
199        .stats-row {
200            display: flex;
201            gap: 30px;
202            flex-wrap: wrap;
203            margin: 15px 0;
204        }
205
206        .stat-item {
207            display: flex;
208            align-items: center;
209            gap: 8px;
210        }
211
212        .stat-value {
213            color: var(--accent-green);
214            font-weight: bold;
215            font-size: 24px;
216        }
217
218        .stat-label {
219            color: var(--text-secondary);
220            font-size: 14px;
221        }
222
223        /* Repository List */
224        .repo-list {
225            list-style: none;
226        }
227
228        .repo-item {
229            padding: 15px;
230            border-left: 3px solid var(--border-color);
231            margin-bottom: 10px;
232            background: rgba(22, 27, 34, 0.5);
233            transition: all 0.3s ease;
234        }
235
236        .repo-item:hover {
237            border-left-color: var(--accent-green);
238            background: rgba(35, 134, 54, 0.1);
239        }
240
241        .repo-name {
242            color: var(--accent-blue);
243            font-weight: bold;
244            text-decoration: none;
245            display: flex;
246            align-items: center;
247            gap: 10px;
248        }
249
250        .repo-name:hover {
251            text-decoration: underline;
252        }
253
254        .repo-desc {
255            color: var(--text-secondary);
256            margin-top: 5px;
257            font-size: 14px;
258        }
259
260        .repo-stats {
261            display: flex;
262            gap: 20px;
263            margin-top: 10px;
264            font-size: 13px;
265            color: var(--text-secondary);
266        }
267
268        .repo-stats span {
269            display: flex;
270            align-items: center;
271            gap: 5px;
272        }
273
274        /* Skills */
275        .skills-container {
276            display: flex;
277            flex-wrap: wrap;
278            gap: 10px;
279            margin-top: 15px;
280        }
281
282        .skill-tag {
283            background: rgba(88, 166, 255, 0.1);
284            color: var(--accent-blue);
285            padding: 5px 12px;
286            border-radius: 20px;
287            font-size: 13px;
288            border: 1px solid rgba(88, 166, 255, 0.3);
289        }
290
291        /* Links */
292        .link-list {
293            display: flex;
294            flex-wrap: wrap;
295            gap: 15px;
296            margin-top: 15px;
297        }
298
299        .link-item {
300            color: var(--accent-cyan);
301            text-decoration: none;
302            display: flex;
303            align-items: center;
304            gap: 8px;
305            padding: 8px 15px;
306            border: 1px solid var(--border-color);
307            border-radius: 6px;
308            transition: all 0.3s ease;
309        }
310
311        .link-item:hover {
312            background: rgba(57, 197, 207, 0.1);
313            border-color: var(--accent-cyan);
314        }
315
316        /* Typing Effect */
317        .typing-text {
318            overflow: hidden;
319            white-space: nowrap;
320            animation: typing 3s steps(40, end);
321        }
322
323        @keyframes typing {
324            from { width: 0; }
325            to { width: 100%; }
326        }
327
328        /* Command Menu */
329        .command-menu {
330            position: fixed;
331            bottom: 20px;
332            left: 50%;
333            transform: translateX(-50%);
334            background: var(--terminal-bg);
335            border: 1px solid var(--border-color);
336            border-radius: 8px;
337            padding: 10px 20px;
338            display: flex;
339            gap: 15px;
340            z-index: 100;
341        }
342
343        .menu-btn {
344            background: none;
345            border: 1px solid var(--border-color);
346            color: var(--text-secondary);
347            padding: 8px 16px;
348            border-radius: 6px;
349            cursor: pointer;
350            font-family: inherit;
351            transition: all 0.3s ease;
352        }
353
354        .menu-btn:hover {
355            border-color: var(--accent-green);
356            color: var(--accent-green);
357        }
358
359        /* Scrollbar */
360        ::-webkit-scrollbar {
361            width: 10px;
362            height: 10px;
363        }
364
365        ::-webkit-scrollbar-track {
366            background: var(--bg-color);
367        }
368
369        ::-webkit-scrollbar-thumb {
370            background: var(--border-color);
371            border-radius: 5px;
372        }
373
374        ::-webkit-scrollbar-thumb:hover {
375            background: var(--text-secondary);
376        }
377
378        /* Responsive */
379        @media (max-width: 768px) {
380            .terminal {
381                margin: 10px;
382            }
383
384            .ascii-art {
385                font-size: 8px;
386            }
387
388            .info-grid {
389                grid-template-columns: 1fr;
390            }
391
392            .command-menu {
393                flex-wrap: wrap;
394                width: 90%;
395                justify-content: center;
396            }
397        }
398
399        /* Hidden sections */
400        .section {
401            display: none;
402        }
403
404        .section.active {
405            display: block;
406            animation: fadeIn 0.5s ease;
407        }
408
409        @keyframes fadeIn {
410            from { opacity: 0; transform: translateY(10px); }
411            to { opacity: 1; transform: translateY(0); }
412        }
413
414        /* Progress Bar */
415        .progress-bar {
416            width: 100%;
417            height: 6px;
418            background: var(--border-color);
419            border-radius: 3px;
420            overflow: hidden;
421            margin-top: 5px;
422        }
423
424        .progress-fill {
425            height: 100%;
426            background: linear-gradient(90deg, var(--accent-green), var(--accent-blue));
427            border-radius: 3px;
428            transition: width 1s ease;
429        }
430    </style>
431</head>
432<body>
433    <div class="terminal">
434        <div class="terminal-header">
435            <div class="terminal-btn btn-close"></div>
436            <div class="terminal-btn btn-minimize"></div>
437            <div class="terminal-btn btn-maximize"></div>
438            <div class="terminal-title">kejun@github: ~ (zsh)</div>
439        </div>
440
441        <div class="terminal-body">
442            <!-- ASCII Art -->
443            <pre class="ascii-art">
444 ██╗  ██╗███████╗     ██╗██╗   ██╗███╗   ██╗
445 ██║ ██╔╝██╔════╝     ██║██║   ██║████╗  ██║
446 █████╔╝ █████╗       ██║██║   ██║██╔██╗ ██║
447 ██╔═██╗ ██╔══╝  ██   ██║██║   ██║██║╚██╗██║
448 ██║  ██╗███████╗╚█████╔╝╚██████╔╝██║ ╚████║
449 ╚═╝  ╚═╝╚══════╝ ╚════╝  ╚═════╝ ╚═╝  ╚═══╝
450            </pre>
451
452            <!-- Welcome Message -->
453            <div class="command-line">
454                <span class="prompt">➜</span>
455                <span class="command">whoami<span class="cursor"></span></span>
456            </div>
457
458            <div class="output">
459                <p style="color: var(--accent-cyan); font-size: 18px; margin-bottom: 10px;">
460                    👋 Hi, I'm <strong style="color: var(--text-primary);">克军 (kejun)</strong>
461                </p>
462                <p style="color: var(--text-secondary);">
463                    Frontend Developer | Database Enthusiast | Open Source Contributor
464                </p>
465                <p style="color: var(--text-secondary); margin-top: 10px;">
466                    📍 Beijing, China | Exploring AI-Native Database & Modern Web Dev
467                </p>
468            </div>
469
470            <!-- Command Navigation -->
471            <div class="command-line" style="margin-top: 30px;">
472                <span class="prompt">➜</span>
473                <span class="command">ls -la sections/</span>
474            </div>
475
476            <div class="output">
477                <div style="display: flex; gap: 20px; flex-wrap: wrap;">
478                    <button class="menu-btn" onclick="showSection('about')">about/</button>
479                    <button class="menu-btn" onclick="showSection('projects')">projects/</button>
480                    <button class="menu-btn" onclick="showSection('skills')">skills/</button>
481                    <button class="menu-btn" onclick="showSection('stats')">stats/</button>
482                    <button class="menu-btn" onclick="showSection('contact')">contact/</button>
483                </div>
484            </div>
485
486            <!-- External Links -->
487            <div class="command-line" style="margin-top: 10px;">
488                <span class="prompt">➜</span>
489                <span class="command">ls -la links/</span>
490            </div>
491
492            <div class="output">
493                <div style="display: flex; gap: 20px; flex-wrap: wrap;">
494                    <a href="https://github.com/kejun/blogpost/blob/main/README.md" class="link-item" target="_blank">
495                        <span>📝</span> Blog Posts
496                    </a>
497                    <a href="https://github.com/kejun/daily-investor/tree/main/ideas" class="link-item" target="_blank">
498                        <span>💡</span> Ideas
499                    </a>
500                    <a href="https://github.com/kejun/daily-rss/tree/main" class="link-item" target="_blank">
501                        <span>📡</span> Daily RSS
502                    </a>
503                </div>
504            </div>
505
506            <!-- About Section -->
507            <div id="about" class="section active">
508                <div class="command-line">
509                    <span class="prompt">➜</span>
510                    <span class="command">cat about.md</span>
511                </div>
512                <div class="output">
513                    <div class="info-grid">
514                        <div class="info-card">
515                            <div class="card-header">
516                                <span class="icon">💼</span>
517                                <span>Work</span>
518                            </div>
519                            <div class="card-content">
520                                <p>Frontend Developer passionate about modern web technologies</p>
521                                <p style="margin-top: 10px;">Exploring AI-Native Database & Vector Search applications</p>
522                            </div>
523                        </div>
524                        <div class="info-card">
525                            <div class="card-header">
526                                <span class="icon">🎯</span>
527                                <span>Focus</span>
528                            </div>
529                            <div class="card-content">
530                                <p>• Frontend Architecture</p>
531                                <p>• JavaScript/TypeScript</p>
532                                <p>• Database Systems</p>
533                                <p>• AI Infrastructure</p>
534                            </div>
535                        </div>
536                        <div class="info-card">
537                            <div class="card-header">
538                                <span class="icon">🌟</span>
539                                <span>Highlights</span>
540                            </div>
541                            <div class="card-content">
542                                <p>• Creator of <strong>Do</strong> (291⭐)</p>
543                                <p>• Contributor to <strong>OceanBase</strong></p>
544                                <p>• Open source enthusiast since 2010</p>
545                            </div>
546                        </div>
547                    </div>
548                </div>
549            </div>
550
551            <!-- Projects Section -->
552            <div id="projects" class="section">
553                <div class="command-line">
554                    <span class="prompt">➜</span>
555                    <span class="command">gh repo list --limit 10</span>
556                </div>
557                <div class="output">
558                    <ul class="repo-list">
559                        <li class="repo-item">
560                            <a href="https://github.com/kejun/Do" class="repo-name" target="_blank">
561                                ⚡ Do
562                            </a>
563                            <p class="repo-desc">轻量级文件加载和依赖管理解决方案</p>
564                            <div class="repo-stats">
565                                <span>⭐ 291 stars</span>
566                                <span>🍴 71 forks</span>
567                                <span>🟨 JavaScript</span>
568                            </div>
569                        </li>
570                        <li class="repo-item">
571                            <a href="https://github.com/kejun/CSS-Code
571-Guideline" class="repo-name" target="_blank">
572                                📘 CSS Code Guideline
573                            </a>
574                            <p class="repo-desc">CSS 编码规范与最佳实践</p>
575                            <div class="repo-stats">
576                                <span>⭐ 130 stars</span>
577                                <span>🍴 26 forks</span>
578                            </div>
579                        </li>
580                        <li class="repo-item">
581                            <a href="https://github.com/kejun/Douban-JSLint" class="repo-name" target="_blank">
582                                🔍 Douban JSLint
583                            </a>
584                            <p class="repo-desc">JavaScript 代码检查工具</p>
585                            <div class="repo-stats">
586                                <span>⭐ 41 stars</span>
587                                <span>🍴 3 forks</span>
588                                <span>🟨 JavaScript</span>
589                            </div>
590                        </li>
591                        <li class="repo-item">
592                            <a href="https://github.com/kejun/mcp-oceanbase" class="repo-name" target="_blank">
593                                🔌 mcp-oceanbase
594                            </a>
595                            <p class="repo-desc">MCP Server for OceanBase database and its tools</p>
596                            <div class="repo-stats">
597                                <span>🔧 Apache-2.0</span>
598                            </div>
599                        </li>
600                    </ul>
601                </div>
602            </div>
603
604            <!-- Skills Section -->
605            <div id="skills" class="section">
606                <div class="command-line">
607                    <span class="prompt">➜</span>
608                    <span class="command">cat skills.json</span>
609                </div>
610                <div class="output">
611                    <div class="info-card" style="margin-bottom: 20px;">
612                        <div class="card-header">
613                            <span class="icon">💻</span>
614                            <span>Languages</span>
615                        </div>
616                        <div class="skills-container">
617                            <span class="skill-tag">JavaScript</span>
618                            <span class="skill-tag">TypeScript</span>
619                            <span class="skill-tag">C++</span>
620                            <span class="skill-tag">Python</span>
621                            <span class="skill-tag">SQL</span>
622                        </div>
623                    </div>
624
625                    <div class="info-card" style="margin-bottom: 20px;">
626                        <div class="card-header">
627                            <span class="icon">🗄️</span>
628                            <span>Database & Search</span>
629                        </div>
630                        <div class="skills-container">
631                            <span class="skill-tag">OceanBase</span>
632                            <span class="skill-tag">seekdb</span>
633                            <span class="skill-tag">Vector Search</span>
634                            <span class="skill-tag">Hybrid Search</span>
635                            <span class="skill-tag">RAG</span>
636                        </div>
637                    </div>
638
639                    <div class="info-card">
640                        <div class="card-header">
641                            <span class="icon">🛠️</span>
642                            <span>Tools & Frameworks</span>
643                        </div>
644                        <div class="skills-container">
645                            <span class="skill-tag">Node.js</span>
646                            <span class="skill-tag">React</span>
647                            <span class="skill-tag">LangChain</span>
648                            <span class="skill-tag">MCP</span>
649                            <span class="skill-tag">Git</span>
650                        </div>
651                    </div>
652                </div>
653            </div>
654
655            <!-- Stats Section -->
656            <div id="stats" class="section">
657                <div class="command-line">
658                    <span class="prompt">➜</span>
659                    <span class="command">gh status</span>
660                </div>
661                <div class="output">
662                    <div class="stats-row">
663                        <div class="stat-item">
664                            <span class="stat-value">45</span>
665                            <span class="stat-label">Public Repos</span>
666                        </div>
667                        <div class="stat-item">
668                            <span class="stat-value">75</span>
669                            <span class="stat-label">Gists</span>
670                        </div>
671                        <div class="stat-item">
672                            <span class="stat-value">500+</span>
673                            <span class="stat-label">Total Stars</span>
674                        </div>
675                    </div>
676
677                    <div class="info-card" style="margin-top: 30px;">
678                        <div class="card-header">
679                            <span class="icon">📊</span>
680                            <span>GitHub Stats</span>
681                        </div>
682                        <div style="margin-top: 20px;">
683                            <div style="margin-bottom: 15px;">
684                                <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
685                                    <span>JavaScript</span>
686                                    <span style="color: var(--accent-green);">45%</span>
687                                </div>
688                                <div class="progress-bar">
689                                    <div class="progress-fill" style="width: 45%;"></div>
690                                </div>
691                            </div>
692                            <div style="margin-bottom: 15px;">
693                                <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
694                                    <span>TypeScript</span>
695                                    <span style="color: var(--accent-blue);">25%</span>
696                                </div>
697                                <div class="progress-bar">
698                                    <div class="progress-fill" style="width: 25%;"></div>
699                                </div>
700                            </div>
701                            <div style="margin-bottom: 15px;">
702                                <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
703                                    <span>C++</span>
704                                    <span style="color: var(--accent-yellow);">20%</span>
705                                </div>
706                                <div class="progress-bar">
707                                    <div class="progress-fill" style="width: 20%;"></div>
708                                </div>
709                            </div>
710                            <div>
711                                <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
712                                    <span>Others</span>
713                                    <span style="color: var(--accent-purple);">10%</span>
714                                </div>
715                                <div class="progress-bar">
716                                    <div class="progress-fill" style="width: 10%;"></div>
717                                </div>
718                            </div>
719                        </div>
720                    </div>
721                </div>
722            </div>
723
724            <!-- Contact Section -->
725            <div id="contact" class="section">
726                <div class="command-line">
727                    <span class="prompt">➜</span>
728                    <span class="command">cat contact.txt</span>
729                </div>
730                <div class="output">
731                    <div class="info-card">
732                        <div class="card-header">
733                            <span class="icon">📧</span>
734                            <span>Connect with me</span>
735                        </div>
736                        <div class="link-list" style="margin-top: 20px;">
737                            <a href="https://github.com/kejun" class="link-item" target="_blank">
738                                <span>🐙</span> GitHub
739                            </a>
740                            <a href="http://weibo.com/kejunz" class="link-item" target="_blank">
741                                <span>📱</span> Weibo
742                            </a>
743                            <a href="mailto:[email protected]" class="link-item">
744                                <span>✉️</span> Email
745                            </a>
746                            <a href="https://www.oceanbase.ai" class="link-item" target="_blank">
747                                <span>🌊</span> OceanBase
748                            </a>
749                        </div>
750                    </div>
751
752                    <div class="command-line" style="margin-top: 30px;">
753                        <span class="prompt">➜</span>
754                        <span class="command">echo "Let's build something amazing together!"</span>
755                    </div>
756                    <div class="output">
757                        <p style="color: var(--accent-green); font-size: 16px; margin-top: 10px;">
758                            🚀 Let's build something amazing together!
759                        </p>
760                    </div>
761                </div>
762            </div>
763
764            <!-- Footer -->
765            <div class="command-line" style="margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border-color);">
766                <span class="prompt">➜</span>
767                <span class="command" style="color: var(--text-secondary);">
768                    Last login: <span id="last-login"></span> from <span style="color: var(--accent-cyan);">Beijing, China</span>
769                </span>
770            </div>
771        </div>
772    </div>
773
774    
774<script>
775        // Show current time
776        document.getElementById('last-login').textContent = new Date().toLocaleString('zh-CN');
777
778        // Section navigation
779        function showSection(sectionId) {
780            // Hide all sections
781            const sections = document.querySelectorAll('.section');
782            sections.forEach(section => {
783                section.classList.remove('active');
784            });
785
786            // Show selected section
787            const targetSection = document.getElementById(sectionId);
788            if (targetSection) {
789                targetSection.classList.add('active');
790            }
791
792            // Update command line
793            const commandLine = targetSection.previousElementSibling;
794            if (commandLine && commandLine.classList.contains('command-line')) {
795                const command = commandLine.querySelector('.command');
796                if (command) {
797                    command.innerHTML = command.textContent + '<span class="cursor"></span>';
798                }
799            }
800        }
801
802        // Typing effect for welcome message
803        const welcomeText = "whoami";
804        const welcomeCommand = document.querySelector('.command');
805        let charIndex = 0;
806
807        function typeWelcome() {
808            if (charIndex < welcomeText.length) {
809                welcomeCommand.innerHTML = welcomeText.substring(0, charIndex + 1) + '<span class="cursor"></span>';
810                charIndex++;
811                setTimeout(typeWelcome, 100);
812            }
813        }
814
815        // Start typing effect on load
816        window.addEventListener('load', () => {
817            welcomeCommand.innerHTML = '<span class="cursor"></span>';
818            charIndex = 0;
819            setTimeout(typeWelcome, 500);
820        });
821
822        // Keyboard shortcuts
823        document.addEventListener('keydown', (e) => {
824            if (e.key === '1') showSection('about');
825            if (e.key === '2') showSection('projects');
826            if (e.key === '3') showSection('skills');
827            if (e.key === '4') showSection('stats');
828            if (e.key === '5') showSection('contact');
829        });
830
831        // Animate progress bars when stats section is shown
832        const observer = new IntersectionObserver((entries) => {
833            entries.forEach(entry => {
834                if (entry.isIntersecting) {
835                    const progressBars = entry.target.querySelectorAll('.progress-fill');
836                    progressBars.forEach(bar => {
837                        const width = bar.style.width;
838                        bar.style.width = '0';
839                        setTimeout(() => {
840                            bar.style.width = width;
841                        }, 100);
842                    });
843                }
844            });
845        });
846
847        const statsSection = document.getElementById('stats');
848        if (statsSection) {
849            observer.observe(statsSection);
850        }
851
852        // Easter egg: Konami code
853        let konamiCode = [];
854        const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
855
856        document.addEventListener('keydown', (e) => {
857            konamiCode.push(e.key);
858            konamiCode = konamiCode.slice(-10);
859
860            if (konamiCode.join(',') === konamiSequence.join(',')) {
861                document.body.style.animation = 'rainbow 2s infinite';
862                setTimeout(() => {
863                    document.body.style.animation = '';
864                }, 5000);
865            }
866        });
867
868        // Add rainbow animation
869        const style = document.createElement('style');
870        style.textContent = `
871            @keyframes rainbow {
872                0% { filter: hue-rotate(0deg); }
873                100% { filter: hue-rotate(360deg); }
874            }
875        `;
876        document.head.appendChild(style);
877
878        // ========== AI Companion 3D Robot ==========
879        const aiCompanionCSS = `
880            #ai-companion-canvas{position:fixed;bottom:20px;right:20px;z-index:999999;width:340px;height:50vh;min-height:360px;max-height:calc(100vh - 40px);border-radius:16px;overflow:hidden;pointer-events:none;background:transparent;transition:transform 0.4s cubic-bezier(0.4,0,0.2,1),opacity 0.4s cubic-bezier(0.4,0,0.2,1)}
881            #ai-companion-canvas.collapsed{transform:translateY(calc(100% + 20px));opacity:0;pointer-events:none}
882            #ai-companion-canvas canvas{width:100%;height:100%;display:block}
883            #ai-companion-toggle{position:fixed;bottom:24px;right:24px;z-index:1000000;width:40px;height:40px;border-radius:50%;border:1px solid rgba(48,54,61,0.8);background:rgba(22,27,34,0.85);backdrop-filter:blur(8px);color:#e6edf3;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1;transition:background 0.2s,transform 0.2s;pointer-events:auto;box-shadow:0 2px 8px rgba(0,0,0,0.3)}
884            #ai-companion-toggle:hover{background:rgba(35,134,54,0.6);transform:scale(1.1)}
885        `;
886
887        // 添加样式
888        const aiStyle = document.createElement('style');
889        aiStyle.textContent = aiCompanionCSS;
890        document.head.appendChild(aiStyle);
891
892        // 添加 HTML 结构
893        const aiHTML = `<div id="ai-companion-canvas" class="collapsed"></div><button id="ai-companion-toggle" title="展开3D人物">👤</button>`;
894        document.body.insertAdjacentHTML('beforeend', aiHTML);
895
896        // 收起/展开按钮逻辑
897        (function() {
898            const toggleBtn = document.getElementById('ai-companion-toggle');
899            const companionCanvas = document.getElementById('ai-companion-canvas');
900            let isCollapsed = true;
901
902            toggleBtn.addEventListener('click', function() {
903                isCollapsed = !isCollapsed;
904                if (isCollapsed) {
905                    companionCanvas.classList.add('collapsed');
906                    toggleBtn.textContent = '👤';
907                    toggleBtn.title = '展开3D人物';
908                } else {
909                    companionCanvas.classList.remove('collapsed');
910                    toggleBtn.textContent = '▼';
911                    toggleBtn.title = '收起3D人物';
912                }
913            });
914        })();
915
916        // AI Companion 3D Logic
917        (function() {
918            const AI_COMPANION_VERSION = 'v2026.02.14.5';
919            const canvas = document.getElementById('ai-companion-canvas');
920            const CONFIG = { width: 340 };
921
922            // 获取当前画布高度
923            function getCanvasHeight() {
924                const preferredHeight = Math.max(320, window.innerHeight * 0.45);
925                return Math.max(220, Math.min(preferredHeight, window.innerHeight - 40));
926            }
927
928            let currentHeight = getCanvasHeight();
929            let model, mixer, actions = {}, currentAnimation = 'Walking', clock = new THREE.Clock();
930            
931            // 鼠标跟随旋转变量
932            let targetRotationY = 0;
933            const MAX_ROTATION_RADIANS = 40 * (Math.PI / 180); // 40度,左右各40度,总共80度
934            const ROTATION_SMOOTHING_FACTOR = 0.05; // 旋转平滑插值系数
935
936            // 牛仔裤区域参数(基于模型归一化高度 1.5)
937            const PANTS_BOTTOM_Y = 0.10;  // 脚踝位置
938            const PANTS_TOP_Y = 0.55;     // 腰部位置
939            const PANTS_BLEND_EDGE = 0.03; // 过渡区域宽度
940
941            const scene = new THREE.Scene();
942            scene.background = null;
943
944            const camera = new THREE.PerspectiveCamera(45, CONFIG.width / currentHeight, 0.1, 1000);
945            camera.position.set(0, 2, 6);
946
947            const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
948            renderer.setSize(CONFIG.width, currentHeight);
949            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
950            renderer.shadowMap.enabled = true;
951            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
952            renderer.outputEncoding = THREE.sRGBEncoding;
953            renderer.toneMapping = THREE.ACESFilmicToneMapping;
954            renderer.toneMappingExposure = 1.0;
955            renderer.setClearColor(0x000000, 0);
956            canvas.appendChild(renderer.domElement);
957
958            // OrbitControls - 禁用旋转,改用鼠标跟随
959            const controls = new THREE.OrbitControls(camera, renderer.domElement);
960            controls.enablePan = false;
961            controls.enableZoom = false;
962            controls.enableRotate = false; // 禁用旋转功能
963            controls.minPolarAngle = Math.PI / 4;
964            controls.maxPolarAngle = Math.PI / 2;
965            controls.target.set(0, 0.8, 0);
966
967            // 灯光 - 柔和自然光照,减少塑料感
968            const hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x080820, 0.6);
969            scene.add(hemiLight);
970
971            const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
972            scene.add(ambientLight);
973            
974            const dirLight = new THREE.DirectionalLight(0xfff5e6, 0.8);
975            dirLight.position.set(5, 10, 5);
976            dirLight.castShadow = true;
977            scene.add(dirLight);
978            
979            const fillLight = new THREE.DirectionalLight(0xe8f0ff, 0.3);
980            fillLight.position.set(-3, 5, 3);
981            scene.add(fillLight);
982
983            const backLight = new THREE.DirectionalLight(0x58a6ff, 0.3);
984            backLight.position.set(-5, 5, -5);
985            scene.add(backLight);
986
987            const loader = new THREE.FBXLoader();
988
989            function loadModel(url) {
990                return new Promise((resolve, reject) => {
991                    loader.load(
992                        url,
993                        (object) => {
994                            console.log(`✅ Loaded: ${url}`);
995                            resolve(object);
996                        },
997                        (xhr) => {
998                            console.log(`${(xhr.loaded / xhr.total * 100).toFixed(1)}% loaded`);
999                        },
1000                        (error) => {
1001                            console.error('❌ FBX加载失败:', error);
1002                            reject(error);
1003                        }
1004                    );
1005                });
1006            }
1007
1008            // 获取模型完整包围盒 - 遍历所有 mesh 并应用世界变换
1009            function getCompleteModelBounds(object) {
1010                const box = new THREE.Box3();
1011                object.updateMatrixWorld(true);
1012                
1013                object.traverse(child => {
1014                    if (child.isMesh && child.geometry) {
1015                        // 计算几何体的包围盒
1016                        child.geometry.computeBoundingBox();
1017                        
1018                        if (child.geometry.boundingBox) {
1019                            // 将 mesh 本地包围盒变换到世界坐标
1020                            const gbox = child.geometry.boundingBox.clone();
1021                            gbox.applyMatrix4(child.matrixWorld);
1022                            box.union(gbox);
1023                        }
1024                    }
1025                });
1026                
1027                // 验证包围盒是否有效
1028                if (!box.isEmpty()) {
1029                    return box;
1030                }
1031                
1032                // 如果没有找到有效的 mesh,返回一个默认的包围盒
1033                console.warn('⚠️ No valid meshes found, using fallback bounding box');
1034                return new THREE.Box3().setFromObject(object);
1035            }
1036
1037            // 创建程序化牛仔布纹理
1038            function createDenimTexture() {
1039                const canvas = document.createElement('canvas');
1040                canvas.width = 256;
1041                canvas.height = 256;
1042                const ctx = canvas.getContext('2d');
1043
1044                // 基础牛仔蓝色
1045                ctx.fillStyle = '#2b4570';
1046                ctx.fillRect(0, 0, 256, 256);
1047
1048                // 添加牛仔布纤维纹理(斜纹)
1049                ctx.strokeStyle = 'rgba(60, 90, 140, 0.4)';
1050                ctx.lineWidth = 1;
1051                for (let i = -256; i < 512; i += 3) {
1052                    ctx.beginPath();
1053                    ctx.moveTo(i, 0);
1054                    ctx.lineTo(i + 256, 256);
1055                    ctx.stroke();
1056                }
1057
1058                // 反向斜纹(交叉编织效果)
1059                ctx.strokeStyle = 'rgba(35, 55, 90, 0.3)';
1060                for (let i = -256; i < 512; i += 4) {
1061                    ctx.beginPath();
1062                    ctx.moveTo(i + 256, 0);
1063                    ctx.lineTo(i, 256);
1064                    ctx.stroke();
1065                }
1066
1067                // 添加随机噪点模拟布料质感
1068                const imageData = ctx.getImageData(0, 0, 256, 256);
1069                for (let i = 0; i < imageData.data.length; i += 4) {
1070                    const noise = (Math.random() - 0.5) * 15;
1071                    imageData.data[i] += noise;
1072                    imageData.data[i + 1] += noise;
1073                    imageData.data[i + 2] += noise;
1074                }
1075                ctx.putImageData(imageData, 0, 0);
1076
1077                const texture = new THREE.CanvasTexture(canvas);
1078                texture.wrapS = THREE.RepeatWrapping;
1079                texture.wrapT = THREE.RepeatWrapping;
1080                texture.repeat.set(4, 4);
1081                return texture;
1082            }
1083
1084            // 调整模型(缩放、居中、材质处理)
1085            function setupModel(object) {
1086                // 先计算所有mesh的几何包围盒
1087                object.traverse((child) => {
1088                    if (child.isMesh && child.geometry) {
1089                        child.geometry.computeBoundingBox();
1090                        child.geometry.computeBoundingSphere();
1091                    }
1092                });
1093                
1094                object.updateMatrixWorld(true);
1095
1096                // 使用完整的包围盒计算方法
1097                const box = getCompleteModelBounds(object);
1098                const size = box.getSize(new THREE.Vector3());
1099                const center = box.getCenter(new THREE.Vector3());
1100
1101                console.log(`📐 Model bounds: ${size.x.toFixed(2)} x ${size.y.toFixed(2)} x ${size.z.toFixed(2)}`);
1102                console.log(`📍 Model center: ${center.x.toFixed(2)}, ${center.y.toFixed(2)}, ${center.z.toFixed(2)}`);
1103
1104                const targetHeight = 1.5;
1105                const scale = targetHeight / size.y;
1106                object.scale.setScalar(scale);
1107
1108                object.updateMatrixWorld(true);
1109                
1110                // 重新计算缩放后的包围盒
1111                const scaledBox = getCompleteModelBounds(object);
1112                const scaledCenter = scaledBox.getCenter(new THREE.Vector3());
1113
1114                // 居中 x 和 z 轴
1115                object.position.x -= scaledCenter.x;
1116                object.position.z -= scaledCenter.z;
1117                
1118                // 将脚底放置在画布底部(y = 0.05,稍微离开底部边缘)
1119                const desiredY = 0.05;
1120                object.position.y = object.position.y - (scaledBox.min.y - desiredY);
1121
1122                // 创建牛仔裤纹理
1123                const denimTexture = createDenimTexture();
1124
1125                object.traverse((child) => {
1126                    if (child.isMesh) {
1127                        child.castShadow = true;
1128                        child.receiveShadow = true;
1129                        if (child.material) {
1130                            child.material.side = THREE.DoubleSide;
1131                            // 减少塑料感:增加粗糙度,降低金属度和高光
1132                            if (child.material.roughness !== undefined) {
1133                                child.material.roughness = Math.max(child.material.roughness, 0.7);
1134                            }
1135                            if (child.material.metalness !== undefined) {
1136                                child.material.metalness = Math.min(child.material.metalness, 0.1);
1137                            }
1138                            if (child.material.shininess !== undefined) {
1139                                child.material.shininess = Math.min(child.material.shininess, 30);
1140                            }
1141                            if (child.material.specular) {
1142                                child.material.specular.setHex(0x222222);
1143                            }
1144
1145                            // 将裤子区域贴图改为牛仔裤效果
1146                            // 使用 onBeforeCompile 注入自定义着色器,根据世界坐标 Y 值混合牛仔布纹理
1147                            child.material.onBeforeCompile = (shader) => {
1148                                shader.uniforms.denimMap = { value: denimTexture };
1149                                shader.uniforms.pantsBottom = { value: PANTS_BOTTOM_Y };
1150                                shader.uniforms.pantsTop = { value: PANTS_TOP_Y };
1151                                shader.uniforms.blendEdge = { value: PANTS_BLEND_EDGE };
1152
1153                                // 顶点着色器:传递世界坐标 Y(支持骨骼动画)
1154                                shader.vertexShader = shader.vertexShader.replace(
1155                                    'void main() {',
1156                                    'varying float vWorldY;\nvoid main() {'
1157                                );
1158                                shader.vertexShader = shader.vertexShader.replace(
1159                                    '#include <project_vertex>',
1160                                    'vWorldY = (modelMatrix * vec4(transformed, 1.0)).y;\n#include <project_vertex>'
1161                                );
1162
1163                                // 片段着色器:在裤子区域混合牛仔布颜色
1164                                shader.fragmentShader = shader.fragmentShader.replace(
1165                                    'void main() {',
1166                                    'uniform sampler2D denimMap;\nuniform float pantsBottom;\nuniform float pantsTop;\nuniform float blendEdge;\nvarying float vWorldY;\nvoid main() {'
1167                                );
1168                                shader.fragmentShader = shader.fragmentShader.replace(
1169                                    '#include <dithering_fragment>',
1170                                    `// 牛仔裤贴图混合
1171                                    float bottomEdge = smoothstep(pantsBottom - blendEdge, pantsBottom + blendEdge, vWorldY);
1172                                    float topEdge = 1.0 - smoothstep(pantsTop - blendEdge, pantsTop + blendEdge, vWorldY);
1173                                    float pantsMask = bottomEdge * topEdge;
1174                                    #ifdef USE_UV
1175                                    vec2 denimUV = vUv * 4.0;
1176                                    #else
1177                                    vec2 denimUV = gl_FragCoord.xy / 64.0;
1178                                    #endif
1179                                    vec3 denimColor = texture2D(denimMap, denimUV).rgb;
1180                                    gl_FragColor.rgb = mix(gl_FragColor.rgb, denimColor, pantsMask * 0.85);
1181                                    #include <dithering_fragment>`
1182                                );
1183                            };
1184                            child.material.needsUpdate = true;
1185                        }
1186                    }
1187                });
1188
1189                object.updateMatrixWorld(true);
1190                
1191                // 最终包围盒计算
1192                const finalBox = getCompleteModelBounds(object);
1193                const finalSize = finalBox.getSize(new THREE.Vector3());
1194                const modelHeight = finalSize.y;
1195                const footY = finalBox.min.y; // 脚底位置
1196                const headY = finalBox.max.y; // 头顶位置
1197                const centerX = (finalBox.min.x + finalBox.max.x) / 2;
1198                const centerZ = (finalBox.min.z + finalBox.max.z) / 2;
1199
1200                // 计算合适的距离和高度,使【头-脚】范围完全出现在视口内,头部居中
1201                const padding = 0.20 * modelHeight; // 20% 留白用于视觉舒适度
1202                const visibleHeight = modelHeight + padding;
1203                const fov = camera.fov * (Math.PI / 180);
1204                // 计算摄像机距离,使整个模型(从头到脚)完全可见
1205                // 摄像机 y 对准头顶,z 轴退后足够距离以看到全身(包括脚)
1206                const camDist = visibleHeight / (2 * Math.tan(fov / 2));
1207                
1208                // Camera target 对准头顶位置,使头在视野中心
1209                // 这样可以确保头部在画布中央,脚部一定可见
1210                controls.target.set(centerX, headY, centerZ);
1211                
1212                // 摄像机位置:x, z 居中,y 设置为头顶高度(与头齐平)
1213                // z 轴退后足够距离,确保从头到脚的完整模型都可见
1214                camera.position.set(centerX, headY, centerZ + camDist);
1215                camera.lookAt(centerX, headY, centerZ);
1216
1217                controls.update();
1218
1219                // 详细的调试日志 - 输出全部坐标与参数
1220                console.log(`📐 Model bounds: ${finalSize.x.toFixed(2)} x ${finalSize.y.toFixed(2)} x ${finalSize.z.toFixed(2)}`);
1221                console.log(`👣 Feet at Y: ${footY.toFixed(2)}, Head at Y: ${headY.toFixed(2)}`);
1222                console.log(`📏 Model height: ${modelHeight.toFixed(2)}, Padding: ${padding.toFixed(2)}`);
1223                console.log(`📏 Visible height: ${visibleHeight.toFixed(2)}`);
1224                console.log(`📷 Camera FOV: ${camera.fov}°, FOV (radians): ${fov.toFixed(4)}`);
1225                console.log(`🔭 Camera distance (z): ${camDist.toFixed(2)}`);
1226                console.log(`📷 Camera position: (${camera.position.x.toFixed(2)}, ${camera.position.y.toFixed(2)}, ${camera.position.z.toFixed(2)})`);
1227                console.log(`🎯 Camera lookAt: (${centerX.toFixed(2)}, ${headY.toFixed(2)}, ${centerZ.toFixed(2)})`);
1228                console.log(`🎯 Controls target: (${controls.target.x.toFixed(2)}, ${controls.target.y.toFixed(2)}, ${controls.target.z.toFixed(2)})`);
1229                console.log(`📊 Center position: X=${centerX.toFixed(2)}, Z=${centerZ.toFixed(2)}`);
1230
1231                return object;
1232            }
1233
1234            // 动画自动交替
1235            function startAnimationLoop() {
1236                const WALK_DURATION = 5000;
1237                const RUN_DURATION = 4000;
1238
1239                async function loop() {
1240                    while (true) {
1241                        if (currentAnimation !== 'Walking' && actions['Walking']) {
1242                            actions[currentAnimation]?.fadeOut(0.5);
1243                            actions['Walking'].reset().fadeIn(0.5).play();
1244                            currentAnimation = 'Walking';
1245                        }
1246                        await new Promise(r => setTimeout(r, WALK_DURATION));
1247
1248                        if (currentAnimation !== 'Running' && actions['Running']) {
1249                            actions[currentAnimation]?.fadeOut(0.5);
1250                            actions['Running'].reset().fadeIn(0.5).play();
1251                            currentAnimation = 'Running';
1252                        }
1253                        await new Promise(r => setTimeout(r, RUN_DURATION));
1254                    }
1255                }
1256                loop();
1257            }
1258
1259            // 窗口调整
1260            function handleResize() {
1261                const newHeight = getCanvasHeight();
1262                if (Math.abs(newHeight - currentHeight) > 5) {
1263                    currentHeight = newHeight;
1264                    canvas.style.height = currentHeight + 'px';
1265                    camera.aspect = CONFIG.width / currentHeight;
1266                    camera.updateProjectionMatrix();
1267                    renderer.setSize(CONFIG.width, currentHeight);
1268                }
1269            }
1270            window.addEventListener('resize', handleResize);
1271            
1272            // 鼠标跟随旋转事件
1273            document.addEventListener('mousemove', (event) => {
1274                // 鼠标 x 归一化到 [-1, 1]
1275                const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
1276                // 同向跟随:鼠标往左(mouseX < 0),模型往左(rotation.y < 0);鼠标往右(mouseX > 0),模型往右(rotation.y > 0)
1277                targetRotationY = mouseX * MAX_ROTATION_RADIANS;
1278            });
1279
1280            async function init() {
1281                try {
1282                    console.log(`[AI Companion] Version: ${AI_COMPANION_VERSION}`);
1283                    console.log('🚀 开始加载 FBX 模型...');
1284                    
1285                    const [walkingFbx, runningFbx] = await Promise.all([
1286                        loadModel('./models/Walking.fbx'),
1287                        loadModel('./models/Standard_Run.fbx')
1288                    ]);
1289
1290                    // 创建动画混合器并设置到初始帧(确保包围盒计算准确)
1291                    mixer = new THREE.AnimationMixer(walkingFbx);
1292                    
1293                    if (walkingFbx.animations?.length) {
1294                        actions['Walking'] = mixer.clipAction(walkingFbx.animations[0]);
1295                        actions['Walking'].setLoop(THREE.LoopRepeat);
1296                        // 设置到初始帧但不播放 (reset() 已经将动画设为初始状态)
1297                        actions['Walking'].reset();
1298                        console.log(`🎬 Walking animation: ${walkingFbx.animations.length} clips`);
1299                    }
1300                    
1301                    if (runningFbx.animations?.length) {
1302                        actions['Running'] = mixer.clipAction(runningFbx.animations[0]);
1303                        actions['Running'].setLoop(THREE.LoopRepeat);
1304                        console.log(`🎬 Running animation: ${runningFbx.animations.length} clips`);
1305                    }
1306
1307                    // 在动画暂停状态下设置模型位置
1308                    model = setupModel(walkingFbx);
1309                    scene.add(model);
1310
1311                    // 现在可以开始播放动画
1312                    if (actions['Walking']) {
1313                        actions['Walking'].play();
1314                        startAnimationLoop();
1315                    }
1316
1317                    // 渲染循环
1318                    (function animate() {
1319                        requestAnimationFrame(animate);
1320                        const delta = clock.getDelta();
1321                        if (mixer) mixer.update(delta);
1322                        
1323                        // 平滑旋转 - 鼠标跟随
1324                        if (model) {
1325                            model.rotation.y += (targetRotationY - model.rotation.y) * ROTATION_SMOOTHING_FACTOR;
1326                        }
1327                        
1328                        controls.update();
1329                        renderer.render(scene, camera);
1330                    })();
1331
1332                    console.log('✅ AI Companion 初始化完成!');
1333
1334                } catch (e) {
1335                    console.error('❌ 加载模型失败:', e);
1336                }
1337            }
1338
1339            // 初始化
1340            canvas.style.height = currentHeight + 'px';
1341            init();
1342        })();
1343    </script>
1343
1344</body>
1345</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.