upload image api
This commit is contained in:
parent
2d70674f0f
commit
db0d6affdd
6
server/.gitignore
vendored
6
server/.gitignore
vendored
|
@ -1 +1,5 @@
|
|||
/node_modules
|
||||
/node_modules
|
||||
|
||||
upload-files/
|
||||
|
||||
upload-files-tmp/
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
14
server/util.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
BIN
upload-files-tmp/de7c4fce2e35df56020c06a00
Normal file
BIN
upload-files-tmp/de7c4fce2e35df56020c06a00
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
BIN
upload-files-tmp/f77ef960b5167f781ec77d800
Normal file
BIN
upload-files-tmp/f77ef960b5167f781ec77d800
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
Loading…
Reference in a new issue