Trae Assistant commited on
Commit
3d6c10f
·
1 Parent(s): c038eac

Enhance: Add background upload, custom themes, save/load config, and demo reset

Browse files
Files changed (1) hide show
  1. templates/index.html +319 -124
templates/index.html CHANGED
@@ -17,6 +17,27 @@
17
  background-position: 0 0, 10px 10px;
18
  background-size: 20px 20px;
19
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  </style>
21
  </head>
22
  <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden flex flex-col">
@@ -25,16 +46,26 @@
25
  <!-- Header -->
26
  <header class="bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center shadow-sm z-10">
27
  <div class="flex items-center gap-3">
28
- <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-white text-xl">
29
  <i class="fa-solid fa-calendar-week"></i>
30
  </div>
31
  <div>
32
- <h1 class="text-xl font-bold text-gray-900">直播日程表生成器</h1>
33
- <p class="text-xs text-gray-500">Stream Schedule Maker</p>
34
  </div>
35
  </div>
36
  <div class="flex gap-3">
37
- <a href="https://github.com/duqing26" target="_blank" class="text-gray-500 hover:text-gray-900 transition">
 
 
 
 
 
 
 
 
 
 
38
  <i class="fa-brands fa-github text-xl"></i>
39
  </a>
40
  </div>
@@ -43,58 +74,132 @@
43
  <!-- Main Content -->
44
  <main class="flex-1 flex overflow-hidden">
45
  <!-- Sidebar: Editor -->
46
- <div class="w-1/3 bg-white border-r border-gray-200 flex flex-col h-full shadow-lg z-0">
47
- <div class="p-4 border-b border-gray-100">
48
- <h2 class="font-bold text-gray-700 mb-4 flex items-center gap-2">
49
- <i class="fa-solid fa-sliders"></i> 全局设置
50
- </h2>
51
- <div class="grid grid-cols-2 gap-4">
52
- <div>
53
- <label class="block text-xs font-medium text-gray-500 mb-1">主标题</label>
54
- <input v-model="config.title" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
55
- </div>
56
- <div>
57
- <label class="block text-xs font-medium text-gray-500 mb-1">日期/副标题</label>
58
- <input v-model="config.subtitle" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
59
- </div>
60
- <div>
61
- <label class="block text-xs font-medium text-gray-500 mb-1">主题风格</label>
62
- <select v-model="config.theme" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
63
- <option value="modern">现代极简 (Modern)</option>
64
- <option value="dark">黑金科技 (Dark)</option>
65
- <option value="cute">元气粉嫩 (Cute)</option>
66
- <option value="cyber">赛博朋克 (Cyber)</option>
67
- </select>
 
 
 
 
 
 
 
 
 
68
  </div>
69
- <div>
70
- <label class="block text-xs font-medium text-gray-500 mb-1">作者/主播名</label>
71
- <input v-model="config.author" type="text" class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
  </div>
74
- </div>
75
 
76
- <div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
77
- <h2 class="font-bold text-gray-700 mb-4 flex items-center gap-2">
78
- <i class="fa-solid fa-list-check"></i> 日程安排
79
- </h2>
80
- <div class="space-y-4">
81
- <div v-for="(day, index) in days" :key="index" class="bg-gray-50 p-3 rounded-lg border border-gray-200 hover:border-indigo-300 transition group">
82
- <div class="flex justify-between items-center mb-2">
83
- <span class="font-bold text-sm text-gray-700 bg-white px-2 py-0.5 rounded shadow-sm">{{ day.name }}</span>
84
- <label class="flex items-center gap-2 text-xs text-gray-500 cursor-pointer">
85
- <input type="checkbox" v-model="day.active" class="rounded text-indigo-600 focus:ring-indigo-500">
86
- 启用
87
  </label>
88
  </div>
89
- <div v-if="day.active" class="space-y-2 animate-fade-in">
 
90
  <div class="flex gap-2">
91
- <input v-model="day.time" placeholder="时间 (e.g. 20:00)" class="w-1/3 px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none">
92
- <input v-model="day.game" placeholder="内容/游戏 (e.g. 杂谈)" class="w-2/3 px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none">
 
 
 
 
93
  </div>
