<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>藤原的个人终端</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap">
<style>
:root {
--editor-bg: rgba(30, 30, 30, 0.8);
--editor-secondary-bg: rgba(37, 37, 38, 0.8);
--line-color: rgba(133, 133, 133, 0.7);
--text-color: #d4d4d4;
--keyword-color: #569cd6;
--string-color: #ce9178;
--comment-color: #6a9955;
--number-color: #b5cea8;
--function-color: #dcdcaa;
--font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
--header-bg: rgba(45, 45, 45, 0.8);
--window-btn-close: #ff5f56;
--window-btn-min: #ffbd2e;
--window-btn-max: #27c93f;
--ai-output-color: #4ec9b0;
--user-input-color: #d7ba7d;
--video-bg: rgba(0, 0, 0, 0.8);
--accent-color: #0a84ff;
--accent-hover: #409eff;
--box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
--border-radius: 8px;
--calendar-today-bg: rgba(10, 132, 255, 0.3);
--calendar-selected-bg: var(--accent-color);
--weather-temp-high: #ff7e67;
--weather-temp-low: #70c1ff;
--birthday-color: #ff4081;
--birthday-bg: rgba(255, 64, 129, 0.15);
--success-color: #4caf50;
--warning-color: #ff9800;
--error-color: #f44336;
--tool-header-height: 40px;
--tool-padding: 16px;
}
[data-theme="light"] {
--editor-bg: rgba(245, 245, 245, 0.8);
--editor-secondary-bg: rgba(235, 235, 235, 0.8);
--line-color: rgba(133, 133, 133, 0.7);
--text-color: #333333;
--keyword-color: #0000ff;
--string-color: #a31515;
--comment-color: #008000;
--number-color: #098658;
--function-color: #795e26;
--header-bg: rgba(229, 229, 229, 0.8);
--ai-output-color: #098658;
--user-input-color: #0000ff;
--video-bg: rgba(240, 240, 240, 0.8);
--accent-color: #0a84ff;
--accent-hover: #409eff;
--box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
--calendar-today-bg: rgba(10, 132, 255, 0.2);
--calendar-selected-bg: var(--accent-color);
--weather-temp-high: #ff5a3d;
--weather-temp-low: #4a9eff;
--birthday-color: #ff4081;
--birthday-bg: rgba(255, 64, 129, 0.1);
--success-color: #2e7d32;
--warning-color: #e65100;
--error-color: #c62828;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #1a1a1a;
color: var(--text-color);
font-family: var(--font-family);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
transition: background-color 0.3s, color 0.3s;
background-image:
radial-gradient(circle at 10% 20%, rgba(100, 100, 255, 0.1) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(100, 255, 100, 0.1) 0%, transparent 20%);
background-attachment: fixed;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(26, 26, 26, 0.9) 0%, rgba(40, 40, 40, 0.9) 100%);
z-index: -1;
}
.theme-switch {
position: fixed;
top: 20px;
right: 20px;
background: var(--header-bg);
border-radius: 30px;
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
z-index: 100;
box-shadow: var(--box-shadow);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.theme-switch:hover {
transform: translateY(-2px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
}
.theme-icon {
width: 18px;
height: 18px;
transition: transform 0.3s ease;
}
.theme-switch:hover .theme-icon {
transform: rotate(30deg);
}
.editor {
width: 100%;
max-width: 900px;
margin: 20px auto;
background: var(--editor-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
transition: all 0.3s;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
height: 80vh;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.editor:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4);
}
.editor-header {
background: var(--header-bg);
padding: 12px;
border-radius: var(--border-radius) var(--border-radius) 0 0;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 0.3s;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.window-btn {
width: 12px;
height: 12px;
border-radius: 50%;
transition: all 0.2s;
position: relative;
cursor: pointer;
}
.window-btn:hover::after {
content: '';
position: absolute;
bottom: 120%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
}
.close { background: var(--window-btn-close); }
.minimize { background: var(--window-btn-min); }
.maximize { background: var(--window-btn-max); }
.title-bar {
color: var(--line-color);
margin-left: 20px;
font-size: 0.9em;
transition: color 0.3s;
}
.editor-content {
padding: 20px;
counter-reset: line;
font-size: 14px;
overflow-x: auto;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-color) transparent;
flex: 1;
}
.editor-content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.editor-content::-webkit-scrollbar-track {
background: transparent;
}
.editor-content::-webkit-scrollbar-thumb {
background-color: var(--line-color);
border-radius: 4px;
}
.line {
display: flex;
padding: 2px 0;
position: relative;
min-height: 1.6em;
}
.line::before {
counter-increment: line;
content: counter(line);
color: var(--line-color);
width: 2em;
text-align: right;
padding-right: 1em;
position: absolute;
transition: color 0.3s;
opacity: 0.7;
}
.line-content {
padding-left: 3em;
width: 100%;
}
.indent { margin-left: 2em; }
.indent-2 { margin-left: 4em; }
.keyword { color: var(--keyword-color); }
.string { color: var(--string-color); }
.comment { color: var(--comment-color); }
.number { color: var(--number-color); }
.function { color: var(--function-color); }
.ai-output { color: var(--ai-output-color); }
.user-input { color: var(--user-input-color); }
a {
color: var(--accent-color);
text-decoration: none;
transition: all 0.2s;
position: relative;
}
a:hover {
color: var(--accent-hover);
}
a::after {
content: '';
position: absolute;
width: 0;
height: 1px;
bottom: 0;
left: 0;
background-color: var(--accent-hover);
transition: width 0.3s ease;
}
a:hover::after {
width: 100%;
}
.cursor {
display: inline-block;
width: 2px;
height: 1.2em;
background: var(--text-color);
margin-left: 2px;
animation: blink 1s step-end infinite;
vertical-align: middle;
transition: background 0.3s;
}
@keyframes blink {
50% { opacity: 0; }
}
.input-line {
display: flex;
align-items: center;
background: var(--editor-secondary-bg);
padding: 8px 10px;
border-radius: 4px;
transition: all 0.3s;
margin: 0 20px 20px;
position: sticky;
bottom: 0;
z-index: 10;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.input-line:focus-within {
box-shadow: 0 0 0 2px var(--accent-color);
}
.input-line::before {
content: ">";
color: var(--accent-color);
width: 2em;
text-align: right;
padding-right: 1em;
font-weight: bold;
}
.input-line input {
flex: 1;
background: transparent;
border: none;
color: var(--text-color);
font-family: var(--font-family);
font-size: 14px;
outline: none;
padding: 2px 0;
}
/* ================== 视频播放器 ================== */
.video-container {
margin-top: 20px;
width: 100%;
background: var(--video-bg);
border-radius: var(--border-radius);
overflow: hidden;
transition: all 0.3s;
box-shadow: var(--box-shadow);
display: flex;
flex-direction: column;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.video-player {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: #000;
}
.video-player video {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
}
.video-controls {
display: flex;
flex-direction: column;
background: var(--editor-secondary-bg);
padding: var(--tool-padding);
gap: 12px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.video-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85em;
color: var(--text-color);
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.video-status {
color: var(--line-color);
font-size: 0.8em;
}
.video-main-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.video-button-group {
display: flex;
gap: 8px;
}
.video-button {
background: var(--header-bg);
color: var(--text-color);
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.video-button:hover {
background: var(--accent-color);
color: white;
}
.video-button svg {
width: 14px;
height: 14px;
}
.video-timeline {
flex: 1;
height: 6px;
background: var(--editor-bg);
border-radius: 3px;
position: relative;
cursor: pointer;
overflow: hidden;
}
.video-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
background: var(--accent-color);
border-radius: 3px;
}
.video-time {
font-size: 0.8em;
color: var(--line-color);
min-width: 80px;
text-align: right;
}
.video-advanced-controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 8px;
}
.video-api-selector {
grid-column: span 2;
}
.video-api-select {
width: 100%;
background: var(--editor-bg);
color: var(--text-color);
padding: 8px 12px;
border-radius: 4px;
border: 1px solid var(--line-color);
font-family: var(--font-family);
font-size: 0.85em;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.video-api-option {
background: var(--editor-bg);
}
/* ================== 图片转换器 ================== */
.image-converter {
margin-top: 20px;
width: 100%;
max-width: 600px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
padding: 0;
transition: all 0.3s;
box-shadow: var(--box-shadow);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-header {
background: var(--header-bg);
padding: 12px var(--tool-padding);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-title {
font-size: 1em;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
}
.image-title svg {
width: 20px;
height: 20px;
}
.image-close {
background: none;
border: none;
color: var(--line-color);
cursor: pointer;
transition: color 0.2s;
padding: 4px;
border-radius: 4px;
}
.image-close:hover {
background: var(--editor-bg);
color: var(--error-color);
}
.image-content {
padding: var(--tool-padding);
}
.image-preview-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
background: var(--editor-bg);
border-radius: var(--border-radius);
padding: 16px;
border: 1px dashed var(--line-color);
min-height: 200px;
justify-content: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-preview {
max-width: 100%;
max-height: 300px;
margin-bottom: 15px;
border-radius: var(--border-radius);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s;
display: none;
}
.image-preview.active {
display: block;
}
.image-placeholder {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--line-color);
text-align: center;
padding: 20px;
transition: all 0.3s;
}
.image-placeholder svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.image-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
background: var(--editor-bg);
padding: 15px;
border-radius: var(--border-radius);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-control {
display: flex;
flex-direction: column;
gap: 8px;
}
.image-control label {
font-size: 0.85em;
color: var(--line-color);
display: flex;
align-items: center;
gap: 6px;
}
.image-control input,
.image-control select {
background: var(--editor-secondary-bg);
border: 1px solid var(--line-color);
color: var(--text-color);
padding: 8px 12px;
border-radius: 4px;
font-family: var(--font-family);
transition: all 0.2s;
width: 100%;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-control input:focus,
.image-control select:focus {
border-color: var(--accent-color);
outline: none;
box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.3);
}
.image-control input[type="range"] {
-webkit-appearance: none;
height: 6px;
background: var(--editor-bg);
border-radius: 3px;
padding: 0;
}
.image-control input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent-color);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.image-control input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.image-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
flex-wrap: wrap;
}
.image-btn {
background: var(--accent-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
font-family: var(--font-family);
font-weight: bold;
transition: all 0.2s;
min-width: 120px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.image-btn svg {
width: 16px;
height: 16px;
}
.image-btn:hover {
background: var(--accent-hover);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(10, 132, 255, 0.3);
}
.image-btn.secondary {
background: var(--editor-secondary-bg);
color: var(--text-color);
}
.image-btn.secondary:hover {
background: var(--header-bg);
box-shadow: none;
}
.image-download-link {
display: none;
}
/* ================== 日历组件 ================== */
.calendar-container {
margin-top: 20px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
padding: 0;
box-shadow: var(--box-shadow);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
overflow: hidden;
width: 100%;
max-width: 400px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px var(--tool-padding);
background: var(--header-bg);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.calendar-title {
font-size: 0.95em;
font-weight: bold;
display: flex;
align-items: center;
gap: 6px;
}
.calendar-title svg {
width: 18px;
height: 18px;
}
.calendar-nav {
display: flex;
gap: 8px;
}
.calendar-btn {
background: var(--editor-bg);
border: none;
color: var(--text-color);
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 0.8em;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.calendar-btn:hover {
background: var(--accent-color);
color: white;
}
.calendar-btn svg {
width: 16px;
height: 16px;
}
.calendar-content {
padding: 12px;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 12px;
}
.calendar-weekday {
text-align: center;
font-size: 0.75em;
color: var(--line-color);
padding: 6px 0;
font-weight: bold;
}
.calendar-day {
text-align: center;
padding: 8px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
position: relative;
font-size: 0.85em;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.calendar-day:hover {
background: var(--editor-bg);
}
.calendar-day.today {
background: var(--calendar-today-bg);
font-weight: bold;
}
.calendar-day.selected {
background: var(--calendar-selected-bg);
color: white;
}
.calendar-day.other-month {
color: var(--line-color);
opacity: 0.5;
}
.calendar-day.birthday {
color: var(--birthday-color);
font-weight: bold;
}
.calendar-day.birthday::after {
content: '🎂';
font-size: 0.6em;
position: absolute;
bottom: 0;
right: 2px;
}
.calendar-day.birthday:hover {
background: var(--birthday-bg);
}
.calendar-events {
margin-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 12px;
}
.calendar-events-title {
font-size: 0.85em;
margin-bottom: 8px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 6px;
}
.calendar-events-title svg {
width: 16px;
height: 16px;
}
.calendar-event {
background: var(--editor-bg);
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
font-size: 0.85em;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.calendar-event:hover {
transform: translateX(2px);
}
.calendar-event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-color);
flex-shrink: 0;
}
.calendar-event-content {
flex: 1;
}
.calendar-event-time {
font-size: 0.8em;
color: var(--line-color);
min-width: 50px;
text-align: right;
}
.calendar-event.birthday-event {
background: var(--birthday-bg);
border-left: 3px solid var(--birthday-color);
}
.calendar-event.birthday-event .calendar-event-dot {
background: var(--birthday-color);
}
.calendar-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
/* ================== 天气组件 ================== */
.weather-container {
margin-top: 20px;
width: 100%;
max-width: 600px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
padding: 0;
box-shadow: var(--box-shadow);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
overflow: hidden;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px var(--tool-padding);
background: var(--header-bg);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.weather-location {
font-size: 1.1em;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
}
.weather-location svg {
width: 20px;
height: 20px;
}
.weather-refresh {
background: var(--editor-bg);
border: none;
color: var(--text-color);
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.weather-refresh:hover {
background: var(--accent-color);
color: white;
transform: rotate(180deg);
}
.weather-refresh svg {
width: 18px;
height: 18px;
}
.weather-content {
padding: var(--tool-padding);
}
.weather-current {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 20px;
}
.weather-icon {
width: 64px;
height: 64px;
flex-shrink: 0;
}
.weather-temp {
font-size: 2.5em;
font-weight: bold;
}
.weather-desc {
display: flex;
flex-direction: column;
gap: 4px;
}
.weather-condition {
font-size: 1.1em;
}
.weather-feels-like {
font-size: 0.9em;
color: var(--line-color);
}
.weather-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.weather-detail {
background: var(--editor-bg);
padding: 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.weather-detail:hover {
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
.weather-detail-icon {
width: 24px;
height: 24px;
opacity: 0.7;
flex-shrink: 0;
}
.weather-detail-content {
flex: 1;
}
.weather-detail-title {
font-size: 0.8em;
color: var(--line-color);
margin-bottom: 4px;
}
.weather-detail-value {
font-weight: bold;
font-size: 1.1em;
}
.weather-forecast {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin-top: 20px;
}
.forecast-day {
background: var(--editor-bg);
padding: 12px;
border-radius: 6px;
text-align: center;
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.forecast-day:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.forecast-date {
font-size: 0.85em;
color: var(--line-color);
margin-bottom: 8px;
}
.forecast-icon {
width: 32px;
height: 32px;
margin: 8px auto;
}
.forecast-temp {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.forecast-high {
color: var(--weather-temp-high);
font-weight: bold;
}
.forecast-low {
color: var(--weather-temp-low);
font-size: 0.9em;
}
.weather-search {
margin-top: 15px;
display: flex;
gap: 10px;
}
.weather-search input {
flex: 1;
background: var(--editor-bg);
border: 1px solid var(--line-color);
color: var(--text-color);
padding: 10px 12px;
border-radius: 4px;
font-family: var(--font-family);
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.weather-search input:focus {
border-color: var(--accent-color);
outline: none;
box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.3);
}
.weather-search button {
background: var(--accent-color);
color: white;
border: none;
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-family: var(--font-family);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.weather-search button:hover {
background: var(--accent-hover);
}
.weather-search button svg {
width: 16px;
height: 16px;
}
/* ================== 命令面板 ================== */
.command-palette {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 600px;
background: var(--editor-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
z-index: 1000;
display: none;
border: 1px solid rgba(255, 255, 255, 0.1);
max-height: 70vh;
overflow: hidden;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.command-palette-header {
background: var(--header-bg);
padding: 8px 12px;
border-radius: var(--border-radius) var(--border-radius) 0 0;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.command-palette .window-btn {
width: 10px;
height: 10px;
margin-right: 6px;
flex-shrink: 0;
}
.command-palette-title {
color: var(--line-color);
font-size: 0.8em;
flex: 1;
}
.command-palette-content {
padding: 12px;
max-height: calc(70vh - 40px);
overflow-y: auto;
}
.command-header {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
position: sticky;
top: 0;
background: var(--editor-bg);
padding: 5px 0;
z-index: 10;
}
.command-search {
flex: 1;
background: var(--editor-secondary-bg);
border: 1px solid var(--line-color);
color: var(--text-color);
padding: 8px 12px;
border-radius: 4px;
font-family: var(--font-family);
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.command-search:focus {
border-color: var(--accent-color);
outline: none;
box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.3);
}
.command-list {
max-height: 50vh;
overflow-y: auto;
}
.command-item {
padding: 8px 10px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0;
transition: all 0.2s;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.command-item:hover {
background: var(--editor-secondary-bg);
}
.command-item.selected {
background: var(--accent-color);
color: white;
}
.command-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.command-content {
flex: 1;
}
.command-title {
font-weight: bold;
font-size: 0.9em;
}
.command-description {
font-size: 0.8em;
color: var(--line-color);
margin-top: 2px;
}
.command-item.selected .command-description {
color: rgba(255, 255, 255, 0.8);
}
.command-shortcut {
margin-left: auto;
color: var(--line-color);
font-size: 0.75em;
background: var(--editor-secondary-bg);
padding: 3px 6px;
border-radius: 4px;
white-space: nowrap;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.command-item.selected .command-shortcut {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.command-category {
font-size: 0.75em;
color: var(--line-color);
padding: 6px 10px;
margin-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 6px;
position: sticky;
top: 0;
background: var(--editor-bg);
z-index: 5;
}
.command-category:first-child {
border-top: none;
margin-top: 0;
}
.command-category svg {
width: 12px;
height: 12px;
opacity: 0.7;
}
/* ================== 命令历史记录 ================== */
.command-history {
margin-top: 20px;
background: var(--editor-secondary-bg);
border-radius: var(--border-radius);
padding: 15px;
display: none;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.history-title {
font-size: 0.9em;
margin-bottom: 10px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 6px;
}
.history-title svg {
width: 16px;
height: 16px;
}
.history-list {
max-height: 200px;
overflow-y: auto;
}
.history-item {
padding: 8px 0;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.history-item:last-child {
border-bottom: none;
}
.history-item:hover {
color: var(--accent-color);
transform: translateX(2px);
}
.history-item::before {
content: ">";
margin-right: 10px;
color: var(--line-color);
font-weight: bold;
}
/* ================== 工具提示 ================== */
.tooltip {
position: relative;
}
.tooltip:hover::before {
content: attr(data-tooltip);
position: absolute;
bottom: 120%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--header-bg);
color: var(--text-color);
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8em;
white-space: nowrap;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
z-index: 100;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
/* ================== 打字机动画 ================== */
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
.typing-animation {
overflow: hidden;
white-space: nowrap;
animation: typing 1s steps(30, end);
}
/* ================== AI思考动画 ================== */
.thinking-animation {
display: inline-block;
position: relative;
width: 80px;
height: 20px;
}
.thinking-animation span {
position: absolute;
width: 8px;
height: 8px;
background: var(--ai-output-color);
border-radius: 50%;
animation: thinking 1.5s infinite ease-in-out;
}
.thinking-animation span:nth-child(1) {
left: 0;
animation-delay: 0s;
}
.thinking-animation span:nth-child(2) {
left: 15px;
animation-delay: 0.2s;
}
.thinking-animation span:nth-child(3) {
left: 30px;
animation-delay: 0.4s;
}
@keyframes thinking {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* ================== 响应式设计 ================== */
@media (max-width: 768px) {
body {
padding: 10px;
}
.editor {
width: 100%;
margin: 0;
font-size: 14px;
height: 90vh;
}
.editor-content {
padding: 15px 10px;
}
.line-content {
padding-left: 2.5em;
}
.indent { margin-left: 1.5em; }
.indent-2 { margin-left: 3em; }
.title-bar {
font-size: 0.8em;
margin-left: 10px;
}
.window-btn {
width: 10px;
height: 10px;
}
.theme-switch {
top: 10px;
right: 10px;
padding: 5px 10px;
font-size: 0.8em;
}
.image-controls {
grid-template-columns: 1fr;
}
.weather-details {
grid-template-columns: 1fr;
}
.weather-forecast {
grid-template-columns: repeat(3, 1fr);
}
.calendar-grid {
gap: 2px;
}
.calendar-day {
padding: 5px;
font-size: 0.9em;
}
.command-palette {
width: 95%;
max-height: 80vh;
}
.video-advanced-controls {
grid-template-columns: 1fr;
}
.video-api-selector {
grid-column: span 1;
}
}
@media (max-width: 480px) {
.editor-content {
font-size: 12px;
}
.indent { margin-left: 1em; }
.indent-2 { margin-left: 2em; }
.weather-forecast {
grid-template-columns: repeat(2, 1fr);
}
.command-palette {
max-height: 85vh;
}
.video-main-controls {
flex-direction: column;
gap: 8px;
}
.video-button-group {
width: 100%;
justify-content: space-between;
}
.video-button {
flex: 1;
justify-content: center;
}
}
</style>
</head>
<body>
<!-- 个人终端 -->
<div class="editor" id="mainEditor">
<div class="editor-header">
<span class="window-btn close" data-tooltip="关闭" id="closeBtn"></span>
<span class="window-btn minimize" data-tooltip="最小化" id="minimizeBtn"></span>
<span class="window-btn maximize" data-tooltip="最大化" id="maximizeBtn"></span>
<span class="title-bar">terminal.js - 藤原的个人终端</span>
</div>
<div class="editor-content" id="editorContent">
<div class="line"><div class="line-content"><span class="comment">// 个人信息配置</span></div></div>
<div class="line"><div class="line-content"><span class="keyword">const</span> <span class="function">profile</span> = {</div></div>
<div class="line"><div class="line-content indent"><span class="keyword">name</span>: <span class="string">"藤原"</span>,</div></div>
<div class="line"><div class="line-content indent"><span class="keyword">title</span>: <span class="string">"职场牛马!!!"</span>,</div></div>
<div class="line"><div class="line-content indent"><span class="keyword">contact</span>: {</div></div>
<div class="line"><div class="line-content indent-2"><span class="keyword">email</span>: <span class="string"><a href="mailto:2083737075@qq.com">"2083737075@qq.com"</a></span>,</div></div>
<div class="line"><div class="line-content indent-2"><span class="keyword">website</span>: <span class="string"><a href="http://tengyuan.icu" target="_blank">"TengYuan.icu"</a></span>,</div></div>
<div class="line"><div class="line-content indent-2"><span class="keyword">FileCodeBox</span>: <span class="string"><a href="http://wp.tengyuan.icu/" target="_blank">"wp.tengyuan.icu"</a></span>,</div></div>
<div class="line"><div class="line-content indent">},</div></div>
<div class="line"><div class="line-content indent"><span class="keyword">links</span>: {</div></div>
<div class="line"><div class="line-content indent-2"><span class="keyword">travel blog</span>: <span class="string"><a href="http://blog.tengyuan.icu/" target="_blank">"blog.tengyuan.icu"</a></span>,</div></div>
<div class="line"><div class="line-content indent-2"><span class="keyword">birthday</span>: <span class="string"><a href="http://sr.0814.cn" target="_blank">"2001/11/01"</a></span>,</div></div>
<div class="line"><div class="line-content indent">},</div></div>
<div class="line"><div class="line-content indent"><span class="comment">// 座右铭</span></div></div>
<div class="line"><div class="line-content indent"><span class="keyword">motto</span>: <span class="string">"以清简代码,筑玖维数字宇宙。"</span>,</div></div>
<div class="line"><div class="line-content indent"><span class="keyword">copyright</span>: <span class="string">"2017-<span id="year"></span> 藤原"</span>,</div></div>
<div class="line"><div class="line-content">};</div></div>
<div class="line"><div class="line-content"><span class="comment">// 终端交互</span></div></div>
<div class="line"><div class="line-content"><span class="function">console</span>.<span class="function">log</span>(<span class="string">"欢迎访问藤原的个人终端"</span>);</div></div>
<div class="line"><div class="line-content"><span class="function">console</span>.<span class="function">log</span>(<span class="string">"输入 'help' 获取可用命令"</span>);</div></div>
<!-- 命令历史记录 -->
<div class="command-history" id="commandHistory">
<div class="history-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
命令历史记录
</div>
<div class="history-list" id="historyList"></div>
</div>
</div>
<!-- 输入行 -->
<div class="input-line">
<input type="text" id="userInput" placeholder="输入命令..." autocomplete="off">
</div>
</div>
<!-- 命令面板 -->
<div class="command-palette" id="commandPalette">
<div class="command-palette-header">
<span class="window-btn close" data-tooltip="关闭" id="closeCommandPaletteBtn"></span>
<span class="window-btn minimize" data-tooltip="最小化"></span>
<span class="window-btn maximize" data-tooltip="最大化"></span>
<span class="command-palette-title">command-palette.js - 命令面板</span>
</div>
<div class="command-palette-content">
<div class="command-header">
<input type="text" class="command-search" id="commandSearch" placeholder="搜索命令...">
</div>
<div class="command-list" id="commandList">
<!-- 命令将通过JS动态添加 -->
</div>
</div>
</div>
<!-- 隐藏的文件输入元素 -->
<input type="file" id="fileInput" accept="image/*" style="display: none;">
<script>
// 主题切换功能
let themeSwitch = document.getElementById('themeSwitch');
let themeText;
if (!themeSwitch) {
themeSwitch = document.createElement('div');
themeSwitch.id = 'themeSwitch';
themeText = document.createElement('span');
themeText.id = 'themeText';
themeSwitch.appendChild(themeText);
document.body.appendChild(themeSwitch);
} else {
themeText = document.getElementById('themeText');
}
const body = document.body;
// 检查本地存储中的主题设置
const currentTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', currentTheme);
themeText.textContent = currentTheme === 'dark' ? '切换主题' : '夜间模式';
themeSwitch.addEventListener('click', () => {
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeText.textContent = newTheme === 'dark' ? '切换主题' : '夜间模式';
});
// 设置当前年份
const currentYear = new Date().getFullYear();
document.getElementById('year').textContent = currentYear;
// 终端交互功能
const editorContent = document.getElementById('editorContent');
const userInput = document.getElementById('userInput');
const inputLine = document.querySelector('.input-line');
const fileInput = document.getElementById('fileInput');
const commandPalette = document.getElementById('commandPalette');
const commandSearch = document.getElementById('commandSearch');
const commandList = document.getElementById('commandList');
const commandHistory = document.getElementById('commandHistory');
const historyList = document.getElementById('historyList');
const closeBtn = document.getElementById('closeBtn');
const minimizeBtn = document.getElementById('minimizeBtn');
const maximizeBtn = document.getElementById('maximizeBtn');
const closeCommandPaletteBtn = document.getElementById('closeCommandPaletteBtn');
const mainEditor = document.getElementById('mainEditor');
// 命令历史记录
let commandHistoryArray = JSON.parse(localStorage.getItem('commandHistory') || '[]');
let historyIndex = -1;
function updateHistoryUI() {
historyList.innerHTML = '';
commandHistoryArray.slice().reverse().forEach((cmd, index) => {
const item = document.createElement('div');
item.className = 'history-item';
item.textContent = cmd;
item.addEventListener('click', () => {
userInput.value = cmd;
userInput.focus();
});
historyList.appendChild(item);
});
}
function saveToHistory(command) {
if (commandHistoryArray.length === 0 || commandHistoryArray[commandHistoryArray.length - 1] !== command) {
commandHistoryArray.push(command);
if (commandHistoryArray.length > 20) {
commandHistoryArray.shift();
}
localStorage.setItem('commandHistory', JSON.stringify(commandHistoryArray));
updateHistoryUI();
}
historyIndex = -1;
}
// AI接口配置
const aiApis = [
{
id: 'api1',
name: 'DeepSeek API',
description: '需要输入API密钥',
requireKey: true,
endpoint: async function(key, message) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${key}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "user",
content: message
}
],
temperature: 0.7
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
code: 200,
msg: data.choices[0].message.content
};
} catch (error) {
return {
code: 500,
msg: `请求失败: ${error.message}`
};
}
}
},
{
id: 'api2',
name: '备用API (默认)',
description: '无需密钥',
requireKey: false,
endpoint: async function(key, message) {
try {
const response = await fetch(`https://cn.apihz.cn/api/ai/wxtiny.php?id=10003788&key=ffa8afb46dce4916c5a74fd73c8de9f6&words=${encodeURIComponent(message)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
code: data.code || 200,
msg: data.msg || data.content || 'AI回复为空'
};
} catch (error) {
return {
code: 500,
msg: `请求失败: ${error.message}`
};
}
}
}
];
let currentApi = aiApis[1]; // 默认使用备用API
// 视频解析接口
const videoApis = [
{ name: "默认线路【推荐】", url: "https://jx.xmflv.com/?url=" },
{ name: "线路一【推荐】", url: "https://jx.we-vip.com/?url=" },
{ name: "线路二【推荐】", url: "https://z1.m1907.top/?jx=" },
{ name: "线路三", url: "https://www.ckplayer.vip/jiexi/?url=" },
{ name: "线路五", url: "https://jx.jsonplayer.com/player/?url=" },
{ name: "线路六", url: "https://jx.4kdv.com/?url=" },
{ name: "线路七", url: "https://api.jiexi.la/?url=" },
{ name: "线路八", url: "https://jx.qqwtt.com/?url=" },
{ name: "线路九", url: "https://www.playm3u8.cn/jiexi.php?url=" },
{ name: "线路十", url: "https://www.8090g.cn/jiexi/?url=" }
];
// 命令定义
const commands = [
{
id: 'help',
name: '帮助',
description: '显示所有可用命令',
category: '基础',
shortcut: 'help'
},
{
id: 'clear',
name: '清空终端',
description: '清除终端中的所有内容',
category: '基础',
shortcut: 'Ctrl+L'
},
{
id: 'history',
name: '历史记录',
description: '显示之前执行的命令',
category: '基础',
shortcut: 'history'
},
{
id: 'theme',
name: '切换主题',
description: '在深色和浅色主题之间切换',
category: '基础',
shortcut: 'theme'
},
{
id: 'video',
name: '视频播放器',
description: '解析并播放视频',
category: '工具',
shortcut: 'video [url]'
},
{
id: 'ai',
name: 'AI助手',
description: '与AI助手对话 (使用 --api [api名称] 切换接口)',
category: '工具',
shortcut: 'ai [message]'
},
{
id: 'image',
name: '图片转换',
description: '转换图片格式 (支持: jpg, png, webp, bmp)',
category: '工具',
shortcut: 'image [format]'
},
{
id: 'calendar',
name: '日历',
description: '显示日历和事件管理',
category: '新功能',
shortcut: 'calendar'
},
{
id: 'weather',
name: '天气',
description: '查看天气预报',
category: '新功能',
shortcut: 'weather [城市]'
},
{
id: 'set-api',
name: '设置AI接口',
description: '设置当前使用的AI接口',
category: '配置',
shortcut: 'ai --api [api名称]'
}
];
// 初始化命令面板
function initCommandPalette() {
commandList.innerHTML = '';
const categories = [...new Set(commands.map(cmd => cmd.category))];
categories.forEach(category => {
const categoryCommands = commands.filter(cmd => cmd.category === category);
const categoryTitle = document.createElement('div');
categoryTitle.className = 'command-category';
categoryTitle.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
${getCategoryIcon(category)}
</svg>
${category}
`;
commandList.appendChild(categoryTitle);
categoryCommands.forEach(cmd => {
const item = document.createElement('div');
item.className = 'command-item';
item.dataset.id = cmd.id;
item.dataset.category = cmd.category;
const icon = document.createElement('div');
icon.className = 'command-icon';
icon.innerHTML = getCommandIcon(cmd.id);
const content = document.createElement('div');
content.className = 'command-content';
const title = document.createElement('div');
title.className = 'command-title';
title.textContent = cmd.name;
const description = document.createElement('div');
description.className = 'command-description';
description.textContent = cmd.description;
content.appendChild(title);
content.appendChild(description);
const shortcut = document.createElement('div');
shortcut.className = 'command-shortcut';
shortcut.textContent = cmd.shortcut || '';
item.appendChild(icon);
item.appendChild(content);
item.appendChild(shortcut);
item.addEventListener('click', () => {
executeCommandFromPalette(cmd);
});
commandList.appendChild(item);
});
});
}
function getCategoryIcon(category) {
switch(category) {
case '基础': return '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline>';
case '工具': return '<rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect>';
case '新功能': return '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>';
case '配置': return '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>';
default: return '<circle cx="12" cy="12" r="10"></circle>';
}
}
function getCommandIcon(cmdId) {
switch(cmdId) {
case 'help': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
case 'clear': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
case 'video': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>';
case 'ai': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>';
case 'image': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>';
case 'history': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
case 'theme': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
case 'calendar': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>';
case 'weather': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>';
case 'set-api': return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>';
default: return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>';
}
}
function executeCommandFromPalette(cmd) {
commandPalette.style.display = 'none';
switch(cmd.id) {
case 'help':
case 'clear':
case 'history':
case 'calendar':
userInput.value = cmd.id;
break;
case 'video':
case 'ai':
case 'image':
case 'weather':
userInput.value = cmd.id + ' ';
break;
case 'theme':
themeSwitch.click();
return;
case 'set-api':
userInput.value = 'ai --api ';
break;
default:
return;
}
userInput.focus();
if (['help', 'clear', 'history', 'calendar'].includes(cmd.id)) {
const event = new KeyboardEvent('keypress', {'key': 'Enter'});
userInput.dispatchEvent(event);
}
}
function filterCommands(query) {
const items = commandList.querySelectorAll('.command-item');
const categories = commandList.querySelectorAll('.command-category');
query = query.toLowerCase();
let firstVisible = null;
let visibleCategories = new Set();
categories.forEach(category => {
category.style.display = 'none';
});
items.forEach(item => {
const cmd = commands.find(c => c.id === item.dataset.id);
const matchesName = cmd.name.toLowerCase().includes(query);
const matchesId = cmd.id.toLowerCase().includes(query);
const matchesDesc = cmd.description.toLowerCase().includes(query);
const matchesCategory = cmd.category.toLowerCase().includes(query);
if (matchesName || matchesId || matchesDesc || matchesCategory) {
item.style.display = 'flex';
visibleCategories.add(cmd.category);
if (!firstVisible) {
firstVisible = item;
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
} else {
item.style.display = 'none';
item.classList.remove('selected');
}
});
categories.forEach(category => {
if (visibleCategories.has(category.textContent.trim())) {
category.style.display = 'flex';
}
});
if (!query) {
categories.forEach(category => {
category.style.display = 'flex';
});
}
}
// 初始化键盘事件
function initKeyboardEvents() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
commandPalette.style.display = 'block';
commandSearch.value = '';
filterCommands('');
commandSearch.focus();
return;
}
if (e.ctrlKey && e.key === 'l') {
e.preventDefault();
clearTerminal();
return;
}
if (e.key === 'Escape') {
commandPalette.style.display = 'none';
userInput.focus();
return;
}
if (commandPalette.style.display === 'block') {
const items = Array.from(commandList.querySelectorAll('.command-item')).filter(
item => item.style.display !== 'none'
);
if (items.length > 0) {
const currentIndex = items.findIndex(item => item.classList.contains('selected'));
if (e.key === 'ArrowDown') {
e.preventDefault();
if (currentIndex < items.length - 1) {
items[currentIndex].classList.remove('selected');
items[currentIndex + 1].classList.add('selected');
items[currentIndex + 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (currentIndex > 0) {
items[currentIndex].classList.remove('selected');
items[currentIndex - 1].classList.add('selected');
items[currentIndex - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} else if (e.key === 'Enter') {
e.preventDefault();
const selectedItem = commandList.querySelector('.command-item.selected');
if (selectedItem) {
const cmdId = selectedItem.dataset.id;
const cmd = commands.find(c => c.id === cmdId);
executeCommandFromPalette(cmd);
}
}
}
}
if (document.activeElement === userInput) {
if (e.key === 'ArrowUp' && commandHistoryArray.length > 0) {
e.preventDefault();
if (historyIndex < commandHistoryArray.length - 1) {
historyIndex++;
userInput.value = commandHistoryArray[commandHistoryArray.length - 1 - historyIndex];
}
} else if (e.key === 'ArrowDown' && historyIndex > -1) {
e.preventDefault();
historyIndex--;
if (historyIndex === -1) {
userInput.value = '';
} else {
userInput.value = commandHistoryArray[commandHistoryArray.length - 1 - historyIndex];
}
}
}
});
commandSearch.addEventListener('input', () => {
filterCommands(commandSearch.value);
});
}
// 帮助信息
const helpText = `
可用命令:
基础命令:
- help: 显示帮助信息
- clear: 清空终端
- history: 显示命令历史记录
- theme: 切换深色/浅色主题
工具命令:
- video [url]: 解析并播放视频
例如: video https://v.qq.com/x/cover/mzc00200mp8er9d.html
- ai [message]: 与AI助手对话
例如: ai 你好
切换API: ai --api [api名称]
- image [format]: 转换图片格式 (支持: jpg, png, webp, bmp)
例如: image png
新功能:
- calendar: 显示日历和事件管理
- weather [城市名]: 查看天气预报
例如: weather 北京
配置命令:
- ai --api [api名称]: 切换AI接口
可用API: DeepSeek API (需要密钥), 备用API (默认)
`;
// 打字机效果函数
function typeWriterEffect(element, text, speed = 10) {
return new Promise((resolve) => {
let i = 0;
const typing = () => {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
setTimeout(typing, speed);
} else {
const cursor = element.querySelector('.cursor');
if (cursor) cursor.remove();
resolve();
}
};
const cursorSpan = document.createElement('span');
cursorSpan.className = 'cursor';
element.appendChild(cursorSpan);
typing();
});
}
async function addAILine(text) {
const line = document.createElement('div');
line.className = 'line';
const lineContent = document.createElement('div');
lineContent.className = 'line-content ai-output';
lineContent.textContent = '藤原: ';
line.appendChild(lineContent);
editorContent.appendChild(line);
editorContent.scrollTop = editorContent.scrollHeight;
await typeWriterEffect(lineContent, text);
return line;
}
function addUserLine(text) {
const line = document.createElement('div');
line.className = 'line';
const lineContent = document.createElement('div');
lineContent.className = 'line-content user-input';
lineContent.textContent = `> ${text}`;
line.appendChild(lineContent);
editorContent.appendChild(line);
editorContent.scrollTop = editorContent.scrollHeight;
return line;
}
function clearTerminal() {
const lines = document.querySelectorAll('.line');
const commandHistoryElem = document.getElementById('commandHistory');
const calendarContainer = document.querySelector('.calendar-container');
const weatherContainer = document.querySelector('.weather-container');
const videoContainer = document.querySelector('.video-container');
const imageConverter = document.querySelector('.image-converter');
lines.forEach(line => {
if (!line.classList.contains('input-line')) {
line.remove();
}
});
if (commandHistoryElem && commandHistoryElem.style.display === 'block') {
editorContent.appendChild(commandHistoryElem);
}
if (calendarContainer) {
editorContent.appendChild(calendarContainer);
}
if (weatherContainer) {
editorContent.appendChild(weatherContainer);
}
if (videoContainer) {
editorContent.appendChild(videoContainer);
}
if (imageConverter) {
editorContent.appendChild(imageConverter);
}
}
// ============== 视频播放器功能 ==============
function createVideoPlayer(videoUrl) {
// 移除现有的播放器
const existingPlayer = document.querySelector('.video-container');
if (existingPlayer) existingPlayer.remove();
// 创建容器
const videoContainer = document.createElement('div');
videoContainer.className = 'video-container';
// 创建视频元素
const videoPlayer = document.createElement('div');
videoPlayer.className = 'video-player';
const videoElement = document.createElement('video');
videoElement.id = 'terminal-video';
videoElement.controls = true;
videoElement.src = videoUrl;
videoPlayer.appendChild(videoElement);
// 视频控制区域
const videoControls = document.createElement('div');
videoControls.className = 'video-controls';
// 视频信息
const videoInfo = document.createElement('div');
videoInfo.className = 'video-info';
const videoTitle = document.createElement('div');
videoTitle.textContent = '视频播放器';
const videoStatus = document.createElement('div');
videoStatus.className = 'video-status';
videoStatus.textContent = '加载中...';
videoInfo.appendChild(videoTitle);
videoInfo.appendChild(videoStatus);
// 主控制按钮
const videoMainControls = document.createElement('div');
videoMainControls.className = 'video-main-controls';
// 播放控制按钮组
const playButtonGroup = document.createElement('div');
playButtonGroup.className = 'video-button-group';
const playButton = document.createElement('button');
playButton.className = 'video-button';
playButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> 播放';
playButton.addEventListener('click', () => {
if (videoElement.paused) {
videoElement.play();
playButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> 暂停';
} else {
videoElement.pause();
playButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> 播放';
}
});
const fullscreenButton = document.createElement('button');
fullscreenButton.className = 'video-button';
fullscreenButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg> 全屏';
fullscreenButton.addEventListener('click', toggleFullscreen);
playButtonGroup.appendChild(playButton);
playButtonGroup.appendChild(fullscreenButton);
// 进度条
const videoTimeline = document.createElement('div');
videoTimeline.className = 'video-timeline';
const videoProgress = document.createElement('div');
videoProgress.className = 'video-progress';
videoTimeline.appendChild(videoProgress);
videoTimeline.addEventListener('click', (e) => {
const rect = videoTimeline.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
videoElement.currentTime = pos * videoElement.duration;
});
// 时间显示
const videoTime = document.createElement('div');
videoTime.className = 'video-time';
videoTime.textContent = '00:00 / 00:00';
videoMainControls.appendChild(playButtonGroup);
videoMainControls.appendChild(videoTimeline);
videoMainControls.appendChild(videoTime);
// 高级控制
const videoAdvancedControls = document.createElement('div');
videoAdvancedControls.className = 'video-advanced-controls';
// 播放速度按钮
const speedButton = document.createElement('button');
speedButton.className = 'video-button';
speedButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg> 1.0x';
speedButton.addEventListener('click', changePlaybackSpeed);
// 音量控制
const volumeButton = document.createElement('button');
volumeButton.className = 'video-button';
volumeButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon></svg> 音量';
volumeButton.addEventListener('click', toggleMute);
// 网页全屏按钮
const webFullscreenButton = document.createElement('button');
webFullscreenButton.className = 'video-button';
webFullscreenButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> 网页全屏';
webFullscreenButton.addEventListener('click', () => {
window.open(videoUrl, '_blank');
});
// 关闭按钮
const closeButton = document.createElement('button');
closeButton.className = 'video-button';
closeButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> 关闭';
closeButton.addEventListener('click', () => videoContainer.remove());
// 解析接口选择器
const apiSelector = document.createElement('div');
apiSelector.className = 'video-api-selector';
const apiSelect = document.createElement('select');
apiSelect.className = 'video-api-select';
videoApis.forEach((api, index) => {
const option = document.createElement('option');
option.className = 'video-api-option';
option.value = index;
option.textContent = api.name;
apiSelect.appendChild(option);
});
apiSelect.addEventListener('change', () => {
const selectedApi = videoApis[apiSelect.value];
const newUrl = selectedApi.url + encodeURIComponent(videoUrl.split('url=')[1] || videoUrl);
videoElement.src = newUrl;
videoStatus.textContent = `已切换到 ${selectedApi.name}`;
});
apiSelector.appendChild(apiSelect);
videoAdvancedControls.appendChild(speedButton);
videoAdvancedControls.appendChild(volumeButton);
videoAdvancedControls.appendChild(webFullscreenButton);
videoAdvancedControls.appendChild(closeButton);
videoAdvancedControls.appendChild(apiSelector);
// 组装控制区域
videoControls.appendChild(videoInfo);
videoControls.appendChild(videoMainControls);
videoControls.appendChild(videoAdvancedControls);
// 组装整个播放器
videoContainer.appendChild(videoPlayer);
videoContainer.appendChild(videoControls);
// 添加到编辑器内容区
editorContent.appendChild(videoContainer);
editorContent.scrollTop = editorContent.scrollHeight;
// 自动播放(需要用户交互后才会真正播放)
videoElement.play().catch(e => {
videoStatus.textContent = '点击播放按钮开始播放';
});
// 更新进度条和时间显示
videoElement.addEventListener('timeupdate', updateProgress);
videoElement.addEventListener('loadedmetadata', () => {
videoStatus.textContent = '已加载';
updateTimeDisplay();
});
// 双击全屏
videoPlayer.addEventListener('dblclick', toggleFullscreen);
// 键盘控制
document.addEventListener('keydown', handleVideoKeyControls);
// ============== 功能函数 ==============
function toggleFullscreen() {
if (!document.fullscreenElement) {
videoPlayer.requestFullscreen().catch(err => {
videoStatus.textContent = `全屏错误: ${err.message}`;
});
} else {
document.exitFullscreen();
}
}
function changePlaybackSpeed() {
const speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
const currentSpeed = videoElement.playbackRate;
const currentIndex = speeds.indexOf(currentSpeed);
const nextIndex = (currentIndex + 1) % speeds.length;
videoElement.playbackRate = speeds[nextIndex];
speedButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg> ${speeds[nextIndex]}x`;
}
function toggleMute() {
videoElement.muted = !videoElement.muted;
volumeButton.innerHTML = videoElement.muted ?
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.5 16.5 12 21l-4-4H2V7h4l4.5-4.5"></path><line x1="21" y1="12" x2="23" y2="12"></line></svg> 静音' :
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon></svg> 音量';
}
function updateProgress() {
const percent = (videoElement.currentTime / videoElement.duration) * 100;
videoProgress.style.width = `${percent}%`;
updateTimeDisplay();
}
function updateTimeDisplay() {
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
videoTime.textContent = `${formatTime(videoElement.currentTime)} / ${formatTime(videoElement.duration)}`;
}
function handleVideoKeyControls(e) {
// 只在视频可见时处理按键
if (!videoContainer.isConnected) {
document.removeEventListener('keydown', handleVideoKeyControls);
return;
}
// 空格键切换播放/暂停
if (e.code === 'Space' && document.activeElement !== userInput) {
e.preventDefault();
if (videoElement.paused) {
videoElement.play();
playButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> 暂停';
} else {
videoElement.pause();
playButton.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> 播放';
}
}
// 左右箭头快进/快退
if (e.code === 'ArrowRight') {
videoElement.currentTime += 5;
} else if (e.code === 'ArrowLeft') {
videoElement.currentTime -= 5;
}
// F键切换全屏
if (e.code === 'KeyF') {
toggleFullscreen();
}
// M键切换静音
if (e.code === 'KeyM') {
toggleMute();
}
}
}
// ============== 视频解析功能 ==============
async function parseVideo(url) {
try {
if (!/^https?:\/\/.+/.test(url)) {
addAILine('错误:请提供有效的URL地址');
return;
}
const loadingLine = await addAILine('正在解析视频,请稍候...');
// 尝试直接播放(适用于MP4等直接可播放格式)
if (url.match(/\.(mp4|webm|ogg)$/i)) {
createVideoPlayer(url);
loadingLine.querySelector('.line-content').textContent = '藤原: 视频已加载';
return;
}
// 使用默认解析方案
const defaultApi = videoApis[0];
const apiUrl = defaultApi.url + encodeURIComponent(url);
createVideoPlayer(apiUrl);
loadingLine.querySelector('.line-content').textContent = '藤原: 视频已加载,如果无法播放请尝试其他线路';
} catch (error) {
addAILine(`视频解析失败: ${error.message}`);
}
}
// 创建图片转换器界面
function createImageConverter(targetFormat) {
const existingConverter = document.querySelector('.image-converter');
if (existingConverter) {
existingConverter.remove();
return;
}
const converter = document.createElement('div');
converter.className = 'image-converter';
// 头部
const imageHeader = document.createElement('div');
imageHeader.className = 'image-header';
const imageTitle = document.createElement('div');
imageTitle.className = 'image-title';
imageTitle.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
图片格式转换
`;
const imageClose = document.createElement('button');
imageClose.className = 'image-close';
imageClose.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
imageClose.addEventListener('click', () => converter.remove());
imageHeader.appendChild(imageTitle);
imageHeader.appendChild(imageClose);
// 内容区域
const imageContent = document.createElement('div');
imageContent.className = 'image-content';
// 预览区域
const previewContainer = document.createElement('div');
previewContainer.className = 'image-preview-container';
const previewImage = document.createElement('img');
previewImage.className = 'image-preview';
const previewPlaceholder = document.createElement('div');
previewPlaceholder.className = 'image-placeholder';
previewPlaceholder.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
<div>请选择要转换的图片</div>
<div style="font-size:0.8em;margin-top:8px;">支持 JPG, PNG, WEBP, BMP 格式</div>
`;
previewContainer.appendChild(previewImage);
previewContainer.appendChild(previewPlaceholder);
// 控制区域
const controls = document.createElement('div');
controls.className = 'image-controls';
// 图片质量控制
const qualityControl = document.createElement('div');
qualityControl.className = 'image-control';
const qualityLabel = document.createElement('label');
qualityLabel.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8.56 2.75c4.37 6.03 6.02 9.42 8.03 17.72m2.54-15.38c-3.72 4.35-8.94 5.66-16.88 5.85m19.5 1.9c-3.5-.93-6.63-.82-8.94 0-2.58.92-5.01 2.86-7.44 6.32"></path>
</svg>
图片质量 (0-100)
`;
const qualityInput = document.createElement('input');
qualityInput.type = 'range';
qualityInput.id = 'imageQuality';
qualityInput.min = '0';
qualityInput.max = '100';
qualityInput.value = '80';
const qualityValue = document.createElement('span');
qualityValue.textContent = '80';
qualityInput.addEventListener('input', () => {
qualityValue.textContent = qualityInput.value;
});
qualityControl.appendChild(qualityLabel);
qualityControl.appendChild(qualityInput);
qualityControl.appendChild(qualityValue);
// 图片宽度控制
const widthControl = document.createElement('div');
widthControl.className = 'image-control';
const widthLabel = document.createElement('label');
widthLabel.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line>
<line x1="15" y1="9" x2="9" y2="15"></line>
</svg>
宽度 (像素)
`;
const widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.id = 'imageWidth';
widthInput.placeholder = '自动';
widthControl.appendChild(widthLabel);
widthControl.appendChild(widthInput);
// 图片高度控制
const heightControl = document.createElement('div');
heightControl.className = 'image-control';
const heightLabel = document.createElement('label');
heightLabel.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="7" x2="12" y2="17"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
高度 (像素)
`;
const heightInput = document.createElement('input');
heightInput.type = 'number';
heightInput.id = 'imageHeight';
heightInput.placeholder = '自动';
heightControl.appendChild(heightLabel);
heightControl.appendChild(heightInput);
// 图片格式选择
const formatControl = document.createElement('div');
formatControl.className = 'image-control';
const formatLabel = document.createElement('label');
formatLabel.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"></path>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-4"></path>
<path d="M18 17a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2Z"></path>
</svg>
目标格式
`;
const formatSelect = document.createElement('select');
formatSelect.id = 'imageFormat';
const formats = ['jpg', 'jpeg', 'png', 'webp', 'bmp'];
formats.forEach(format => {
const option = document.createElement('option');
option.value = format;
option.textContent = format.toUpperCase();
if (format === targetFormat) option.selected = true;
formatSelect.appendChild(option);
});
formatControl.appendChild(formatLabel);
formatControl.appendChild(formatSelect);
controls.appendChild(qualityControl);
controls.appendChild(widthControl);
controls.appendChild(heightControl);
controls.appendChild(formatControl);
// 操作按钮
const actions = document.createElement('div');
actions.className = 'image-actions';
const selectBtn = document.createElement('button');
selectBtn.className = 'image-btn secondary';
selectBtn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"></path>
<line x1="16" y1="5" x2="22" y2="5"></line>
<line x1="19" y1="2" x2="19" y2="8"></line>
<circle cx="9" cy="9" r="2"></circle>
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
</svg>
选择图片
`;
selectBtn.addEventListener('click', () => {
fileInput.click();
});
const convertBtn = document.createElement('button');
convertBtn.className = 'image-btn';
convertBtn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"></path>
<line x1="16" y1="5" x2="22" y2="5"></line>
<line x1="19" y1="2" x2="19" y2="8"></line>
<path d="M12 12H9.5a2.5 2.5 0 0 1 0-5H17"></path>
<path d="M12 12h2.5a2.5 2.5 0 0 0 0-5H7"></path>
</svg>
转换图片
`;
convertBtn.addEventListener('click', () => {
if (!previewImage.src) {
addAILine('请先选择要转换的图片');
return;
}
convertImage(previewImage, formatSelect.value, qualityInput.value, widthInput.value, heightInput.value);
});
const downloadBtn = document.createElement('button');
downloadBtn.className = 'image-btn';
downloadBtn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载图片
`;
downloadBtn.style.display = 'none';
downloadBtn.id = 'downloadBtn';
const downloadLink = document.createElement('a');
downloadLink.className = 'image-download-link';
downloadLink.id = 'downloadLink';
downloadLink.download = `converted-image.${formatSelect.value}`;
actions.appendChild(selectBtn);
actions.appendChild(convertBtn);
actions.appendChild(downloadBtn);
actions.appendChild(downloadLink);
imageContent.appendChild(previewContainer);
imageContent.appendChild(controls);
imageContent.appendChild(actions);
converter.appendChild(imageHeader);
converter.appendChild(imageContent);
editorContent.appendChild(converter);
editorContent.scrollTop = editorContent.scrollHeight;
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.match('image.*')) {
addAILine('请选择有效的图片文件');
return;
}
const reader = new FileReader();
reader.onload = (event) => {
previewImage.src = event.target.result;
previewImage.classList.add('active');
previewPlaceholder.style.display = 'none';
const img = new Image();
img.onload = function() {
previewPlaceholder.innerHTML = `
<div>预览 (原始: ${img.width}×${img.height}, ${formatToHumanReadable(file.size)})</div>
<div style="font-size:0.8em;margin-top:8px;">点击"转换图片"按钮开始转换</div>
`;
previewPlaceholder.style.display = 'flex';
previewPlaceholder.style.padding = '10px';
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
};
formatSelect.addEventListener('change', () => {
const downloadLink = document.getElementById('downloadLink');
if (downloadLink) {
downloadLink.download = `converted-image.${formatSelect.value}`;
}
});
}
// 转换图片
function convertImage(imgElement, format, quality, width, height) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
let newWidth = parseInt(width) || imgElement.naturalWidth;
let newHeight = parseInt(height) || imgElement.naturalHeight;
if (width && !height) {
const ratio = imgElement.naturalHeight / imgElement.naturalWidth;
newHeight = Math.round(newWidth * ratio);
} else if (height && !width) {
const ratio = imgElement.naturalWidth / imgElement.naturalHeight;
newWidth = Math.round(newHeight * ratio);
}
canvas.width = newWidth;
canvas.height = newHeight;
ctx.drawImage(imgElement, 0, 0, newWidth, newHeight);
let mimeType;
switch (format.toLowerCase()) {
case 'jpg':
case 'jpeg':
mimeType = 'image/jpeg';
break;
case 'png':
mimeType = 'image/png';
break;
case 'webp':
mimeType = 'image/webp';
break;
case 'bmp':
mimeType = 'image/bmp';
break;
default:
mimeType = 'image/jpeg';
}
let qualityValue = parseInt(quality) / 100;
if (isNaN(qualityValue)) qualityValue = 0.8;
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const downloadLink = document.getElementById('downloadLink');
const downloadBtn = document.getElementById('downloadBtn');
if (downloadLink && downloadBtn) {
downloadLink.href = url;
downloadLink.download = `converted-image.${format}`;
downloadBtn.style.display = 'inline-block';
downloadBtn.onclick = () => {
downloadLink.click();
addAILine(`图片已转换完成 (${newWidth}×${newHeight}, ${formatToHumanReadable(blob.size)})`);
};
}
const previewImage = document.querySelector('.image-preview');
if (previewImage) {
previewImage.src = url;
}
const previewPlaceholder = document.querySelector('.image-placeholder');
if (previewPlaceholder) {
previewPlaceholder.innerHTML = `
<div>预览 (转换后: ${newWidth}×${newHeight}, ${formatToHumanReadable(blob.size)})</div>
<div style="font-size:0.8em;margin-top:8px;">点击"下载图片"按钮保存结果</div>
`;
}
addAILine(`图片已成功转换为 ${format.toUpperCase()} 格式!点击"下载图片"按钮保存结果。`);
}, mimeType, qualityValue);
}
// 将字节大小转换为易读格式
function formatToHumanReadable(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 显示命令历史
function toggleCommandHistory() {
if (commandHistory.style.display === 'none' || !commandHistory.style.display) {
commandHistory.style.display = 'block';
updateHistoryUI();
} else {
commandHistory.style.display = 'none';
}
}
// 创建日历组件
function createCalendar() {
const existingCalendar = document.querySelector('.calendar-container');
if (existingCalendar) {
existingCalendar.remove();
return;
}
const now = new Date();
let currentMonth = now.getMonth();
let currentYear = now.getFullYear();
let selectedDate = now;
const calendarContainer = document.createElement('div');
calendarContainer.className = 'calendar-container';
// 日历头部
const calendarHeader = document.createElement('div');
calendarHeader.className = 'calendar-header';
const calendarTitle = document.createElement('div');
calendarTitle.className = 'calendar-title';
const calendarNav = document.createElement('div');
calendarNav.className = 'calendar-nav';
const prevBtn = document.createElement('button');
prevBtn.className = 'calendar-btn';
prevBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>';
prevBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar();
});
const nextBtn = document.createElement('button');
nextBtn.className = 'calendar-btn';
nextBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>';
nextBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar();
});
const todayBtn = document.createElement('button');
todayBtn.className = 'calendar-btn';
todayBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>';
todayBtn.addEventListener('click', () => {
const today = new Date();
currentMonth = today.getMonth();
currentYear = today.getFullYear();
selectedDate = today;
renderCalendar();
});
calendarNav.appendChild(prevBtn);
calendarNav.appendChild(todayBtn);
calendarNav.appendChild(nextBtn);
calendarHeader.appendChild(calendarTitle);
calendarHeader.appendChild(calendarNav);
// 日历内容
const calendarContent = document.createElement('div');
calendarContent.className = 'calendar-content';
// 日历网格
const calendarGrid = document.createElement('div');
calendarGrid.className = 'calendar-grid';
// 星期标题
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
weekdays.forEach(day => {
const weekdayElem = document.createElement('div');
weekdayElem.className = 'calendar-weekday';
weekdayElem.textContent = day;
calendarGrid.appendChild(weekdayElem);
});
// 日历事件区域
const calendarEvents = document.createElement('div');
calendarEvents.className = 'calendar-events';
const eventsTitle = document.createElement('div');
eventsTitle.className = 'calendar-events-title';
eventsTitle.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
今日事件
`;
calendarEvents.appendChild(eventsTitle);
// 添加到容器
calendarContent.appendChild(calendarGrid);
calendarContent.appendChild(calendarEvents);
calendarContainer.appendChild(calendarHeader);
calendarContainer.appendChild(calendarContent);
editorContent.appendChild(calendarContainer);
// 渲染日历函数
function renderCalendar() {
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
calendarTitle.textContent = `${monthNames[currentMonth]} ${currentYear}`;
const days = calendarGrid.querySelectorAll('.calendar-day');
days.forEach(day => day.remove());
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const prevMonthLastDay = new Date(currentYear, currentMonth, 0).getDate();
const firstDayIndex = firstDay.getDay();
const lastDate = lastDay.getDate();
const today = new Date();
for (let i = firstDayIndex; i > 0; i--) {
const dayElem = document.createElement('div');
dayElem.className = 'calendar-day other-month';
dayElem.textContent = prevMonthLastDay - i + 1;
calendarGrid.appendChild(dayElem);
}
for (let i = 1; i <= lastDate; i++) {
const dayElem = document.createElement('div');
dayElem.className = 'calendar-day';
dayElem.textContent = i;
if (i === today.getDate() && currentMonth === today.getMonth() && currentYear === today.getFullYear()) {
dayElem.classList.add('today');
}
if (i === selectedDate.getDate() && currentMonth === selectedDate.getMonth() && currentYear === selectedDate.getFullYear()) {
dayElem.classList.add('selected');
}
if (i === 1 && currentMonth === 10) {
dayElem.classList.add('birthday');
dayElem.setAttribute('title', '藤原生日');
}
dayElem.addEventListener('click', () => {
const selectedDays = calendarGrid.querySelectorAll('.calendar-day.selected');
selectedDays.forEach(day => day.classList.remove('selected'));
dayElem.classList.add('selected');
selectedDate = new Date(currentYear, currentMonth, i);
updateEvents();
});
calendarGrid.appendChild(dayElem);
}
const daysNeeded = 42 - (firstDayIndex + lastDate);
for (let i = 1; i <= daysNeeded; i++) {
const dayElem = document.createElement('div');
dayElem.className = 'calendar-day other-month';
dayElem.textContent = i;
calendarGrid.appendChild(dayElem);
}
updateEvents();
}
function updateEvents() {
calendarEvents.innerHTML = '';
const eventsTitle = document.createElement('div');
eventsTitle.className = 'calendar-events-title';
eventsTitle.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
${selectedDate.getMonth() + 1}月${selectedDate.getDate()}日 事件
`;
calendarEvents.appendChild(eventsTitle);
const dateStr = `${selectedDate.getFullYear()}-${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}-${selectedDate.getDate().toString().padStart(2, '0')}`;
const allEvents = JSON.parse(localStorage.getItem('calendarEvents') || '{}');
const dayEvents = allEvents[dateStr] || [];
if (selectedDate.getMonth() === 10 && selectedDate.getDate() === 1) {
const birthdayEvent = document.createElement('div');
birthdayEvent.className = 'calendar-event birthday-event';
const eventDot = document.createElement('div');
eventDot.className = 'calendar-event-dot';
const eventContent = document.createElement('div');
eventContent.className = 'calendar-event-content';
eventContent.textContent = '藤原生日';
const eventTime = document.createElement('div');
eventTime.className = 'calendar-event-time';
eventTime.textContent = '全天';
birthdayEvent.appendChild(eventDot);
birthdayEvent.appendChild(eventContent);
birthdayEvent.appendChild(eventTime);
calendarEvents.appendChild(birthdayEvent);
}
if (dayEvents.length === 0 && !(selectedDate.getMonth() === 10 && selectedDate.getDate() === 1)) {
const noEvent = document.createElement('div');
noEvent.className = 'calendar-event';
noEvent.textContent = '今日没有事件';
calendarEvents.appendChild(noEvent);
} else {
dayEvents.forEach(event => {
const eventElem = document.createElement('div');
eventElem.className = 'calendar-event';
const eventDot = document.createElement('div');
eventDot.className = 'calendar-event-dot';
const eventContent = document.createElement('div');
eventContent.className = 'calendar-event-content';
eventContent.textContent = event.title;
const eventTime = document.createElement('div');
eventTime.className = 'calendar-event-time';
eventTime.textContent = event.time || '';
eventElem.appendChild(eventDot);
eventElem.appendChild(eventContent);
eventElem.appendChild(eventTime);
calendarEvents.appendChild(eventElem);
});
}
const addEventBtn = document.createElement('button');
addEventBtn.className = 'image-btn';
addEventBtn.style.marginTop = '8px';
addEventBtn.style.padding = '6px 12px';
addEventBtn.style.fontSize = '0.8em';
addEventBtn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
添加事件
`;
addEventBtn.addEventListener('click', () => {
const eventTitle = prompt('请输入事件标题:');
if (eventTitle) {
const eventTime = prompt('请输入事件时间 (可选):');
const allEvents = JSON.parse(localStorage.getItem('calendarEvents') || '{}');
const dateStr = `${selectedDate.getFullYear()}-${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}-${selectedDate.getDate().toString().padStart(2, '0')}`;
if (!allEvents[dateStr]) {
allEvents[dateStr] = [];
}
allEvents[dateStr].push({
title: eventTitle,
time: eventTime || ''
});
localStorage.setItem('calendarEvents', JSON.stringify(allEvents));
updateEvents();
}
});
const eventActions = document.createElement('div');
eventActions.className = 'calendar-actions';
eventActions.appendChild(addEventBtn);
calendarEvents.appendChild(eventActions);
}
renderCalendar();
}
// 创建天气组件
async function createWeather(city = '北京') {
const existingWeather = document.querySelector('.weather-container');
if (existingWeather) {
existingWeather.remove();
}
const weatherContainer = document.createElement('div');
weatherContainer.className = 'weather-container';
// 天气头部
const weatherHeader = document.createElement('div');
weatherHeader.className = 'weather-header';
const weatherLocation = document.createElement('div');
weatherLocation.className = 'weather-location';
weatherLocation.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
${city}
`;
const weatherRefresh = document.createElement('button');
weatherRefresh.className = 'weather-refresh';
weatherRefresh.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>';
weatherRefresh.addEventListener('click', () => {
fetchWeatherData(city);
});
weatherHeader.appendChild(weatherLocation);
weatherHeader.appendChild(weatherRefresh);
// 天气内容区域
const weatherContent = document.createElement('div');
weatherContent.className = 'weather-content';
weatherContent.innerHTML = '<div class="ai-output">正在获取天气数据...</div>';
// 天气搜索
const weatherSearch = document.createElement('div');
weatherSearch.className = 'weather-search';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '输入城市名称';
searchInput.value = city;
const searchButton = document.createElement('button');
searchButton.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
搜索
`;
searchButton.addEventListener('click', () => {
const newCity = searchInput.value.trim();
if (newCity) {
fetchWeatherData(newCity);
}
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const newCity = searchInput.value.trim();
if (newCity) {
fetchWeatherData(newCity);
}
}
});
weatherSearch.appendChild(searchInput);
weatherSearch.appendChild(searchButton);
weatherContainer.appendChild(weatherHeader);
weatherContainer.appendChild(weatherContent);
weatherContainer.appendChild(weatherSearch);
editorContent.appendChild(weatherContainer);
async function fetchWeatherData(cityName) {
try {
weatherContent.innerHTML = '<div class="ai-output">正在获取天气数据...</div>';
weatherLocation.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
${cityName}
`;
await new Promise(resolve => setTimeout(resolve, 1000));
const weatherData = {
city: cityName,
current: {
temp: Math.floor(Math.random() * 15) + 15,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)],
humidity: Math.floor(Math.random() * 30) + 40,
wind: Math.floor(Math.random() * 5) + 1,
feelsLike: Math.floor(Math.random() * 15) + 15,
pressure: Math.floor(Math.random() * 100) + 1000
},
forecast: [
{
date: '今天',
high: Math.floor(Math.random() * 10) + 20,
low: Math.floor(Math.random() * 10) + 10,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)]
},
{
date: '明天',
high: Math.floor(Math.random() * 10) + 20,
low: Math.floor(Math.random() * 10) + 10,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)]
},
{
date: '后天',
high: Math.floor(Math.random() * 10) + 20,
low: Math.floor(Math.random() * 10) + 10,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)]
},
{
date: '周四',
high: Math.floor(Math.random() * 10) + 20,
low: Math.floor(Math.random() * 10) + 10,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)]
},
{
date: '周五',
high: Math.floor(Math.random() * 10) + 20,
low: Math.floor(Math.random() * 10) + 10,
condition: ['晴', '多云', '阴', '小雨', '中雨'][Math.floor(Math.random() * 5)]
}
]
};
renderWeatherData(weatherData);
} catch (error) {
weatherContent.innerHTML = `<div class="ai-output">获取天气数据失败: ${error.message}</div>`;
}
}
function renderWeatherData(data) {
function getWeatherIcon(condition) {
switch(condition) {
case '晴':
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
case '多云':
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>';
case '阴':
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 3H5a2 2 0 0 1 0-4h14v4"></path><path d="M3 5v14a2 2 0 0 0 2 2h16v-4"></path><path d="M18 17a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2Z"></path></svg>';
case '小雨':
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 16.2A4.5 4.5 0 0 0 17.5 8h-1.8A7 7 0 1 0 4 14.9"></path><path d="M16 14v6"></path><path d="M8 14v6"></path><path d="M12 16v6"></path></svg>';
case '中雨':
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 13v8"></path><path d="M8 13v8"></path><path d="M12 15v8"></path><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 1 4 15.25"></path></svg>';
default:
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
}
}
const currentWeatherHTML = `
<div class="weather-current">
<div class="weather-icon">${getWeatherIcon(data.current.condition)}</div>
<div class="weather-temp">${data.current.temp}°</div>
<div class="weather-desc">
<div class="weather-condition">${data.current.condition}</div>
<div class="weather-feels-like">体感温度: ${data.current.feelsLike}°</div>
</div>
</div>
<div class="weather-details">
<div class="weather-detail">
<div class="weather-detail-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path>
</svg>
</div>
<div class="weather-detail-content">
<div class="weather-detail-title">湿度</div>
<div class="weather-detail-value">${data.current.humidity}%</div>
</div>
</div>
<div class="weather-detail">
<div class="weather-detail-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"></path>
</svg>
</div>
<div class="weather-detail-content">
<div class="weather-detail-title">风速</div>
<div class="weather-detail-value">${data.current.wind} m/s</div>
</div>
</div>
<div class="weather-detail">
<div class="weather-detail-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"></path>
</svg>
</div>
<div class="weather-detail-content">
<div class="weather-detail-title">气压</div>
<div class="weather-detail-value">${data.current.pressure} hPa</div>
</div>
</div>
<div class="weather-detail">
<div class="weather-detail-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<div class="weather-detail-content">
<div class="weather-detail-title">更新时间</div>
<div class="weather-detail-value">${new Date().toLocaleTimeString()}</div>
</div>
</div>
</div>
`;
let forecastHTML = '<div class="weather-forecast">';
data.forecast.forEach(day => {
forecastHTML += `
<div class="forecast-day">
<div class="forecast-date">${day.date}</div>
<div class="forecast-icon">${getWeatherIcon(day.condition)}</div>
<div class="forecast-temp">
<div class="forecast-high">${day.high}°</div>
<div class="forecast-low">${day.low}°</div>
</div>
</div>
`;
});
forecastHTML += '</div>';
weatherContent.innerHTML = currentWeatherHTML + forecastHTML;
}
fetchWeatherData(city);
}
// 处理用户命令
function processCommand(command) {
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
saveToHistory(command);
switch(cmd) {
case 'help':
addAILine(helpText);
break;
case 'clear':
clearTerminal();
break;
case 'video':
if (!args) {
addAILine('请提供视频URL,例如: video https://v.qq.com/x/cover/mzc00200mp8er9d.html');
} else {
parseVideo(args);
}
break;
case 'ai':
if (!args) {
addAILine('请输入您想对AI说的话');
} else if (args.startsWith('--api')) {
// 处理API切换命令
const apiName = args.split(' ')[1];
if (apiName) {
const selectedApi = aiApis.find(api => api.name.toLowerCase().includes(apiName.toLowerCase()));
if (selectedApi) {
currentApi = selectedApi;
if (selectedApi.requireKey) {
addAILine(`已切换到 ${selectedApi.name},请确保已设置API密钥。如需设置密钥,请使用命令: ai --set-key [您的密钥]`);
} else {
addAILine(`已切换到 ${selectedApi.name}`);
}
} else {
addAILine(`未找到API: ${apiName}。可用API: ${aiApis.map(api => api.name).join(', ')}`);
}
} else {
addAILine(`当前API: ${currentApi.name}。可用API: ${aiApis.map(api => api.name).join(', ')}`);
}
} else if (args.startsWith('--set-key')) {
const key = args.split(' ')[1];
if (key) {
localStorage.setItem('deepSeekKey', key);
addAILine('DeepSeek API密钥已设置');
} else {
addAILine('请提供API密钥');
}
} else {
sendMessage(args);
}
break;
case 'image':
if (!args) {
addAILine('请指定目标图片格式,例如: image png');
} else {
const validFormats = ['jpg', 'jpeg', 'png', 'webp', 'bmp'];
if (validFormats.includes(args.toLowerCase())) {
createImageConverter(args.toLowerCase());
} else {
addAILine(`不支持的目标格式: ${args}。支持格式: ${validFormats.join(', ')}`);
}
}
break;
case 'history':
toggleCommandHistory();
break;
case 'theme':
themeSwitch.click();
break;
case 'calendar':
createCalendar();
break;
case 'weather':
createWeather(args || '北京');
break;
default:
if (command.startsWith('http://') || command.startsWith('https://')) {
parseVideo(command);
} else if (command) {
sendMessage(command);
}
}
}
async function sendMessage(message) {
if (!message) return;
addUserLine(message);
const thinkingLine = document.createElement('div');
thinkingLine.className = 'line';
const thinkingContent = document.createElement('div');
thinkingContent.className = 'line-content ai-output';
const thinkingText = document.createElement('span');
thinkingText.textContent = '藤原: 正在思考中 ';
const thinkingAnimation = document.createElement('div');
thinkingAnimation.className = 'thinking-animation';
for (let i = 0; i < 3; i++) {
const dot = document.createElement('span');
thinkingAnimation.appendChild(dot);
}
thinkingContent.appendChild(thinkingText);
thinkingContent.appendChild(thinkingAnimation);
thinkingLine.appendChild(thinkingContent);
editorContent.appendChild(thinkingLine);
editorContent.scrollTop = editorContent.scrollHeight;
try {
let response;
if (currentApi.requireKey) {
let apiKey = localStorage.getItem('deepSeekKey');
if (!apiKey) {
apiKey = prompt(`请输入${currentApi.name}的API密钥:`);
if (!apiKey) {
throw new Error('未提供API密钥');
}
localStorage.setItem('deepSeekKey', apiKey);
}
response = await currentApi.endpoint(apiKey, message);
} else {
response = await currentApi.endpoint('', message);
}
thinkingLine.remove();
if (response.code === 200 && response.msg) {
const replyLine = document.createElement('div');
replyLine.className = 'line';
const replyContent = document.createElement('div');
replyContent.className = 'line-content ai-output';
replyContent.textContent = '藤原: ';
replyLine.appendChild(replyContent);
editorContent.appendChild(replyLine);
editorContent.scrollTop = editorContent.scrollHeight;
await typeWriterEffect(replyContent, response.msg);
} else {
addAILine(`AI回复失败: ${response.msg || '未知错误'}`);
}
} catch (error) {
thinkingLine.remove();
const errorLine = document.createElement('div');
errorLine.className = 'line';
const errorContent = document.createElement('div');
errorContent.className = 'line-content ai-output';
errorContent.textContent = `藤原: 请求失败: ${error.message}`;
errorLine.appendChild(errorContent);
editorContent.appendChild(errorLine);
editorContent.scrollTop = editorContent.scrollHeight;
}
}
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const command = userInput.value.trim();
if (command) {
processCommand(command);
userInput.value = '';
}
}
});
// 命令面板关闭按钮
closeCommandPaletteBtn.addEventListener('click', () => {
commandPalette.style.display = 'none';
});
// 初始化
initCommandPalette();
initKeyboardEvents();
updateHistoryUI();
// 自动聚焦输入框
userInput.focus();
// 添加欢迎消息
setTimeout(() => {
addAILine('欢迎来到藤原的个人终端!输入 "help" 获取可用命令。');
}, 500);
</script>
</body>
</html>
index.html
style.css
index.js