Procházet zdrojové kódy

Merge branch 'pre' of http://git.bjzxtw.org.cn:3000/zxt/admin_home into pre

Jing před 1 dnem
rodič
revize
7ebba647b7

+ 4 - 4
src/layout/components/Chat/ChatPanel.vue

@@ -1657,7 +1657,7 @@
           // 通过 WebSocket 发送心跳响应
           if (wsService.getReadyState() === WebSocket.OPEN) {
             wsService.send(heartbeatResponse);
-            console.log("心跳响应已发送:", heartbeatResponse);
+            // console.log("心跳响应已发送:", heartbeatResponse);
           } else {
             console.warn("WebSocket 连接已断开,无法发送心跳响应");
           }
@@ -1672,10 +1672,10 @@
           this.sendHeartbeat();
         }, 30000);
         
-        // 每60秒检查一次连接状态
+        // 每90秒检查一次连接状态
         this.connectionCheckInterval = setInterval(() => {
           this.checkConnectionHealth();
-        }, 60000);
+        }, 90000);
       },
       
       // 发送心跳消息
@@ -1738,7 +1738,7 @@
       //1.1获取会话列表
       getConversationList(){
         this.$store.dispatch('chat/getConversation',{}).then(res=> {
-          console.log(res)
+          // console.log(res)
           let data = res.data;
           for(let item of data){
             item.status = 0; //默认未选中

+ 2 - 2
src/layout/components/Navbar.vue

@@ -837,9 +837,9 @@ export default {
     //2.1 获取通知消息列表
     getMsg() {
       this.$store.dispatch('news/getMSG').then(response => {
-        console.log('response1111111111', response);
+        // console.log('response1111111111', response);
         this.msg = response.data;
-        console.log('response1111111111msg', this.msg);
+        // console.log('response1111111111msg', this.msg);
       }).catch(error => {
         console.log(error);
       });

+ 5 - 1
src/permission.js

@@ -160,7 +160,11 @@ router.beforeEach(async (to, from, next) => {
             }
             next()
         } else {
-            next(`/login`)
+            if(to.path === '/file'){
+                next()
+            }else{
+                next(`/login`)
+            }
             NProgress.done()
         }
     }

+ 5 - 0
src/router/index.js

@@ -52,6 +52,11 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/file',
+    component: () => import('@/views/file/index'),
+    hidden: true
+  },
   {
     path: '/login',
     component: () => import('@/views/login/index'),

+ 4 - 3
src/utils/websocketService.js

@@ -13,7 +13,7 @@ class WebSocketService {
     }
     const baseUrls = baseUrl.WebsocketUrl; // 替换为你的 ws 地址
     const url = `${baseUrls}?token=${token}`;
-    console.log('#############websocket url:', url);
+    // console.log('#############websocket url:', url);
     this.ws = new window.WebSocket(url);
 
     this.ws.onopen = () => {
@@ -22,18 +22,19 @@ class WebSocketService {
     };
 
     this.ws.onmessage = (event) => {
-      console.log('#############websocket event:', event);
+      // console.log('#############websocket event:', event);
       let message;
       
       try {
         // 尝试解析为 JSON
         message = JSON.parse(event.data);
+        console.log('#############websocket message:', message);
       } catch (error) {
         // 如果不是 JSON 格式,直接使用原始字符串
         message = event.data;
       }
       
-      console.log('#############websocket message:', message);
+      // console.log('#############websocket message:', message);
       this.messageListeners.forEach(cb => {
         if (typeof cb === 'function') {
           cb(message);

+ 129 - 0
src/views/file/README.md

@@ -0,0 +1,129 @@
+# 文件上传页面使用说明
+
+## 功能描述
+
+这个H5页面是为小程序提供的web-view页面,主要功能包括:
+
+1. **文件选择**:支持选择多种格式的文件(图片、文档、视频等)
+2. **文件预览**:显示已选择的文件列表,包含文件名、大小等信息
+3. **文件管理**:可以删除不需要的文件
+4. **提交给小程序**:将选择的文件数据提交给小程序
+
+## 支持的文件格式
+
+- 图片:jpg, jpeg, png, gif, bmp, webp
+- 文档:pdf, doc, docx, xls, xlsx, txt
+- 视频:mp4, avi, mov
+- 音频:mp3, wav, aac
+
+## 文件大小限制
+
+- 单个文件最大50MB
+- 建议文件总大小不超过100MB
+
+## 使用方法
+
+### 1. 在小程序中嵌入
+
+在小程序的web-view组件中加载此页面:
+
+```javascript
+// 小程序页面
+<web-view src="https://your-domain.com/file-upload"></web-view>
+```
+
+### 2. 接收文件数据
+
+在小程序中监听web-view发送的消息:
+
+```javascript
+// 小程序页面
+Page({
+  onLoad() {
+    // 监听web-view发送的消息
+    wx.onWebviewMessage((message) => {
+      if (message.data.type === 'file_upload') {
+        const files = message.data.files
+        console.log('接收到文件数据:', files)
+        
+        // 处理文件数据
+        this.handleFiles(files)
+      }
+    })
+  },
+  
+  handleFiles(files) {
+    files.forEach(file => {
+      console.log('文件名:', file.name)
+      console.log('文件类型:', file.type)
+      console.log('文件大小:', file.size)
+      console.log('文件数据:', file.data) // Base64格式
+    })
+  }
+})
+```
+
+### 3. 文件数据结构
+
+每个文件对象包含以下属性:
+
+```javascript
+{
+  name: "文件名.jpg",
+  type: "image/jpeg",
+  size: 1024000, // 字节
+  data: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." // Base64数据
+}
+```
+
+## 技术实现
+
+### 1. 微信环境检测
+
+页面会自动检测是否在微信环境中运行,如果不是会显示提示信息。
+
+### 2. 文件处理
+
+- 使用HTML5 File API进行文件选择
+- 将文件转换为Base64格式以便传输
+- 支持多文件同时选择
+
+### 3. 与小程序通信
+
+使用微信提供的API与小程序进行通信:
+
+- `window.wx.miniProgram.postMessage()` - 在web-view中发送消息
+- `WeixinJSBridge.invoke()` - 备用通信方式
+
+## 注意事项
+
+1. **必须在微信环境中使用**:此页面依赖微信的API,只能在微信中正常使用
+2. **文件大小限制**:大文件会影响传输性能,建议控制文件大小
+3. **网络环境**:需要稳定的网络连接以确保文件传输成功
+4. **浏览器兼容性**:主要支持现代浏览器,特别是微信内置浏览器
+
+## 样式定制
+
+页面使用了Element UI组件库,可以通过修改CSS来自定义样式:
+
+- 主容器:`.file-upload-container`
+- 上传区域:`.upload-area`
+- 文件列表:`.file-list`
+- 文件项:`.file-item`
+
+## 错误处理
+
+页面包含以下错误处理机制:
+
+- 文件大小超限提示
+- 重复文件检测
+- 网络错误处理
+- 微信环境检测
+
+## 开发调试
+
+在开发过程中,可以通过以下方式调试:
+
+1. 在浏览器控制台查看日志
+2. 使用微信开发者工具进行调试
+3. 检查网络请求和响应 

+ 381 - 0
src/views/file/index.vue

@@ -0,0 +1,381 @@
+<template>
+  <div class="file-upload-container">
+    <div class="header">
+      <h2>文件上传</h2>
+      <p>选择文件后提交给小程序</p>
+    </div>
+
+    <div class="upload-area" @click="triggerFileInput">
+      <div class="upload-icon">
+        <i class="el-icon-upload"></i>
+      </div>
+      <div class="upload-text">
+        <p>点击选择文件</p>
+        <p class="upload-hint">支持图片、文档、视频等格式</p>
+      </div>
+      <input
+        ref="fileInput"
+        type="file"
+        multiple
+        accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.mp4,.mp3"
+        @change="handleFileSelect"
+        style="display: none"
+      />
+    </div>
+
+    <div v-if="selectedFiles.length > 0" class="file-list">
+      <h3>已选择的文件</h3>
+      <div class="file-item" v-for="(file, index) in selectedFiles" :key="index">
+        <div class="file-info">
+          <div class="file-icon">
+            <i :class="getFileIcon(file.type)"></i>
+          </div>
+          <div class="file-details">
+            <div class="file-name">{{ file.name }}</div>
+            <div class="file-size">{{ formatFileSize(file.size) }}</div>
+          </div>
+        </div>
+        <div class="file-actions">
+          <el-button 
+            type="danger" 
+            size="mini" 
+            @click="removeFile(index)"
+            icon="el-icon-delete"
+          >
+            删除
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="submit-area">
+      <el-button 
+        type="primary" 
+        size="large" 
+        @click="submitToMiniProgram"
+        :disabled="selectedFiles.length === 0"
+        :loading="submitting"
+      >
+        {{ submitting ? '提交中...' : '提交给小程序' }}
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FileUpload',
+  data() {
+    return {
+      selectedFiles: [],
+      submitting: false
+    }
+  },
+  mounted() {
+    this.checkWechatEnvironment()
+  },
+  methods: {
+    // 检查微信环境
+    checkWechatEnvironment() {
+      const isWechat = /micromessenger/i.test(navigator.userAgent)
+      if (!isWechat) {
+        this.$message.warning('请在微信中打开此页面')
+      }
+    },
+
+    // 触发文件选择
+    triggerFileInput() {
+      this.$refs.fileInput.click()
+    },
+
+    // 处理文件选择
+    handleFileSelect(event) {
+      const files = Array.from(event.target.files)
+      
+      files.forEach(file => {
+        // 检查文件大小限制 (50MB)
+        if (file.size > 50 * 1024 * 1024) {
+          this.$message.error(`文件 ${file.name} 超过50MB限制`)
+          return
+        }
+        
+        // 检查是否已存在同名文件
+        const exists = this.selectedFiles.find(f => f.name === file.name)
+        if (exists) {
+          this.$message.warning(`文件 ${file.name} 已存在`)
+          return
+        }
+        
+        this.selectedFiles.push(file)
+      })
+      
+      // 清空input值,允许重复选择同一文件
+      event.target.value = ''
+    },
+
+    // 删除文件
+    removeFile(index) {
+      this.selectedFiles.splice(index, 1)
+    },
+
+    // 获取文件图标
+    getFileIcon(type) {
+      if (type.startsWith('image/')) {
+        return 'el-icon-picture'
+      } else if (type.includes('pdf')) {
+        return 'el-icon-document'
+      } else if (type.includes('word') || type.includes('document')) {
+        return 'el-icon-document'
+      } else if (type.includes('excel') || type.includes('spreadsheet')) {
+        return 'el-icon-document'
+      } else if (type.includes('video')) {
+        return 'el-icon-video-camera'
+      } else if (type.includes('audio')) {
+        return 'el-icon-headset'
+      } else {
+        return 'el-icon-document'
+      }
+    },
+
+    // 格式化文件大小
+    formatFileSize(bytes) {
+      if (bytes === 0) return '0 B'
+      const k = 1024
+      const sizes = ['B', 'KB', 'MB', 'GB']
+      const i = Math.floor(Math.log(bytes) / Math.log(k))
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+    },
+
+    // 提交给小程序
+    async submitToMiniProgram() {
+      if (this.selectedFiles.length === 0) {
+        this.$message.warning('请先选择文件')
+        return
+      }
+
+      this.submitting = true
+
+      try {
+        // 检查微信环境
+        if (typeof WeixinJSBridge === 'undefined') {
+          this.$message.error('请在微信中打开此页面')
+          return
+        }
+
+        // 准备文件数据
+        const filesData = await this.prepareFilesData()
+
+        // 通过微信JS-SDK发送消息给小程序
+        if (window.wx && window.wx.miniProgram) {
+          // 在微信小程序web-view中
+          window.wx.miniProgram.postMessage({
+            data: {
+              type: 'file_upload',
+              files: filesData
+            }
+          })
+          
+          this.$message.success('文件已提交给小程序')
+          this.selectedFiles = []
+        } else {
+          // 尝试通过WeixinJSBridge
+          WeixinJSBridge.invoke('sendAppMessage', {
+            title: '文件上传',
+            desc: `已选择 ${this.selectedFiles.length} 个文件`,
+            link: window.location.href,
+            imgUrl: '',
+            data: {
+              type: 'file_upload',
+              files: filesData
+            }
+          }, (res) => {
+            if (res.err_msg === 'send_app_msg:ok') {
+              this.$message.success('文件已提交给小程序')
+              this.selectedFiles = []
+            } else {
+              this.$message.error('提交失败,请重试')
+            }
+          })
+        }
+
+      } catch (error) {
+        console.error('提交文件失败:', error)
+        this.$message.error('提交失败,请重试')
+      } finally {
+        this.submitting = false
+      }
+    },
+
+    // 准备文件数据
+    async prepareFilesData() {
+      const filesData = []
+      
+      for (const file of this.selectedFiles) {
+        try {
+          const base64 = await this.fileToBase64(file)
+          filesData.push({
+            name: file.name,
+            type: file.type,
+            size: file.size,
+            data: base64
+          })
+        } catch (error) {
+          console.error('转换文件失败:', error)
+        }
+      }
+      
+      return filesData
+    },
+
+    // 文件转Base64
+    fileToBase64(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = () => resolve(reader.result)
+        reader.onerror = reject
+        reader.readAsDataURL(file)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.file-upload-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #fff;
+  min-height: 100vh;
+}
+
+.header {
+  text-align: center;
+  margin-bottom: 30px;
+}
+
+.header h2 {
+  color: #303133;
+  margin-bottom: 10px;
+}
+
+.header p {
+  color: #909399;
+  font-size: 14px;
+}
+
+.upload-area {
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  padding: 40px;
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.3s;
+  background: #fafafa;
+}
+
+.upload-area:hover {
+  border-color: #409eff;
+  background: #f0f9ff;
+}
+
+.upload-icon {
+  font-size: 48px;
+  color: #c0c4cc;
+  margin-bottom: 20px;
+}
+
+.upload-text p {
+  margin: 5px 0;
+  color: #606266;
+}
+
+.upload-hint {
+  font-size: 12px;
+  color: #c0c4cc;
+}
+
+.file-list {
+  margin-top: 30px;
+}
+
+.file-list h3 {
+  margin-bottom: 15px;
+  color: #303133;
+}
+
+.file-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 15px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  margin-bottom: 10px;
+  background: #fff;
+  transition: all 0.3s;
+}
+
+.file-item:hover {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.file-info {
+  display: flex;
+  align-items: center;
+  flex: 1;
+}
+
+.file-icon {
+  font-size: 24px;
+  color: #409eff;
+  margin-right: 15px;
+}
+
+.file-details {
+  flex: 1;
+}
+
+.file-name {
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 5px;
+  word-break: break-all;
+}
+
+.file-size {
+  font-size: 12px;
+  color: #909399;
+}
+
+.file-actions {
+  margin-left: 15px;
+}
+
+.submit-area {
+  margin-top: 30px;
+  text-align: center;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .file-upload-container {
+    padding: 15px;
+  }
+  
+  .upload-area {
+    padding: 30px 20px;
+  }
+  
+  .file-item {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+  
+  .file-actions {
+    margin-left: 0;
+    margin-top: 10px;
+    align-self: flex-end;
+  }
+}
+</style>

+ 385 - 0
src/views/file/test.vue

@@ -0,0 +1,385 @@
+<template>
+  <div class="test-container">
+    <h2>文件上传测试页面</h2>
+    <p>这个页面用于测试文件上传功能,可以在浏览器中直接访问</p>
+    
+    <div class="test-info">
+      <h3>测试说明:</h3>
+      <ul>
+        <li>此页面可以在任何浏览器中访问</li>
+        <li>文件选择功能会正常工作</li>
+        <li>提交功能会模拟发送数据</li>
+        <li>实际使用时需要在微信小程序中打开</li>
+      </ul>
+    </div>
+
+    <div class="upload-area" @click="triggerFileInput">
+      <div class="upload-icon">
+        <i class="el-icon-upload"></i>
+      </div>
+      <div class="upload-text">
+        <p>点击选择文件</p>
+        <p class="upload-hint">支持图片、文档、视频等格式</p>
+      </div>
+      <input
+        ref="fileInput"
+        type="file"
+        multiple
+        accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.mp4,.mp3"
+        @change="handleFileSelect"
+        style="display: none"
+      />
+    </div>
+
+    <div v-if="selectedFiles.length > 0" class="file-list">
+      <h3>已选择的文件</h3>
+      <div class="file-item" v-for="(file, index) in selectedFiles" :key="index">
+        <div class="file-info">
+          <div class="file-icon">
+            <i :class="getFileIcon(file.type)"></i>
+          </div>
+          <div class="file-details">
+            <div class="file-name">{{ file.name }}</div>
+            <div class="file-size">{{ formatFileSize(file.size) }}</div>
+          </div>
+        </div>
+        <div class="file-actions">
+          <el-button 
+            type="danger" 
+            size="mini" 
+            @click="removeFile(index)"
+            icon="el-icon-delete"
+          >
+            删除
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="submit-area">
+      <el-button 
+        type="primary" 
+        size="large" 
+        @click="testSubmit"
+        :disabled="selectedFiles.length === 0"
+        :loading="submitting"
+      >
+        {{ submitting ? '提交中...' : '测试提交' }}
+      </el-button>
+    </div>
+
+    <div v-if="testResult" class="test-result">
+      <h3>测试结果:</h3>
+      <pre>{{ testResult }}</pre>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FileUploadTest',
+  data() {
+    return {
+      selectedFiles: [],
+      submitting: false,
+      testResult: ''
+    }
+  },
+  methods: {
+    triggerFileInput() {
+      this.$refs.fileInput.click()
+    },
+
+    handleFileSelect(event) {
+      const files = Array.from(event.target.files)
+      
+      files.forEach(file => {
+        if (file.size > 50 * 1024 * 1024) {
+          this.$message.error(`文件 ${file.name} 超过50MB限制`)
+          return
+        }
+        
+        const exists = this.selectedFiles.find(f => f.name === file.name)
+        if (exists) {
+          this.$message.warning(`文件 ${file.name} 已存在`)
+          return
+        }
+        
+        this.selectedFiles.push(file)
+      })
+      
+      event.target.value = ''
+    },
+
+    removeFile(index) {
+      this.selectedFiles.splice(index, 1)
+    },
+
+    getFileIcon(type) {
+      if (type.startsWith('image/')) {
+        return 'el-icon-picture'
+      } else if (type.includes('pdf')) {
+        return 'el-icon-document'
+      } else if (type.includes('word') || type.includes('document')) {
+        return 'el-icon-document'
+      } else if (type.includes('excel') || type.includes('spreadsheet')) {
+        return 'el-icon-document'
+      } else if (type.includes('video')) {
+        return 'el-icon-video-camera'
+      } else if (type.includes('audio')) {
+        return 'el-icon-headset'
+      } else {
+        return 'el-icon-document'
+      }
+    },
+
+    formatFileSize(bytes) {
+      if (bytes === 0) return '0 B'
+      const k = 1024
+      const sizes = ['B', 'KB', 'MB', 'GB']
+      const i = Math.floor(Math.log(bytes) / Math.log(k))
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+    },
+
+    async testSubmit() {
+      if (this.selectedFiles.length === 0) {
+        this.$message.warning('请先选择文件')
+        return
+      }
+
+      this.submitting = true
+      this.testResult = ''
+
+      try {
+        const filesData = await this.prepareFilesData()
+        
+        // 模拟提交数据
+        const mockResult = {
+          success: true,
+          message: '测试提交成功',
+          timestamp: new Date().toISOString(),
+          filesCount: filesData.length,
+          files: filesData.map(file => ({
+            name: file.name,
+            type: file.type,
+            size: file.size,
+            dataLength: file.data.length
+          }))
+        }
+
+        this.testResult = JSON.stringify(mockResult, null, 2)
+        this.$message.success('测试提交成功')
+        
+        // 清空文件列表
+        this.selectedFiles = []
+
+      } catch (error) {
+        console.error('测试提交失败:', error)
+        this.$message.error('测试提交失败')
+        this.testResult = JSON.stringify({ error: error.message }, null, 2)
+      } finally {
+        this.submitting = false
+      }
+    },
+
+    async prepareFilesData() {
+      const filesData = []
+      
+      for (const file of this.selectedFiles) {
+        try {
+          const base64 = await this.fileToBase64(file)
+          filesData.push({
+            name: file.name,
+            type: file.type,
+            size: file.size,
+            data: base64
+          })
+        } catch (error) {
+          console.error('转换文件失败:', error)
+        }
+      }
+      
+      return filesData
+    },
+
+    fileToBase64(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = () => resolve(reader.result)
+        reader.onerror = reject
+        reader.readAsDataURL(file)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.test-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #fff;
+  min-height: 100vh;
+}
+
+.test-info {
+  background: #f0f9ff;
+  border: 1px solid #b3d8ff;
+  border-radius: 6px;
+  padding: 15px;
+  margin-bottom: 20px;
+}
+
+.test-info h3 {
+  margin-top: 0;
+  color: #409eff;
+}
+
+.test-info ul {
+  margin: 10px 0;
+  padding-left: 20px;
+}
+
+.test-info li {
+  margin: 5px 0;
+  color: #606266;
+}
+
+.upload-area {
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  padding: 40px;
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.3s;
+  background: #fafafa;
+  margin-bottom: 20px;
+}
+
+.upload-area:hover {
+  border-color: #409eff;
+  background: #f0f9ff;
+}
+
+.upload-icon {
+  font-size: 48px;
+  color: #c0c4cc;
+  margin-bottom: 20px;
+}
+
+.upload-text p {
+  margin: 5px 0;
+  color: #606266;
+}
+
+.upload-hint {
+  font-size: 12px;
+  color: #c0c4cc;
+}
+
+.file-list {
+  margin: 20px 0;
+}
+
+.file-list h3 {
+  margin-bottom: 15px;
+  color: #303133;
+}
+
+.file-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 15px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  margin-bottom: 10px;
+  background: #fff;
+  transition: all 0.3s;
+}
+
+.file-item:hover {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.file-info {
+  display: flex;
+  align-items: center;
+  flex: 1;
+}
+
+.file-icon {
+  font-size: 24px;
+  color: #409eff;
+  margin-right: 15px;
+}
+
+.file-details {
+  flex: 1;
+}
+
+.file-name {
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 5px;
+  word-break: break-all;
+}
+
+.file-size {
+  font-size: 12px;
+  color: #909399;
+}
+
+.file-actions {
+  margin-left: 15px;
+}
+
+.submit-area {
+  margin: 20px 0;
+  text-align: center;
+}
+
+.test-result {
+  margin-top: 20px;
+  background: #f5f5f5;
+  border-radius: 6px;
+  padding: 15px;
+}
+
+.test-result h3 {
+  margin-top: 0;
+  color: #303133;
+}
+
+.test-result pre {
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  padding: 10px;
+  overflow-x: auto;
+  font-size: 12px;
+  line-height: 1.4;
+}
+
+@media (max-width: 768px) {
+  .test-container {
+    padding: 15px;
+  }
+  
+  .upload-area {
+    padding: 30px 20px;
+  }
+  
+  .file-item {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+  
+  .file-actions {
+    margin-left: 0;
+    margin-top: 10px;
+    align-self: flex-end;
+  }
+}
+</style>