上传

root
10
2025-10-10
<template>
  <div class="article-form">
    <el-form :model="form" label-width="80px">
      <el-form-item label="文章标题" prop="title" required>
        <el-input v-model="form.title"></el-input>
      </el-form-item>
      
      <el-form-item label="文章内容" prop="content" required>
        <el-input type="textarea" v-model="form.content" rows="5"></el-input>
      </el-form-item>
      
      <el-form-item label="文章图片">
        <el-upload
          class="avatar-uploader"
          action="/api/upload"
          :show-file-list="false"
          :auto-upload="false"
          :on-change="handleChange"
          :on-success="handleSuccess"
          ref="upload"
        >
          <img v-if="form.imageUrl" :src="form.imageUrl" class="avatar">
          <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
        <el-button size="small" @click="submitUpload">添加图片</el-button>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="submitForm">保存文章</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        title: '',
        content: '',
        imageUrl: '',
        imageFile: null
      }
    }
  },
  methods: {
    handleChange(file) {
      const isImage = file.raw.type.includes('image/')
      const isLt2M = file.size / 1024 / 1024 < 2
      
      if (!isImage) {
        this.$message.error('只能上传图片文件!')
        return false
      }
      if (!isLt2M) {
        this.$message.error('图片大小不能超过2MB!')
        return false
      }
      
      const reader = new FileReader()
      reader.onload = (e) => {
        this.form.imageUrl = e.target.result
      }
      reader.readAsDataURL(file.raw)
      
      this.form.imageFile = file.raw
      return false
    },
    
    handleSuccess(response) {
      this.form.imageUrl = response.data.url
      this.$message.success('图片上传成功!')
      // 图片上传成功后自动提交表单
      this.submitFormData()
    },
    
    submitUpload() {
      if (!this.form.imageFile) {
        this.$message.warning('请先选择图片!')
        return
      }
      this.$refs.upload.submit()
    },
    
    submitForm() {
      if (!this.form.title || !this.form.content) {
        this.$message.error('标题和内容为必填项!')
        return
      }
      
      if (this.form.imageFile && !this.form.imageUrl.startsWith('http')) {
        this.submitUpload()
      } else {
        this.submitFormData()
      }
    },
    
    submitFormData() {
      this.$axios.post('/api/article', {
        title: this.form.title,
        content: this.form.content,
        image: this.form.imageUrl.startsWith('http') ? this.form.imageUrl : null
      }).then(() => {
        this.$message.success('文章保存成功!')
      }).catch(error => {
        this.$message.error('保存失败: ' + error.message)
      })
    }
  }
}
</script>

<style>
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

代码

生成


<template>
  <div class="article-form">
    <el-form :model="form" :rules="rules" ref="formRef">
      <!-- 标题字段 -->
      <el-form-item label="文章标题" prop="title">
        <el-input v-model="form.title"></el-input>
      </el-form-item>
      
      <!-- 内容字段 -->
      <el-form-item label="文章内容" prop="content">
        <el-input type="textarea" v-model="form.content" rows="5"></el-input>
      </el-form-item>
      
      <!-- 图片上传 -->
      <el-form-item label="封面图片">
        <el-upload
          class="avatar-uploader"
          action="/api/upload"
          :show-file-list="false"
          :auto-upload="false"
          :on-change="handleChange"
          :on-success="handleSuccess"
          ref="upload"
        >
          <img v-if="form.imageUrl" :src="form.imageUrl" class="avatar">
          <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="submitForm">保存文章</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        title: '',
        content: '',
        imageUrl: '',
        imageFile: null
      },
      rules: {
        title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
        content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
      }
    }
  },
  methods: {
    handleChange(file) {
      const isImage = file.raw.type.includes('image/');
      const isLt2M = file.size / 1024 / 1024 < 2;
      
      if (!isImage) {
        this.$message.error('只能上传图片文件!');
        return false;
      }
      if (!isLt2M) {
        this.$message.error('图片大小不能超过2MB!');
        return false;
      }
      
      // 本地预览
      const reader = new FileReader();
      reader.onload = (e) => {
        this.form.imageUrl = e.target.result;
      };
      reader.readAsDataURL(file.raw);
      
      this.form.imageFile = file.raw;
      return false;
    },
    
    handleSuccess(response) {
      if (response.code === 200) {
        this.form.imageUrl = response.data.url;
        this.submitArticleData();
      } else {
        this.$message.error('图片上传失败');
      }
    },
    
    submitForm() {
      this.$refs.formRef.validate(valid => {
        if (!valid) return;
        
        if (this.form.imageFile) {
          // 有图片需要先上传
          this.$refs.upload.submit();
        } else {
          // 无图片直接提交
          this.submitArticleData();
        }
      });
    },
    
    submitArticleData() {
      this.$axios.post('/api/article', {
        title: this.form.title,
        content: this.form.content,
        image: this.form.imageUrl.startsWith('http') ? this.form.imageUrl : null
      }).then(() => {
        this.$message.success('保存成功');
      }).catch(error => {
        this.$message.error('保存失败: ' + error.message);
      });
    }
  }
}
</script>