94
- <textarea v-model="day.desc" placeholder="详细描述/备注" rows="2" class="w-full px-2 py-1.5 border rounded text-sm focus:border-indigo-500 outline-none resize-none"></textarea>
95
  </div>
96
- <div v-else class="text-xs text-gray-400 italic text-center py-2">
97
- (该日无直播计划)
98
  </div>
99
  </div>
100
  </div>
@@ -104,15 +209,22 @@
104
  <!-- Main: Preview -->
105
  <div class="flex-1 bg-gray-100 flex flex-col relative">
106
  <!-- Toolbar -->
107
- <div class="absolute top-4 right-4 z-20 flex gap-2">
108
- <button @click="downloadImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition transform hover:scale-105">
109
- <i class="fa-solid fa-download"></i> 下载 PNG
110
- </button>
 
 
111
  </div>
112
 
113
  <!-- Canvas Container -->
114
  <div class="flex-1 overflow-auto flex items-center justify-center p-8 preview-container" id="canvas-wrapper">
115
- <canvas ref="canvas" class="shadow-2xl rounded-sm max-h-full max-w-full"></canvas>
 
 
 
 
 
116
  </div>
117
  </div>
118
  </main>
@@ -121,87 +233,90 @@
121
  <script>
122
  const { createApp, ref, reactive, onMounted, watch } = Vue;
123
 
 
 
 
 
 
 
 
 
 
 
124
  createApp({
125
  setup() {
126
  const canvas = ref(null);
 
 
 
 
127
 
128
  // Configuration
129
  const config = reactive({
130
  title: 'WEEKLY SCHEDULE',
131
  subtitle: '2023.10.01 - 10.07',
132
- theme: 'modern', // modern, dark, cute, cyber
133
  author: '@你的名字',
134
  });
135
 
136
- // Days Data
137
- const days = reactive([
138
- { name: 'MON', label: '周一', active: true, time: '20:00', game: '杂谈', desc: '聊聊最近发生的趣事' },
139
- { name: 'TUE', label: '周二', active: false, time: '', game: '休息', desc: '' },
140
- { name: 'WED', label: '周三', active: true, time: '21:00', game: '恐怖游戏', desc: '不要被吓到哦' },
141
- { name: 'THU', label: '周四', active: false, time: '', game: '休息', desc: '' },
142
- { name: 'FRI', label: '周五', active: true, time: '20:00', game: '歌回', desc: '点歌环节' },
143
- { name: 'SAT', label: '周六', active: true, time: '14:00', game: '耐久直播', desc: '不通关不下播!' },
144
- { name: 'SUN', label: '周日', active: true, time: '19:00', game: '总结', desc: '本周总结 & 下周预告' },
145
- ]);
 
 
 
146
 
147
  // Theme Definitions
148
  const themes = {
149
- modern: {
150
- bg: '#ffffff',
151
- text: '#333333',
152
- accent: '#6366f1', // Indigo
153
- secondary: '#9ca3af',
154
- cardBg: '#f9fafb',
155
- cardBorder: '#e5e7eb',
156
- font: 'Noto Sans SC'
157
- },
158
- dark: {
159
- bg: '#111827',
160
- text: '#f3f4f6',
161
- accent: '#fbbf24', // Amber
162
- secondary: '#9ca3af',
163
- cardBg: '#1f2937',
164
- cardBorder: '#374151',
165
- font: 'Noto Sans SC'
166
- },
167
- cute: {
168
- bg: '#fff1f2',
169
- text: '#881337',
170
- accent: '#fb7185', // Rose
171
- secondary: '#fda4af',
172
- cardBg: '#ffffb1',
173
- cardBorder: '#fecdd3',
174
- font: 'Noto Sans SC'
175
- },
176
- cyber: {
177
- bg: '#09090b',
178
- text: '#00f2ff',
179
- accent: '#ff0055', // Neon Pink
180
- secondary: '#71717a',
181
- cardBg: '#18181b',
182
- cardBorder: '#27272a',
183
- font: 'Noto Sans SC'
184
- }
185
  };
186
 
187
  // Draw Function
188
  const draw = () => {
 
189
  const ctx = canvas.value.getContext('2d');
190
  const width = 1200;
191
- const height = 1600; // Vertical aspect ratio fits mobile better
192
 
193
- // Set canvas resolution
194
  canvas.value.width = width;
195
  canvas.value.height = height;
196
 
197
- const theme = themes[config.theme];
 
 
 
 
 
198
 
199
  // 1. Background
200
- ctx.fillStyle = theme.bg;
201
- ctx.fillRect(0, 0, width, height);
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
- // Optional: Draw grid or pattern based on theme
204
- if (config.theme === 'cyber') {
205
  ctx.strokeStyle = '#27272a';
206
  ctx.lineWidth = 2;
207
  for(let i=0; i<width; i+=50) { ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,height); ctx.stroke(); }
@@ -221,28 +336,27 @@
221
  ctx.font = `bold 40px ${theme.font}`;
222
  ctx.fillText(config.subtitle, width / 2, 190);
223
 
224
- // Author (Top Right or Bottom)
225
- // Let's put it top left for branding
226
  ctx.textAlign = 'left';
227
  ctx.font = `30px ${theme.font}`;
228
  ctx.fillStyle = theme.secondary;
229
  ctx.fillText(config.author, 50, 60);
230
 
231
-
232
  // 3. Days Grid
233
  const startY = 250;
234
  const gap = 30;
235
- const cardHeight = (height - startY - 100) / 7 - gap; // Distribute vertically
236
- const cardWidth = width - 100; // Full width with padding
237
  const cardX = 50;
238
 
239
  days.forEach((day, index) => {
240
  const y = startY + index * (cardHeight + gap);
241
 
242
- // Card Background
 
243
  ctx.fillStyle = theme.cardBg;
244
- // Rounded rect simulation
245
  ctx.fillRect(cardX, y, cardWidth, cardHeight);
 
246
 
247
  // Border
248
  ctx.strokeStyle = theme.cardBorder;
@@ -296,18 +410,79 @@
296
  ctx.fillText("Generated by Stream Schedule Maker", width/2, height - 30);
297
  };
298
 
299
- // Watchers
300
- watch([config, days], () => {
301
- // Use nextTick equivalent or small timeout to ensure font loaded (simplified)
302
- setTimeout(draw, 50);
303
- }, { deep: true });
304
 
305
- onMounted(() => {
306
- // Load fonts first then draw
307
- document.fonts.ready.then(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  draw();
309
- });
310
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
  const downloadImage = () => {
313
  const link = document.createElement('a');
@@ -316,11 +491,31 @@
316
  link.click();
317
  };
318
 
 
 
 
 
 
 
 
 
 
 
 
319
  return {
320
  config,
321
  days,
 
 
322
  canvas,
323
- downloadImage
 
 
 
 
 
 
 
324
  };
325
  }
326
  }).mount('#app');
 
17
  background-position: 0 0, 10px 10px;
18
  background-size: 20px 20px;
19
  }
20
+ .custom-scrollbar::-webkit-scrollbar {
21
+ width: 6px;
22
+ }
23
+ .custom-scrollbar::-webkit-scrollbar-track {
24
+ background: #f1f1f1;
25
+ }
26
+ .custom-scrollbar::-webkit-scrollbar-thumb {
27
+ background: #c7c7c7;
28
+ border-radius: 3px;
29
+ }
30
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
31
+ background: #a8a8a8;
32
+ }
33
+ /* Fade in animation */
34
+ @keyframes fadeIn {
35
+ from { opacity: 0; transform: translateY(-5px); }
36
+ to { opacity: 1; transform: translateY(0); }
37
+ }
38
+ .animate-fade-in {
39
+ animation: fadeIn 0.3s ease-out forwards;
40
+ }
41
  </style>
42
  </head>
43
  <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden flex flex-col">
 
46
  <!-- Header -->
47
  <header class="bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center shadow-sm z-10">
48
  <div class="flex items-center gap-3">
49
+ <div class="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center text-white text-xl shadow-md">
50
  <i class="fa-solid fa-calendar-week"></i>
51
  </div>
52
  <div>
53
+ <h1 class="text-xl font-bold text-gray-900 tracking-tight">直播日程表生成器</h1>
54
+ <p class="text-xs text-gray-500 font-medium">Stream Schedule Maker v1.1</p>
55
  </div>
56
  </div>
57
  <div class="flex gap-3">
58
+ <button @click="resetDemo" class="text-sm px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-gray-600 transition flex items-center gap-2">
59
+ <i class="fa-solid fa-rotate-left"></i> 重置演示
60
+ </button>
61
+ <button @click="triggerImport" class="text-sm px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-gray-600 transition flex items-center gap-2">
62
+ <i class="fa-solid fa-file-import"></i> 导入配置
63
+ </button>
64
+ <button @click="exportConfig" class="text-sm px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-gray-600 transition flex items-center gap-2">
65
+ <i class="fa-solid fa-file-export"></i> 导出配置
66
+ </button>
67
+ <input type="file" ref="fileInput" class="hidden" @change="handleImport" accept=".json">
68
+ <a href="https://github.com/duqing26" target="_blank" class="text-gray-400 hover:text-gray-900 transition ml-2 flex items-center">
69
  <i class="fa-brands fa-github text-xl"></i>
70
  </a>
71
  </div>
 
74
  <!-- Main Content -->
75
  <main class="flex-1 flex overflow-hidden">
76
  <!-- Sidebar: Editor -->
77
+ <div class="w-1/3 bg-white border-r border-gray-200 flex flex-col h-full shadow-xl z-10 flex-shrink-0">
78
+ <!-- Tabs -->
79
+ <div class="flex border-b border-gray-200">
80
+ <button @click="activeTab = 'settings'" :class="['flex-1 py-3 text-sm font-medium transition', activeTab === 'settings' ? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50']">
81
+ <i class="fa-solid fa-sliders mr-1"></i> 设置
82
+ </button>
83
+ <button @click="activeTab = 'schedule'" :class="['flex-1 py-3 text-sm font-medium transition', activeTab === 'schedule' ? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50']">
84
+ <i class="fa-solid fa-list-check mr-1"></i> 日程
85
+ </button>
86
+ </div>
87
+
88
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-gray-50/50">
89
+ <!-- Settings Tab -->
90
+ <div v-show="activeTab === 'settings'" class="p-5 space-y-6">
91
+ <!-- Basic Info -->
92
+ <div class="bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
93
+ <h3 class="text-sm font-bold text-gray-800 mb-3 border-l-4 border-indigo-500 pl-2">基本信息</h3>
94
+ <div class="space-y-3">
95
+ <div>
96
+ <label class="block text-xs font-medium text-gray-500 mb-1">主标题</label>
97
+ <input v-model="config.title" type="text" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition bg-gray-50 focus:bg-white">
98
+ </div>
99
+ <div>
100
+ <label class="block text-xs font-medium text-gray-500 mb-1">日期/副标题</label>
101
+ <input v-model="config.subtitle" type="text" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition bg-gray-50 focus:bg-white">
102
+ </div>
103
+ <div>
104
+ <label class="block text-xs font-medium text-gray-500 mb-1">作者/主播名</label>
105
+ <input v-model="config.author" type="text" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition bg-gray-50 focus:bg-white">
106
+ </div>
107
+ </div>
108
  </div>
109
+
110
+ <!-- Appearance -->
111
+ <div class="bg-white p-4 rounded-xl border border-gray-100 shadow-sm">
112
+ <h3 class="text-sm font-bold text-gray-800 mb-3 border-l-4 border-pink-500 pl-2">外观样式</h3>
113
+ <div class="space-y-4">
114
+ <div>
115
+ <label class="block text-xs font-medium text-gray-500 mb-1">预设主题</label>
116
+ <select v-model="config.theme" class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none transition bg-gray-50">
117
+ <option value="modern">⚪ 现代极简 (Modern)</option>
118
+ <option value="dark">⚫ 黑金科技 (Dark)</option>
119
+ <option value="cute">🌸 元气粉嫩 (Cute)</option>
120
+ <option value="cyber">🤖 赛博朋克 (Cyber)</option>
121
+ <option value="custom">🎨 自定义 (Custom)</option>
122
+ </select>
123
+ </div>
124
+
125
+ <!-- Custom Colors -->
126
+ <div v-if="config.theme === 'custom'" class="grid grid-cols-2 gap-3 p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300 animate-fade-in">
127
+ <div>
128
+ <label class="block text-xs text-gray-500 mb-1">背景色</label>
129
+ <div class="flex items-center gap-2">
130
+ <input type="color" v-model="customTheme.bg" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
131
+ <span class="text-xs font-mono text-gray-400">{{customTheme.bg}}</span>
132
+ </div>
133
+ </div>
134
+ <div>
135
+ <label class="block text-xs text-gray-500 mb-1">文字色</label>
136
+ <div class="flex items-center gap-2">
137
+ <input type="color" v-model="customTheme.text" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
138
+ <span class="text-xs font-mono text-gray-400">{{customTheme.text}}</span>
139
+ </div>
140
+ </div>
141
+ <div>
142
+ <label class="block text-xs text-gray-500 mb-1">强调色</label>
143
+ <div class="flex items-center gap-2">
144
+ <input type="color" v-model="customTheme.accent" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
145
+ <span class="text-xs font-mono text-gray-400">{{customTheme.accent}}</span>
146
+ </div>
147
+ </div>
148
+ <div>
149
+ <label class="block text-xs text-gray-500 mb-1">卡片底色</label>
150
+ <div class="flex items-center gap-2">
151
+ <input type="color" v-model="customTheme.cardBg" class="w-8 h-8 rounded cursor-pointer border-0 p-0">
152
+ <span class="text-xs font-mono text-gray-400">{{customTheme.cardBg}}</span>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <div>
158
+ <label class="block text-xs font-medium text-gray-500 mb-1">背景图片 (可选)</label>
159
+ <div class="flex items-center gap-2">
160
+ <label class="flex-1 cursor-pointer bg-gray-50 hover:bg-gray-100 border border-gray-300 border-dashed rounded-lg px-3 py-2 text-center transition group">
161
+ <span class="text-xs text-gray-500 group-hover:text-indigo-600"><i class="fa-solid fa-image mr-1"></i> 上传图片</span>
162
+ <input type="file" class="hidden" @change="handleBgUpload" accept="image/*">
163
+ </label>
164
+ <button v-if="bgImage" @click="bgImage = null" class="px-3 py-2 bg-red-50 hover:bg-red-100 text-red-500 rounded-lg border border-red-200 transition" title="清除背景">
165
+ <i class="fa-solid fa-trash"></i>
166
+ </button>
167
+ </div>
168
+ <div v-if="bgImage" class="mt-2 text-xs text-green-600 flex items-center gap-1">
169
+ <i class="fa-solid fa-check-circle"></i> 已加载背景图
170
+ </div>
171
+ </div>
172
+ </div>
173
  </div>
174
  </div>
 
175
 
176
+ <!-- Schedule Tab -->
177
+ <div v-show="activeTab === 'schedule'" class="p-5 space-y-4">
178
+ <div v-for="(day, index) in days" :key="index" class="bg-white p-3 rounded-xl border border-gray-200 shadow-sm hover:border-indigo-300 hover:shadow-md transition group">
179
+ <div class="flex justify-between items-center mb-3">
180
+ <div class="flex items-center gap-2">
181
+ <span class="w-8 h-8 rounded-full bg-indigo-50 text-indigo-600 flex items-center justify-center font-bold text-xs">{{ day.name.substring(0,1) }}</span>
182
+ <span class="font-bold text-sm text-gray-700">{{ day.label }}</span>
183
+ </div>
184
+ <label class="relative inline-flex items-center cursor-pointer">
185
+ <input type="checkbox" v-model="day.active" class="sr-only peer">
186
+ <div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
187
  </label>
188
  </div>
189
+
190
+ <div v-if="day.active" class="space-y-2 animate-fade-in pl-1">
191
  <div class="flex gap-2">
192
+ <div class="w-1/3">
193
+ <input v-model="day.time" placeholder="时间" class="w-full px-2 py-1.5 bg-gray-50 border border-gray-200 rounded text-sm focus:border-indigo-500 focus:bg-white outline-none transition placeholder-gray-400">
194
+ </div>
195
+ <div class="w-2/3">
196
+ <input v-model="day.game" placeholder="内容/游戏" class="w-full px-2 py-1.5 bg-gray-50 border border-gray-200 rounded text-sm focus:border-indigo-500 focus:bg-white outline-none transition placeholder-gray-400 font-medium">
197
+ </div>
198
  </div>
199
+ <textarea v-model="day.desc" placeholder="详细描述/备注 (可选)" rows="2" class="w-full px-2 py-1.5 bg-gray-50 border border-gray-200 rounded text-sm focus:border-indigo-500 focus:bg-white outline-none resize-none transition placeholder-gray-400"></textarea>
200
  </div>
201
+ <div v-else class="text-xs text-gray-400 italic text-center py-2 bg-gray-50 rounded border border-dashed border-gray-200">
202
+ 💤 休息日 / 无计划
203
  </div>
204
  </div>
205
  </div>
 
209
  <!-- Main: Preview -->
210
  <div class="flex-1 bg-gray-100 flex flex-col relative">
211
  <!-- Toolbar -->
212
+ <div class="absolute top-6 right-6 z-20 flex gap-3">
213
+ <div class="bg-white/90 backdrop-blur rounded-lg p-1 shadow-lg border border-gray-200 flex gap-1">
214
+ <button @click="downloadImage" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md shadow-sm flex items-center gap-2 transition font-medium text-sm">
215
+ <i class="fa-solid fa-download"></i> 保存图片 (PNG)
216
+ </button>
217
+ </div>
218
  </div>
219
 
220
  <!-- Canvas Container -->
221
  <div class="flex-1 overflow-auto flex items-center justify-center p-8 preview-container" id="canvas-wrapper">
222
+ <canvas ref="canvas" class="shadow-2xl rounded-sm max-h-full max-w-full ring-1 ring-black/5"></canvas>
223
+ </div>
224
+
225
+ <!-- Footer Info -->
226
+ <div class="absolute bottom-2 right-4 text-xs text-gray-400 select-none">
227
+ Preview Mode (Scale: Auto)
228
  </div>
229
  </div>
230
  </main>
 
233
  <script>
234
  const { createApp, ref, reactive, onMounted, watch } = Vue;
235
 
236
+ const DEMO_DAYS = [
237
+ { name: 'MON', label: '周一', active: true, time: '20:00', game: '杂谈', desc: '聊聊最近发生的趣事' },
238
+ { name: 'TUE', label: '周二', active: false, time: '', game: '休息', desc: '' },
239
+ { name: 'WED', label: '周三', active: true, time: '21:00', game: '恐怖游戏', desc: '不要被吓到哦' },
240
+ { name: 'THU', label: '周四', active: false, time: '', game: '休息', desc: '' },
241
+ { name: 'FRI', label: '周五', active: true, time: '20:00', game: '歌回', desc: '点歌环节' },
242
+ { name: 'SAT', label: '周六', active: true, time: '14:00', game: '耐久直播', desc: '不通关不下播!' },
243
+ { name: 'SUN', label: '周日', active: true, time: '19:00', game: '总结', desc: '本周总结 & 下周预告' },
244
+ ];
245
+
246
  createApp({
247
  setup() {
248
  const canvas = ref(null);
249
+ const activeTab = ref('schedule');
250
+ const fileInput = ref(null);
251
+ const bgImage = ref(null);
252
+ const bgImageObj = ref(null); // Stores the Image object
253
 
254
  // Configuration
255
  const config = reactive({
256
  title: 'WEEKLY SCHEDULE',
257
  subtitle: '2023.10.01 - 10.07',
258
+ theme: 'modern',
259
  author: '@你的名字',
260
  });
261
 
262
+ // Custom Theme State
263
+ const customTheme = reactive({
264
+ bg: '#ffffff',
265
+ text: '#333333',
266
+ accent: '#6366f1',
267
+ secondary: '#9ca3af',
268
+ cardBg: '#f9fafb',
269
+ cardBorder: '#e5e7eb',
270
+ font: 'Noto Sans SC'
271
+ });
272
+
273
+ // Days Data (Initialize with copy of DEMO)
274
+ const days = reactive(JSON.parse(JSON.stringify(DEMO_DAYS)));
275
 
276
  // Theme Definitions
277
  const themes = {
278
+ modern: { bg: '#ffffff', text: '#333333', accent: '#6366f1', secondary: '#9ca3af', cardBg: '#f9fafb', cardBorder: '#e5e7eb', font: 'Noto Sans SC' },
279
+ dark: { bg: '#111827', text: '#f3f4f6', accent: '#fbbf24', secondary: '#9ca3af', cardBg: '#1f2937', cardBorder: '#374151', font: 'Noto Sans SC' },
280
+ cute: { bg: '#fff1f2', text: '#881337', accent: '#fb7185', secondary: '#fda4af', cardBg: '#ffffb1', cardBorder: '#fecdd3', font: 'Noto Sans SC' },
281
+ cyber: { bg: '#09090b', text: '#00f2ff', accent: '#ff0055', secondary: '#71717a', cardBg: '#18181b', cardBorder: '#27272a', font: 'Noto Sans SC' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  };
283
 
284
  // Draw Function
285
  const draw = () => {
286
+ if (!canvas.value) return;
287
  const ctx = canvas.value.getContext('2d');
288
  const width = 1200;
289
+ const height = 1600;
290
 
 
291
  canvas.value.width = width;
292
  canvas.value.height = height;
293
 
294
+ // Determine current theme colors
295
+ let theme = config.theme === 'custom' ? customTheme : themes[config.theme];
296
+ // Fallback if custom fields are missing
297
+ if (config.theme === 'custom') {
298
+ theme = { ...themes.modern, ...customTheme };
299
+ }
300
 
301
  // 1. Background
302
+ if (bgImageObj.value) {
303
+ // Draw Image cover
304
+ const img = bgImageObj.value;
305
+ const scale = Math.max(width / img.width, height / img.height);
306
+ const x = (width / 2) - (img.width / 2) * scale;
307
+ const y = (height / 2) - (img.height / 2) * scale;
308
+ ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
309
+
310
+ // Add overlay for readability if needed
311
+ ctx.fillStyle = config.theme === 'dark' || config.theme === 'cyber' ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.7)';
312
+ ctx.fillRect(0, 0, width, height);
313
+ } else {
314
+ ctx.fillStyle = theme.bg;
315
+ ctx.fillRect(0, 0, width, height);
316
+ }
317
 
318
+ // Optional: Draw grid or pattern
319
+ if (config.theme === 'cyber' && !bgImageObj.value) {
320
  ctx.strokeStyle = '#27272a';
321
  ctx.lineWidth = 2;
322
  for(let i=0; i<width; i+=50) { ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,height); ctx.stroke(); }
 
336
  ctx.font = `bold 40px ${theme.font}`;
337
  ctx.fillText(config.subtitle, width / 2, 190);
338
 
339
+ // Author
 
340
  ctx.textAlign = 'left';
341
  ctx.font = `30px ${theme.font}`;
342
  ctx.fillStyle = theme.secondary;
343
  ctx.fillText(config.author, 50, 60);
344
 
 
345
  // 3. Days Grid
346
  const startY = 250;
347
  const gap = 30;
348
+ const cardHeight = (height - startY - 100) / 7 - gap;
349
+ const cardWidth = width - 100;
350
  const cardX = 50;
351
 
352
  days.forEach((day, index) => {
353
  const y = startY + index * (cardHeight + gap);
354
 
355
+ // Card Background (with transparency if bg image exists)
356
+ ctx.globalAlpha = bgImageObj.value ? 0.9 : 1.0;
357
  ctx.fillStyle = theme.cardBg;
 
358
  ctx.fillRect(cardX, y, cardWidth, cardHeight);
359
+ ctx.globalAlpha = 1.0;
360
 
361
  // Border
362
  ctx.strokeStyle = theme.cardBorder;
 
410
  ctx.fillText("Generated by Stream Schedule Maker", width/2, height - 30);
411
  };
412
 
413
+ // Helper: Load Image
414
+ const handleBgUpload = (event) => {
415
+ const file = event.target.files[0];
416
+ if (!file) return;
 
417
 
418
+ const reader = new FileReader();
419
+ reader.onload = (e) => {
420
+ bgImage.value = e.target.result;
421
+ const img = new Image();
422
+ img.onload = () => {
423
+ bgImageObj.value = img;
424
+ draw(); // Redraw when image is ready
425
+ };
426
+ img.src = e.target.result;
427
+ };
428
+ reader.readAsDataURL(file);
429
+ };
430
+
431
+ // Actions
432
+ const resetDemo = () => {
433
+ if(confirm('确定要重置为演示数据吗?当前修改将丢失。')) {
434
+ // Reset days
435
+ days.splice(0, days.length, ...JSON.parse(JSON.stringify(DEMO_DAYS)));
436
+ // Reset config (optional, kept simple)
437
+ config.title = 'WEEKLY SCHEDULE';
438
+ config.subtitle = '2023.10.01 - 10.07';
439
+ config.theme = 'modern';
440
+ bgImage.value = null;
441
+ bgImageObj.value = null;
442
  draw();
443
+ }
444
+ };
445
+
446
+ const exportConfig = () => {
447
+ const data = {
448
+ config: config,
449
+ days: days,
450
+ customTheme: customTheme
451
+ };
452
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
453
+ const url = URL.createObjectURL(blob);
454
+ const link = document.createElement('a');
455
+ link.href = url;
456
+ link.download = `schedule-config-${new Date().toISOString().slice(0,10)}.json`;
457
+ link.click();
458
+ URL.revokeObjectURL(url);
459
+ };
460
+
461
+ const triggerImport = () => {
462
+ fileInput.value.click();
463
+ };
464
+
465
+ const handleImport = (event) => {
466
+ const file = event.target.files[0];
467
+ if (!file) return;
468
+
469
+ const reader = new FileReader();
470
+ reader.onload = (e) => {
471
+ try {
472
+ const data = JSON.parse(e.target.result);
473
+ if (data.config) Object.assign(config, data.config);
474
+ if (data.days) days.splice(0, days.length, ...data.days);
475
+ if (data.customTheme) Object.assign(customTheme, data.customTheme);
476
+ alert('配置导入成功!');
477
+ draw();
478
+ } catch (err) {
479
+ alert('导入失败:无效的 JSON 文件');
480
+ }
481
+ };
482
+ reader.readAsText(file);
483
+ // Reset input
484
+ event.target.value = '';
485
+ };
486
 
487
  const downloadImage = () => {
488
  const link = document.createElement('a');
 
491
  link.click();
492
  };
493
 
494
+ // Watchers
495
+ watch([config, days, customTheme, bgImage], () => {
496
+ setTimeout(draw, 50);
497
+ }, { deep: true });
498
+
499
+ onMounted(() => {
500
+ document.fonts.ready.then(() => {
501
+ draw();
502
+ });
503
+ });
504
+
505
  return {
506
  config,
507
  days,
508
+ customTheme,
509
+ activeTab,
510
  canvas,
511
+ fileInput,
512
+ bgImage,
513
+ downloadImage,
514
+ handleBgUpload,
515
+ resetDemo,
516
+ exportConfig,
517
+ triggerImport,
518
+ handleImport
519
  };
520
  }
521
  }).mount('#app');