upload image api

This commit is contained in:
KUN1007 2023-06-12 13:32:55 +08:00
parent 2d70674f0f
commit db0d6affdd
11 changed files with 224 additions and 188 deletions

6
server/.gitignore vendored
View file

@ -1 +1,5 @@
/node_modules
/node_modules
upload-files/
upload-files-tmp/

View file

@ -14,6 +14,7 @@
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"formidable": "^2.1.1",
"multer": "1.4.5-lts.1"
}
}

View file

@ -14,6 +14,9 @@ dependencies:
express:
specifier: ^4.18.2
version: 4.18.2
formidable:
specifier: ^2.1.1
version: 2.1.1
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
@ -36,6 +39,10 @@ packages:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
dev: false
/body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -163,6 +170,13 @@ packages:
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/dezalgo@1.0.4:
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
dependencies:
asap: 2.0.6
wrappy: 1.0.2
dev: false
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
@ -235,6 +249,15 @@ packages:
- supports-color
dev: false
/formidable@2.1.1:
resolution: {integrity: sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==}
dependencies:
dezalgo: 1.0.4
hexoid: 1.0.0
once: 1.4.0
qs: 6.11.0
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@ -275,6 +298,11 @@ packages:
function-bind: 1.1.1
dev: false
/hexoid@1.0.0:
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
engines: {node: '>=8'}
dev: false
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@ -391,6 +419,12 @@ packages:
ee-first: 1.1.1
dev: false
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@ -565,6 +599,10 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

View file

