duqing2026 commited on
Commit
731e990
·
1 Parent(s): 4ec910f

Initial commit

Browse files
Files changed (5) hide show
  1. Dockerfile +15 -0
  2. README.md +63 -5
  3. app.py +15 -0
  4. requirements.txt +2 -0
  5. templates/index.html +499 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Create a user to avoid running as root
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+ ENV PATH="/home/user/.local/bin:$PATH"
14
+
15
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,10 +1,68 @@
1
  ---
2
- title: Chat Mockup Studio
3
- emoji: 🏢
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 社交对话生成工坊
3
+ emoji: 💬
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: docker
7
+ app_port: 7860
8
+ short_description: 逼真的微信/聊天记录生成工具,支持文本、图片、红包、转账等多种消息类型,一键导出长截图。
9
  pinned: false
10
  ---
11
 
12
+ # 社交对话生成工坊 (Chat Mockup Studio)
13
+
14
+ 一个专注于生成逼真社交聊天记录的工具,专为内容创作者、营销人员和产品经理设计。支持微信风格,提供高度可定制的编辑器和即时预览。
15
+
16
+ ## ✨ 核心功能
17
+
18
+ * **多类型消息支持**:
19
+ * 💬 **文本消息**:支持换行、表情。
20
+ * 📸 **图片消息**:上传本地图片。
21
+ * 🔊 **语音消息**:自定义时长,支持红点标记。
22
+ * 🧧 **红包/转账**:逼真的卡片样式。
23
+ * 🕒 **时间显示**:自定义系统时间标签。
24
+ * **高度可定制**:
25
+ * 🔋 **状态栏**:自定义电量、WiFi、信号、运营商、顶部标题。
26
+ * 🌓 **深色模式**:一键切换黑夜/白天模式。
27
+ * 🖼 **自定义背景**:支持上传聊天背景图。
28
+ * 👤 **头像定制**:左右侧头像自由上传。
29
+ * **一键导出**:
30
+ * 📷 **智能长截图**:自动根据消息长度生成完整长图,不再受限于屏幕高度。
31
+ * ⚡ **本地处理**:所有数据在浏览器端处理,保护隐私。
32
+
33
+ ## 🛠️ 技术栈
34
+
35
+ * **Frontend**: Vue 3, Tailwind CSS
36
+ * **Rendering**: HTML2Canvas
37
+ * **Backend**: Flask (Serving Static Files)
38
+ * **Deployment**: Docker
39
+
40
+ ## 🚀 快速开始
41
+
42
+ ### Docker 部署
43
+
44
+ ```bash
45
+ docker build -t chat-mockup-studio .
46
+ docker run -p 7860:7860 chat-mockup-studio
47
+ ```
48
+
49
+ 访问 `http://localhost:7860` 即可使用。
50
+
51
+ ### 本地开发
52
+
53
+ ```bash
54
+ pip install -r requirements.txt
55
+ python app.py
56
+ ```
57
+
58
+ ## 📝 使用指南
59
+
60
+ 1. **全局设置**:在左侧面板设置顶部标题(如"文件传输助手")、电量、时间等状态栏信息。
61
+ 2. **添加消息**:点击"对方"或"我"添加消息,支持文本、图片、语音等类型。
62
+ 3. **编辑内容**:点击消息列表中的条目,修改内容、上传头像。
63
+ 4. **调整顺序**:使用消息卡片右上角的箭头调整消息顺序。
64
+ 5. **导出图片**:点击左侧底部的"生成长截图"按钮,自动下载 PNG 图片。
65
+
66
+ ## ⚠️ 免责声明
67
+
68
+ 本工具仅供娱乐、创作和设计用途,请勿用于制作虚假证据或进行欺诈活动。使用者需对生成的图片内容负责。
app.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory
2
+ import os
3
+
4
+ app = Flask(__name__)
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/static/<path:path>')
11
+ def serve_static(path):
12
+ return send_from_directory('static', path)
13
+
14
+ if __name__ == '__main__':
15
+ app.run(host='0.0.0.0', port=7860)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask==3.0.0
2
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>社交对话生成工坊 - Chat Mockup Studio</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
13
+ body { font-family: 'Noto Sans SC', sans-serif; }
14
+ .ios-scrollbar::-webkit-scrollbar { display: none; }
15
+ .ios-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
16
+
17
+ /* WeChat Green */
18
+ .bg-wechat-green { background-color: #95ec69; }
19
+ .bg-wechat-dark { background-color: #111111; }
20
+
21
+ /* Chat Bubble Triangle */
22
+ .bubble-left::before {
23
+ content: "";
24
+ position: absolute;
25
+ right: 100%;
26
+ top: 14px;
27
+ border: 6px solid transparent;
28
+ border-right-color: #ffffff;
29
+ }
30
+ .bubble-right::before {
31
+ content: "";
32
+ position: absolute;
33
+ left: 100%;
34
+ top: 14px;
35
+ border: 6px solid transparent;
36
+ border-left-color: #95ec69;
37
+ }
38
+ /* Dark Mode Bubbles */
39
+ .dark .bubble-left::before { border-right-color: #2c2c2c; }
40
+ .dark .bubble-right::before { border-left-color: #2b7936; } /* Adjusted for dark mode green */
41
+
42
+ .phone-frame {
43
+ box-shadow: 0 0 0 12px #1f2937, 0 0 0 14px #4b5563;
44
+ border-radius: 40px;
45
+ overflow: hidden;
46
+ width: 375px;
47
+ height: 812px; /* iPhone X size */
48
+ position: relative;
49
+ }
50
+ /* Dynamic Height for Screenshot */
51
+ .phone-frame.screenshot-mode {
52
+ height: auto;
53
+ min-height: 812px;
54
+ border-radius: 0;
55
+ box-shadow: none;
56
+ overflow: visible;
57
+ }
58
+ </style>
59
+ </head>
60
+ <body class="bg-gray-100 h-screen flex overflow-hidden">
61
+ <div id="app" class="flex w-full h-full">
62
+ <!-- Sidebar Controls -->
63
+ <div class="w-96 bg-white border-r border-gray-200 flex flex-col h-full overflow-y-auto p-4 shadow-lg z-10">
64
+ <h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
65
+ <i class="fas fa-comments text-green-500"></i> 社交对话生成工坊
66
+ </h1>
67
+
68
+ <!-- Global Settings -->
69
+ <div class="mb-6 border-b border-gray-200 pb-4">
70
+ <h2 class="text-lg font-semibold mb-3 text-gray-700">全局设置</h2>
71
+ <div class="space-y-3">
72
+ <div>
73
+ <label class="block text-sm font-medium text-gray-600">顶部标题</label>
74
+ <input v-model="settings.title" type="text" class="mt-1 w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-green-500 focus:border-green-500" placeholder="例如:文件传输助手">
75
+ </div>
76
+ <div class="grid grid-cols-2 gap-2">
77
+ <div>
78
+ <label class="block text-sm font-medium text-gray-600">时间</label>
79
+ <input v-model="settings.time" type="text" class="mt-1 w-full rounded-md border-gray-300 shadow-sm border p-2" placeholder="12:00">
80
+ </div>
81
+ <div>
82
+ <label class="block text-sm font-medium text-gray-600">电量 %</label>
83
+ <input v-model="settings.battery" type="number" class="mt-1 w-full rounded-md border-gray-300 shadow-sm border p-2" min="1" max="100">
84
+ </div>
85
+ </div>
86
+ <div class="flex items-center gap-4">
87
+ <label class="flex items-center">
88
+ <input type="checkbox" v-model="settings.showWifi" class="rounded text-green-500 focus:ring-green-500">
89
+ <span class="ml-2 text-sm text-gray-600">显示 WiFi</span>
90
+ </label>
91
+ <label class="flex items-center">
92
+ <input type="checkbox" v-model="settings.showEar" class="rounded text-green-500 focus:ring-green-500">
93
+ <span class="ml-2 text-sm text-gray-600">听筒模式</span>
94
+ </label>
95
+ <label class="flex items-center">
96
+ <input type="checkbox" v-model="settings.isDark" class="rounded text-green-500 focus:ring-green-500">
97
+ <span class="ml-2 text-sm text-gray-600">深色模式</span>
98
+ </label>
99
+ </div>
100
+ <div>
101
+ <label class="block text-sm font-medium text-gray-600">背景图片</label>
102
+ <input type="file" @change="handleBgUpload" accept="image/*" class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-green-50 file:text-green-700 hover:file:bg-green-100">
103
+ <button v-if="settings.bgImage" @click="settings.bgImage = ''" class="text-xs text-red-500 mt-1">清除背景</button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Message Editor -->
109
+ <div class="flex-1 overflow-y-auto">
110
+ <h2 class="text-lg font-semibold mb-3 text-gray-700 flex justify-between items-center">
111
+ 消息列表
112
+ <div class="flex gap-2">
113
+ <button @click="addMessage('text', 'left')" class="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded" title="添加左侧消息"><i class="fas fa-arrow-left"></i></button>
114
+ <button @click="addMessage('time')" class="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded" title="添加时间"><i class="fas fa-clock"></i></button>
115
+ <button @click="addMessage('text', 'right')" class="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded" title="添加右侧消息"><i class="fas fa-arrow-right"></i></button>
116
+ </div>
117
+ </h2>
118
+
119
+ <div class="space-y-3">
120
+ <div v-for="(msg, index) in messages" :key="msg.id" class="bg-gray-50 p-3 rounded-lg border border-gray-200 group relative hover:border-green-400 transition-colors">
121
+ <!-- Tools -->
122
+ <div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 flex gap-1 transition-opacity">
123
+ <button @click="moveUp(index)" class="text-gray-400 hover:text-blue-500"><i class="fas fa-chevron-up"></i></button>
124
+ <button @click="moveDown(index)" class="text-gray-400 hover:text-blue-500"><i class="fas fa-chevron-down"></i></button>
125
+ <button @click="removeMessage(index)" class="text-gray-400 hover:text-red-500"><i class="fas fa-trash"></i></button>
126
+ </div>
127
+
128
+ <!-- Content Editor -->
129
+ <div v-if="msg.type === 'time'" class="text-center">
130
+ <input v-model="msg.content" class="text-xs text-center bg-transparent border-b border-gray-300 focus:border-green-500 outline-none w-20" placeholder="时间">
131
+ </div>
132
+
133
+ <div v-else class="flex gap-2 items-start">
134
+ <div class="flex flex-col items-center gap-1">
135
+ <span class="text-xs font-bold text-gray-500">{{ msg.side === 'left' ? '对方' : '我' }}</span>
136
+ <div class="w-8 h-8 rounded overflow-hidden bg-gray-300 relative cursor-pointer" @click="$refs['avatarInput'+msg.id].click()">
137
+ <img v-if="msg.avatar" :src="msg.avatar" class="w-full h-full object-cover">
138
+ <i v-else class="fas fa-user absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-gray-500"></i>
139
+ <input type="file" :ref="'avatarInput'+msg.id" class="hidden" accept="image/*" @change="(e) => handleAvatarUpload(e, msg)">
140
+ </div>
141
+ </div>
142
+
143
+ <div class="flex-1">
144
+ <select v-model="msg.contentType" class="w-full mb-1 text-xs border-gray-300 rounded">
145
+ <option value="text">文本</option>
146
+ <option value="image">图片</option>
147
+ <option value="voice">语音</option>
148
+ <option value="redpacket">红包</option>
149
+ <option value="transfer">转账</option>
150
+ </select>
151
+
152
+ <textarea v-if="msg.contentType === 'text'" v-model="msg.content" rows="2" class="w-full text-sm p-1 border rounded resize-none" placeholder="输入消息内容..."></textarea>
153
+
154
+ <div v-if="msg.contentType === 'image'" class="text-xs">
155
+ <input type="file" @change="(e) => handleImageMsgUpload(e, msg)" accept="image/*" class="w-full">
156
+ <img v-if="msg.image" :src="msg.image" class="mt-1 h-16 rounded border">
157
+ </div>
158
+
159
+ <div v-if="msg.contentType === 'voice'" class="flex items-center gap-2">
160
+ <input v-model.number="msg.duration" type="number" class="w-16 p-1 text-xs border rounded" placeholder="秒数">
161
+ <span class="text-xs">秒</span>
162
+ <label class="flex items-center text-xs ml-2">
163
+ <input type="checkbox" v-model="msg.unread" class="mr-1"> 红点
164
+ </label>
165
+ </div>
166
+
167
+ <div v-if="msg.contentType === 'redpacket'" class="text-xs">
168
+ <input v-model="msg.content" class="w-full p-1 border rounded mb-1" placeholder="恭喜发财,大吉大利">
169
+ </div>
170
+
171
+ <div v-if="msg.contentType === 'transfer'" class="text-xs">
172
+ <input v-model="msg.amount" class="w-full p-1 border rounded mb-1" placeholder="¥ 100.00">
173
+ <input v-model="msg.content" class="w-full p-1 border rounded" placeholder="备注(可选)">
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="mt-4 flex justify-center">
181
+ <button @click="addMessage('text', 'left')" class="mx-1 px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm text-gray-600">+ 对方</button>
182
+ <button @click="addMessage('text', 'right')" class="mx-1 px-3 py-1 bg-green-100 hover:bg-green-200 rounded text-sm text-green-600">+ 我</button>
183
+ </div>
184
+ </div>
185
+
186
+ <div class="mt-4 pt-4 border-t border-gray-200">
187
+ <button @click="exportImage" class="w-full bg-green-500 hover:bg-green-600 text-white py-2 rounded-lg font-bold shadow transition-transform active:scale-95 flex justify-center items-center gap-2">
188
+ <i class="fas fa-download"></i> 生成长截图
189
+ </button>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Preview Area -->
194
+ <div class="flex-1 bg-gray-200 flex justify-center items-center p-8 overflow-auto relative">
195
+ <div id="capture-area" class="phone-frame bg-gray-100 flex flex-col relative transition-all duration-300"
196
+ :class="{ 'dark': settings.isDark, 'screenshot-mode': isExporting }">
197
+
198
+ <!-- Background Image -->
199
+ <div v-if="settings.bgImage" class="absolute inset-0 z-0 bg-cover bg-center opacity-100" :style="{ backgroundImage: `url(${settings.bgImage})` }"></div>
200
+ <div v-else class="absolute inset-0 z-0 bg-[#ededed] dark:bg-[#111111]"></div>
201
+
202
+ <!-- Status Bar -->
203
+ <div class="h-11 px-6 flex justify-between items-center z-10 text-black dark:text-white text-sm font-semibold select-none relative">
204
+ <!-- Notch Area (hidden but occupies space) -->
205
+ <div class="absolute left-0 right-0 top-0 h-7 z-[-1]"></div>
206
+
207
+ <div class="pl-2">{{ settings.time }}</div>
208
+ <div class="flex gap-1.5 items-center pr-2">
209
+ <i v-if="settings.showSignal" class="fas fa-signal text-xs"></i>
210
+ <i v-if="settings.showWifi" class="fas fa-wifi text-xs"></i>
211
+ <div class="flex items-center gap-1 ml-1">
212
+ <span class="text-[10px]">{{ settings.battery }}%</span>
213
+ <div class="w-6 h-3 border border-current rounded-[3px] relative p-[1px]">
214
+ <div class="h-full bg-current rounded-[1px]" :style="{ width: settings.battery + '%' }"></div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- WeChat Header -->
221
+ <div class="h-11 px-4 flex items-center justify-between z-10 text-black dark:text-white border-b border-gray-300/50 dark:border-white/10 bg-[#ededed]/90 dark:bg-[#111111]/90 backdrop-blur-sm">
222
+ <div class="w-16 flex items-center gap-1 cursor-pointer">
223
+ <i class="fas fa-chevron-left text-lg"></i>
224
+ <span class="text-base">微信</span> <!-- Or count (12) -->
225
+ </div>
226
+ <div class="flex-1 text-center font-medium text-lg truncate px-2">
227
+ {{ settings.title }}
228
+ <i v-if="settings.showEar" class="fas fa-ear-listen ml-1 text-gray-400 text-xs"></i>
229
+ </div>
230
+ <div class="w-16 flex justify-end">
231
+ <i class="fas fa-ellipsis-h text-lg"></i>
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Chat Area -->
236
+ <div class="flex-1 overflow-y-auto p-4 z-10 ios-scrollbar space-y-4" ref="chatContainer">
237
+ <div v-for="msg in messages" :key="msg.id" class="w-full">
238
+
239
+ <!-- Time -->
240
+ <div v-if="msg.type === 'time'" class="flex justify-center mb-2">
241
+ <span class="text-xs text-white/90 bg-gray-300/60 dark:bg-white/10 px-2 py-0.5 rounded text-shadow-sm">{{ msg.content }}</span>
242
+ </div>
243
+
244
+ <!-- System Notice (Not implemented in editor yet, but logic is here) -->
245
+
246
+ <!-- Message Bubble -->
247
+ <div v-else class="flex gap-2.5" :class="{ 'flex-row-reverse': msg.side === 'right' }">
248
+ <!-- Avatar -->
249
+ <div class="w-10 h-10 rounded-md overflow-hidden bg-gray-300 flex-shrink-0 shadow-sm">
250
+ <img :src="msg.avatar || (msg.side === 'left' ? 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix' : 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka')" crossorigin="anonymous" class="w-full h-full object-cover">
251
+ </div>
252
+
253
+ <!-- Content -->
254
+ <div class="max-w-[70%]">
255
+ <!-- Name (Optional, usually hidden in P2P chat) -->
256
+ <!-- <div v-if="msg.side === 'left'" class="text-xs text-gray-500 mb-1 ml-1">{{ msg.name }}</div> -->
257
+
258
+ <!-- Bubble -->
259
+ <div class="relative px-3 py-2.5 rounded-md text-base leading-relaxed break-words shadow-sm min-h-[40px] flex items-center"
260
+ :class="[
261
+ msg.side === 'right' ? 'bg-wechat-green bubble-right text-black' : 'bg-white dark:bg-[#2c2c2c] dark:text-white bubble-left text-black',
262
+ msg.contentType === 'image' ? '!bg-transparent !p-0 shadow-none bubble-none' : '',
263
+ msg.contentType === 'redpacket' ? '!bg-[#fa9d3b] !p-0 overflow-hidden bubble-none w-60' : '',
264
+ msg.contentType === 'transfer' ? '!bg-[#fa9d3b] !p-0 overflow-hidden bubble-none w-60' : ''
265
+ ]">
266
+
267
+ <!-- Text -->
268
+ <span v-if="msg.contentType === 'text'" class="whitespace-pre-wrap">{{ msg.content }}</span>
269
+
270
+ <!-- Image -->
271
+ <img v-if="msg.contentType === 'image' && msg.image" :src="msg.image" class="rounded-md max-w-full border border-gray-200 dark:border-gray-700">
272
+
273
+ <!-- Voice -->
274
+ <div v-if="msg.contentType === 'voice'" class="flex items-center gap-2 min-w-[60px] cursor-pointer" :style="{ width: Math.min(60 + msg.duration * 5, 200) + 'px' }">
275
+ <i class="fas fa-rss transform rotate-45" :class="{ 'rotate-[225deg]': msg.side === 'right' }"></i>
276
+ <span class="ml-auto text-sm" v-if="msg.side === 'right'">{{ msg.duration }}''</span>
277
+ <span class="mr-auto text-sm" v-if="msg.side === 'left'">{{ msg.duration }}''</span>
278
+ <div v-if="msg.unread && msg.side === 'left'" class="absolute -right-3 top-0 w-2 h-2 bg-red-500 rounded-full"></div>
279
+ </div>
280
+
281
+ <!-- Red Packet -->
282
+ <div v-if="msg.contentType === 'redpacket'" class="flex flex-col w-full">
283
+ <div class="bg-[#fa9d3b] p-3 flex items-center gap-3">
284
+ <div class="w-10 h-12 bg-[#f8e7a8] rounded flex items-center justify-center text-[#fa9d3b]">
285
+ <i class="fas fa-yen-sign"></i>
286
+ </div>
287
+ <div class="text-white">
288
+ <div class="text-base">{{ msg.content }}</div>
289
+ </div>
290
+ </div>
291
+ <div class="bg-white p-1 px-3 text-xs text-gray-400">微信红包</div>
292
+ </div>
293
+
294
+ <!-- Transfer -->
295
+ <div v-if="msg.contentType === 'transfer'" class="flex flex-col w-full">
296
+ <div class="bg-[#fa9d3b] p-3 flex items-center gap-3">
297
+ <div class="w-10 h-10 border-2 border-white rounded-full flex items-center justify-center text-white">
298
+ <i class="fas fa-arrow-right-arrow-left"></i>
299
+ </div>
300
+ <div class="text-white">
301
+ <div class="text-base">{{ msg.amount }}</div>
302
+ <div class="text-xs opacity-80">{{ msg.content || '转账给你' }}</div>
303
+ </div>
304
+ </div>
305
+ <div class="bg-white p-1 px-3 text-xs text-gray-400">微信转账</div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- Footer Input (Visual Only) -->
314
+ <div class="h-14 bg-[#f7f7f7] dark:bg-[#111111] border-t border-gray-300 dark:border-white/10 flex items-center px-3 gap-3 z-10">
315
+ <i class="far fa-keyboard text-2xl text-gray-600 dark:text-gray-400"></i>
316
+ <div class="flex-1 h-9 bg-white dark:bg-[#2c2c2c] rounded px-3 flex items-center text-gray-400 text-sm"></div>
317
+ <i class="far fa-face-smile text-2xl text-gray-600 dark:text-gray-400"></i>
318
+ <i class="fas fa-plus-circle text-2xl text-gray-600 dark:text-gray-400"></i>
319
+ </div>
320
+
321
+ <!-- Home Indicator -->
322
+ <div class="h-8 bg-[#f7f7f7] dark:bg-[#111111] flex justify-center pt-2 z-10">
323
+ <div class="w-32 h-1 bg-gray-800 dark:bg-white rounded-full"></div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ <script>
330
+ const { createApp, ref, reactive } = Vue
331
+
332
+ createApp({
333
+ setup() {
334
+ const isExporting = ref(false)
335
+
336
+ const settings = reactive({
337
+ title: '文件传输助手',
338
+ time: '12:00',
339
+ battery: 85,
340
+ showWifi: true,
341
+ showSignal: true,
342
+ showEar: false,
343
+ isDark: false,
344
+ bgImage: ''
345
+ })
346
+
347
+ const messages = ref([
348
+ { id: 1, type: 'time', content: '10:30' },
349
+ {
350
+ id: 2,
351
+ type: 'message',
352
+ side: 'left',
353
+ contentType: 'text',
354
+ content: '老板,那个项目进度怎么样了?',
355
+ avatar: ''
356
+ },
357
+ {
358
+ id: 3,
359
+ type: 'message',
360
+ side: 'right',
361
+ contentType: 'text',
362
+ content: '已经完成了,正在最后测试!',
363
+ avatar: ''
364
+ },
365
+ {
366
+ id: 4,
367
+ type: 'message',
368
+ side: 'right',
369
+ contentType: 'voice',
370
+ duration: 5,
371
+ avatar: ''
372
+ },
373
+ {
374
+ id: 5,
375
+ type: 'message',
376
+ side: 'left',
377
+ contentType: 'redpacket',
378
+ content: '辛苦了!买杯咖啡喝',
379
+ avatar: ''
380
+ }
381
+ ])
382
+
383
+ const addMessage = (type, side = 'left') => {
384
+ const id = Date.now()
385
+ if (type === 'time') {
386
+ messages.value.push({ id, type: 'time', content: '12:00' })
387
+ } else {
388
+ messages.value.push({
389
+ id,
390
+ type: 'message',
391
+ side,
392
+ contentType: 'text',
393
+ content: '新消息',
394
+ avatar: '',
395
+ image: '',
396
+ duration: 3,
397
+ amount: '¥ 88.00',
398
+ unread: true
399
+ })
400
+ }
401
+ }
402
+
403
+ const removeMessage = (index) => {
404
+ messages.value.splice(index, 1)
405
+ }
406
+
407
+ const moveUp = (index) => {
408
+ if (index > 0) {
409
+ const temp = messages.value[index]
410
+ messages.value[index] = messages.value[index - 1]
411
+ messages.value[index - 1] = temp
412
+ }
413
+ }
414
+
415
+ const moveDown = (index) => {
416
+ if (index < messages.value.length - 1) {
417
+ const temp = messages.value[index]
418
+ messages.value[index] = messages.value[index + 1]
419
+ messages.value[index + 1] = temp
420
+ }
421
+ }
422
+
423
+ const handleAvatarUpload = (event, msg) => {
424
+ const file = event.target.files[0]
425
+ if (file) {
426
+ const reader = new FileReader()
427
+ reader.onload = (e) => {
428
+ msg.avatar = e.target.result
429
+ }
430
+ reader.readAsDataURL(file)
431
+ }
432
+ }
433
+
434
+ const handleImageMsgUpload = (event, msg) => {
435
+ const file = event.target.files[0]
436
+ if (file) {
437
+ const reader = new FileReader()
438
+ reader.onload = (e) => {
439
+ msg.image = e.target.result
440
+ }
441
+ reader.readAsDataURL(file)
442
+ }
443
+ }
444
+
445
+ const handleBgUpload = (event) => {
446
+ const file = event.target.files[0]
447
+ if (file) {
448
+ const reader = new FileReader()
449
+ reader.onload = (e) => {
450
+ settings.bgImage = e.target.result
451
+ }
452
+ reader.readAsDataURL(file)
453
+ }
454
+ }
455
+
456
+ const exportImage = async () => {
457
+ isExporting.value = true
458
+ // Wait for DOM update
459
+ await new Promise(r => setTimeout(r, 100))
460
+
461
+ const element = document.getElementById('capture-area')
462
+
463
+ try {
464
+ const canvas = await html2canvas(element, {
465
+ scale: 2,
466
+ useCORS: true,
467
+ backgroundColor: settings.isDark ? '#111111' : '#f0f0f0'
468
+ })
469
+
470
+ const link = document.createElement('a')
471
+ link.download = `chat_mockup_${Date.now()}.png`
472
+ link.href = canvas.toDataURL('image/png')
473
+ link.click()
474
+ } catch (err) {
475
+ console.error('Export failed', err)
476
+ alert('导出失败,请重试')
477
+ } finally {
478
+ isExporting.value = false
479
+ }
480
+ }
481
+
482
+ return {
483
+ settings,
484
+ messages,
485
+ addMessage,
486
+ removeMessage,
487
+ moveUp,
488
+ moveDown,
489
+ handleAvatarUpload,
490
+ handleImageMsgUpload,
491
+ handleBgUpload,
492
+ exportImage,
493
+ isExporting
494
+ }
495
+ }
496
+ }).mount('#app')
497
+ </script>
498
+ </body>
499
+ </html>