<style>
.avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
.avatar-uploader .el-upload:hover {
  border-color: #409EFF;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  line-height: 178px;
  text-align: center;
}
.avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>

暂存

<template>
    <div class="editor-container">
      <!-- 工具栏 -->
      <div style="border: 1px solid #ccc">
        <Toolbar
          style="border-bottom: 1px solid #ccc"
          :editor="editor"
          :defaultConfig="toolbarConfig"
          :mode="mode"
        />
      </div>
      
      <div style="height: 500px; overflow-y: hidden">
        <Editor
          v-model="html"
          :defaultConfig="editorConfig"
          :mode="mode"
          @onCreated="onCreated"
          @onChange="onChange"
        />
      </div>
    </div>
  </template>
  
  <script>
  import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
  import '@wangeditor/editor/dist/css/style.css'
  import axios from 'axios'
  
  export default {
    name: 'WangEditor',
    components: { Editor, Toolbar },
    props: {
      value: {
        type: String,
        default: ''
      }
    },
    data() {
      return {
        editor: null,
        html: this.value,
        mode: 'default',
        pendingImages: [], // 暂存待上传的图片
        toolbarConfig: {
          excludeKeys: [
            'uploadVideo',
          ]
        },
        editorConfig: {
          placeholder: '请输入内容...',
          MENU_CONF: {
            uploadImage: {
              // 自定义图片上传逻辑
              customUpload: async (file, insertFn) => {
                // 将图片暂存到pendingImages数组
                const imageId = `pending_${Date.now()}`
                const reader = new FileReader()
                
                reader.onload = (e) => {
                  // 使用base64格式显示图片预览
                  const base64Url = e.target.result
                  insertFn(base64Url, file.name, base64Url)
                  
                  // 保存原始文件对象和相关信息
                  this.pendingImages.push({
                    id: imageId,
                    file,
                    tempUrl: base64Url
                  })
                }
                
                reader.readAsDataURL(file)
              }
            }
          }
        }
      }
    },
    methods: {
      onCreated(editor) {
        this.editor = Object.seal(editor)
      },
      onChange(editor) {
        console.log('onChange', editor.getHtml())
      },
      // 保存并上传所有图片
      async saveAllImages() {
        if (this.pendingImages.length === 0) {
          this.$emit('save', this.html)
          return
        }
        
        try {
          const formData = new FormData()
          
          // 准备批量上传的数据
          this.pendingImages.forEach((img, index) => {
            formData.append(`files[${index}]`, img.file)
          })
          
          // 批量上传图片
          const response = await axios.post('/api/batch-upload', formData, {
            headers: {
              'Content-Type': 'multipart/form-data'
            }
          })
          
          // 替换编辑器中的临时URL
          const htmlContent = this.html
          let newHtml = htmlContent
          
          response.data.urls.forEach((urlObj, index) => {
            const pendingImg = this.pendingImages[index]
            if (pendingImg) {
              newHtml = newHtml.replace(
                new RegExp(pendingImg.tempUrl, 'g'),
                urlObj.url
              )
            }
          })
          
          // 更新编辑器内容
          this.html = newHtml
          this.pendingImages = []
          
          // 触发保存事件
          this.$emit('save', newHtml)
          
        } catch (error) {
          console.error('批量上传图片失败:', error)
          alert('图片上传失败,请重试')
        }
      },
      getHtml() {
        return this.html
      },
      getText() {
        return this.editor.getText()
      },
      destroyEditor() {
        this.editor.destroy()
        this.editor = null
      }
    },
    beforeDestroy() {
      if (this.editor) {
        this.editor.destroy()
      }
    }
  }
  </script>
  
<template>
  <div>
    <wang-editor v-model="content" @save="handleSave" />
  </div>
</template>

<script>
import WangEditor from './WangEditor.vue'

export default {
  components: { WangEditor },
  data() {
    return {
      content: '<p>初始内容</p>'
    }
  },
  methods: {
    handleSave(htmlContent) {
      console.log('保存的内容:', htmlContent)
      // 这里可以执行保存到服务器的操作
    }
  }
}
</script>

动物装饰