@ -1,47 +1,126 @@
// upload-image.js
const os = require('os')
const fs = require('fs')
const path = require('path')
const formidable = require('formidable')
const { objForEach } = require('../util')
const { PROTOCOL, PORT, IP } = {
PROTOCOL: 'http',
PORT: '10007',
IP: '127.0.0.1',
}
const FILE_FOLDER = 'upload-files'
const isWindows = os.type().toLowerCase().indexOf('windows') >= 0
const TMP_FOLDER = 'upload-files-tmp'
const express = require('express')
const router = express.Router()
const multer = require('multer')
const path = require('path')
const upload = multer({
dest: '../assets/upload/images', // 指定上传的目标文件夹
})
/**
* 获取随机数
*/
function getRandom() {
return Math.random().toString(36).slice(-3)
}
router.post('/img', upload.single('image'), (req, res) => {
if (!req.file) {
res
.status(400)
.json({ errno: 1, data: null, error: 'No image file provided' })
return
}
/**
* 给文件名加后缀 a.png 转换为 a-123123.png
* @param {string} fileName 文件名
*/
function genRandomFileName(fileName = '') {
// 如 fileName === 'a.123.png'
const r = getRandom()
if (!fileName) return r
// 从req.file对象中获取上传的文件信息
const fileName = req.file.filename
const filePath = req.file.path
const length = fileName.length // 9
const pointLastIndexOf = fileName.lastIndexOf('.') // 5
if (pointLastIndexOf < 0) return `${fileName}-${r}`
// 将上传的文件移动到指定的目标文件夹
const targetPath = path.join(__dirname, 'images', fileName)
fs.rename(filePath, targetPath, (err) => {
if (err) {
res
.status(500)
.json({ errno: 1, data: null, error: 'Failed to move uploaded file' })
return
const fileNameWithOutExt = fileName.slice(0, pointLastIndexOf) // "a.123"
const ext = fileName.slice(pointLastIndexOf + 1, length) // "png"
return `${fileNameWithOutExt}-${r}.${ext}`
}
/**
* 保存上传的文件
* @param {Object} req request
* @param {number} time time 用于测试超时
*/
function saveFiles(req, time = 0) {
return new Promise((resolve, reject) => {
const imgLinks = []
const form = formidable({ multiples: true })
// windows 系统,处理 rename 报错
if (isWindows) {
const tmpPath = path.resolve(__dirname, '..', '..', TMP_FOLDER) // 在根目录下
if (!fs.existsSync(tmpPath)) {
fs.mkdirSync(tmpPath)
}
form.uploadDir = TMP_FOLDER
}
const imageUrl = `/images/${fileName}`
const responseData = {
errno: 0,
data: {
url: imageUrl,
alt: 'Image description',
href: 'Image link',
},
}
form.parse(req, function (err, fields, files) {
if (err) {
reject('formidable, form.parse err', err.stack)
}
// 存储图片的文件夹
const storePath = path.resolve(__dirname, '..', '..', FILE_FOLDER)
if (!fs.existsSync(storePath)) {
fs.mkdirSync(storePath)
}
res.json(responseData)
console.log('fields......', fields)
// 遍历所有上传来的图片
objForEach(files, (name, file) => {
console.log('name...', name)
console.log('file.name...', file.name)
// 图片临时位置
const tempFilePath = file.path
// 图片名称和路径
const fileName = genRandomFileName(file.name || name) // 为文件名增加一个随机数,防止同名文件覆盖
console.log('fileName...', fileName)
const fullFileName = path.join(storePath, fileName)
console.log('fullFileName...', fullFileName)
// 将临时文件保存为正式文件
fs.renameSync(tempFilePath, fullFileName)
// 存储链接
const url = `${PROTOCOL}://${IP}:${PORT}/${FILE_FOLDER}/${fileName}`
imgLinks.push({ url, alt: fileName, href: url })
})
console.log('imgLinks...', imgLinks)
// 返回结果
let data
if (imgLinks.length === 1) {
data = imgLinks[0]
} else {
data = imgLinks
}
setTimeout(() => {
resolve({
errno: 0,
data: imgLinks,
})
}, time)
})
})
}
// 上传图片
router.post('/img', (req, res) => {
saveFiles(req)
.then((result) => {
res.json(result)
})
.catch((error) => {
console.error('Error:', error)
res.status(500).json({ error: 'Internal Server Error' })
})
})
module.exports = router

14
server/util.js Normal file
View file

@ -0,0 +1,14 @@
module.exports = {
// 遍历对象
objForEach: function (obj, fn) {
let key, result
for (key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result = fn.call(obj, key, obj[key])
if (result === false) {
break
}
}
}
},
}

View file

@ -1,13 +1,3 @@
<!-- 爷组件使用
const alertInfo: props = {
type: 'alert',
info: '确认发布吗',
isShowCancel: true,
status: getAlertValue,
}
provide('info', alertInfo)
-->
<script setup lang="ts">
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'

View file

@ -1,13 +1,3 @@
<!-- 爷组件使用
const alertInfo: props = {
type: 'alert',
info: '确认发布吗',
isShowCancel: true,
status: getAlertValue,
}
provide('info', alertInfo)
-->
<script setup lang="ts">
import { useKUNGalgameMessageStore } from '@/store/modules/message'
import { storeToRefs } from 'pinia'

View file

@ -1,109 +0,0 @@
<!-- 注意这个组件必须用 provide inject 的方式使用因为有子组件 -->
<script setup lang="ts">
import { ref } from 'vue'
import Alert from './Alert.vue'
import Info from './Info.vue'
defineProps(['type'])
const show = ref(false)
const updateStatus = (value: boolean) => {
show.value = value
}
</script>
<template>
<div class="alert" @click="show = true">
<slot></slot>
</div>
<Alert
:show="show"
:type="$props.type"
@update="updateStatus"
v-if="$props.type === 'alert'"
/>
<Info
:show="show"
:type="$props.type"
@update="updateStatus"
v-if="$props.type === 'info'"
/>
</template>
<style lang="scss" scoped>
.mask {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
transition: opacity 0.3s ease;
color: var(--kungalgame-font-color-3);
}
.container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: var(--kungalgame-trans-white-2);
border: 1px solid var(--kungalgame-blue-1);
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.button {
width: 70px;
height: 30px;
color: var(--kungalgame-font-color-3);
cursor: pointer;
&:nth-child(1) {
background-color: var(--kungalgame-trans-blue-1);
border: 1px solid var(--kungalgame-blue-4);
}
&:nth-child(2) {
margin-left: 98px;
background-color: var(--kungalgame-trans-red-1);
border: 1px solid var(--kungalgame-red-4);
}
&:active {
transform: scale(0.9);
transition: 0.1s;
}
}
/*
* 对于 transition="modal" 的元素来说
* 当通过 Vue.js 切换它们的可见性时
* 以下样式会被自动应用
*
* 你可以简单地通过编辑这些样式
* 来体验该模态框的过渡效果
*/
.alert-enter-from {
opacity: 0;
}
.alert-leave-to {
opacity: 0;
}
.alert-enter-from .container,
.alert-leave-to .container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View file

@ -2,44 +2,74 @@
编辑器实例共用组件
-->
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css' // css
import '@wangeditor/editor/dist/css/style.css'
import { IDomEditor } from '@wangeditor/editor'
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
const props = defineProps(['html', 'text'])
//
import '@/styles/editor/editor.scss'
const editorRef = shallowRef<IDomEditor | undefined>(undefined)
// shallowRef
const editorRef = shallowRef()
const valueHtml = ref('<p>hello</p>')
// HTML
const valueHtml = ref(props.html)
const valueText = ref(props.text)
// ajax
onMounted(() => {
// setTimeout(() => {
// valueHtml.value = '<p> Ajax </p>'
// }, 1500)
})
//
//
const editorConfig = {
placeholder: '请输入内容...',
readOnly: false,
MENU_CONF: {
uploadImage: {
// server: 'http://127.0.0.1:10007/upload/img',
// uploadFileName: 'image',
server: 'http://127.0.0.1:10007/upload/img',
// server: '/api/upload-img-10s', // test timeout
// server: '/api/upload-img-failed', // test failed
// server: '/api/xxx', // test 404
timeout: 5 * 1000, // 5s
fieldName: 'custom-fileName',
meta: { token: 'xxx', a: 100 },
metaWithUrl: true, // join params to url
headers: { Accept: 'text/x-json' },
maxFileSize: 10 * 1024 * 1024, // 10M
base64LimitSize: 5 * 1024, // insert base64 format, if file's size less than 5kb
// onBeforeUpload(file) {
// console.log('onBeforeUpload', file)
// return file // will upload this file
// // return false // prevent upload
// },
// onProgress(progress) {
// console.log('onProgress', progress)
// },
// onSuccess(file, res) {
// console.log('onSuccess', file, res)
// },
// onFailed(file, res) {
// alert(res.message)
// console.log('onFailed', file, res)
// },
// onError(file, err, res) {
// alert(err.message)
// console.error('onError', file, err, res)
// },
},
},
}
const handleCreated = (editor: any) => {
editorRef.value = editor // editor
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor
console.log(editor.getConfig())
}
//
onMounted(() => {
// ajax value
setTimeout(() => {
valueHtml.value = '<p>hello world</p>' // v-model
}, 2000)
})
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
@ -52,7 +82,6 @@ onBeforeUnmount(() => {
<div class="editor—wrapper">
<Toolbar class="toolbar-container" :editor="editorRef" />
<Editor
class="editor-container"
style="height: 400px"
v-model="valueHtml"
:defaultConfig="editorConfig"

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB