Skip to content

媒体库增加裁剪上传,扫码上传 #1994

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"axios": "^1.7.7",
"chokidar": "^4.0.0",
"core-js": "^3.38.1",
"default-passive-events": "^2.0.0",
"echarts": "5.5.1",
"element-plus": "^2.8.5",
"highlight.js": "^11.10.0",
Expand All @@ -43,7 +42,9 @@
"vform3-builds": "^3.0.10",
"vite-auto-import-svg": "^1.1.0",
"vue": "^3.5.7",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3",
"vue-qr": "^4.0.9",
"vue-router": "^4.4.3",
"vue3-ace-editor": "^2.2.4",
"vuedraggable": "^4.1.0"
Expand Down
21 changes: 11 additions & 10 deletions web/src/components/selectImage/selectImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/>
</div>

<el-drawer v-model="drawer" title="媒体库" :size="880">
<el-drawer v-model="drawer" title="媒体库 | 点击“文件名”可以编辑,选择的类别即是上传的类别" :size="880">
<div class="flex">
<div class="w-64" style="border-right: solid 1px var(--el-border-color);">
<el-scrollbar style="height: calc(100vh - 110px)">
Expand Down Expand Up @@ -39,14 +39,17 @@
</el-tree>
</el-scrollbar>
</div>
<div class="ml-4 image-library">
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
<div class="ml-4 w-[605px]">
<div class="gva-btn-list gap-2">
<el-button @click="useSelectedImages" type="danger" :disabled="selectedImages.length === 0" :icon="ArrowLeftBold">确认所选</el-button>
<el-input v-model.trim="search.keyword" class="w-96" placeholder="请输入文件名或备注" clearable />
<el-button type="primary" icon="search" @click="onSubmit"></el-button>
</div>
<div class="gva-btn-list gap-2">
<el-button @click="useSelectedImages" type="danger" :disabled="selectedImages.length === 0" :icon="ArrowLeftBold">选定</el-button>
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
<cropper-image :classId="search.classId" @on-success="onSuccess" />
<QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
<upload-image :image-url="imageUrl" :file-size="2048" :max-w-h="1080" :classId="search.classId" @on-success="onSuccess" />
<el-input v-model.trim="search.keyword" class="w-52" placeholder="请输入文件名或备注" clearable />
<el-button type="primary" icon="search" @click="onSubmit"> 查询</el-button>
</div>
<div class="flex flex-wrap gap-4">
<div v-for="(item,key) in picList" :key="key" class="w-40">
Expand Down Expand Up @@ -144,6 +147,8 @@ import {
} from '@element-plus/icons-vue'
import selectComponent from '@/components/selectImage/selectComponent.vue'
import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory'
import CropperImage from "@/components/upload/cropper.vue";
import QRCodeUpload from "@/components/upload/QR-code.vue";

const imageUrl = ref('')
const imageCommon = ref('')
Expand Down Expand Up @@ -425,10 +430,6 @@ const useSelectedImages = () => {
border: 3px solid #409eff;
}

.image-library {
width: 605px;
}

.selected:before {
content: "";
position: absolute;
Expand Down
65 changes: 65 additions & 0 deletions web/src/components/upload/QR-code.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div>
<el-button type="primary" icon="iphone" @click="createQrCode"> 扫码上传</el-button>
</div>

<el-dialog v-model="dialogVisible" title="扫码上传" width="320px" :show-close="false" append-to-body :close-on-click-modal="false"
draggable
>
<div class="m-2">
<vue-qr :logoSrc="logoSrc"
:size="291"
:margin="0"
:autoColor="true"
:dotScale="1"
:text="codeUrl"
colorDark="green"
colorLight="white"
ref="qrcode"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="onFinished">完成上传</el-button>
</div>
</template>
</el-dialog>
</template>

<script setup>
import logoSrc from '@/assets/logo.png'
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
import { ref } from 'vue'
import { useUserStore } from '@/pinia/modules/user'

defineOptions({
name: 'QRCodeUpload'
})

const emit = defineEmits(['on-success'])

const props = defineProps({
classId: {
type: Number,
default: 0
}
})

const dialogVisible = ref(false)
const userStore = useUserStore()
const codeUrl = ref('')

const createQrCode = () => {
const local = window.location
codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now()
dialogVisible.value = true
console.log(codeUrl.value)
}

const onFinished = () => {
dialogVisible.value = false
codeUrl.value = ''
emit('on-success', '')
}
</script>
235 changes: 235 additions & 0 deletions web/src/components/upload/cropper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<template>
<el-upload
ref="uploadRef"
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
accept="image/*"
:show-file-list="false"
:auto-upload="false"
:data="{'classId': props.classId}"
:on-success="handleImageSuccess"
:on-change="handleFileChange"
>
<el-button type="primary" icon="crop"> 裁剪上传</el-button>
</el-upload>

<el-dialog v-model="dialogVisible" title="图片裁剪" width="1200px" append-to-body @close="dialogVisible = false" :close-on-click-modal="false" draggable>
<div class="flex gap-[30px] h-[600px]">
<!-- 左侧编辑区 -->
<div class="flex flex-col flex-1">
<div class="flex-1 bg-[#f8f8f8] rounded-lg overflow-hidden">
<VueCropper
ref="cropperRef"
:img="imgSrc"
outputType="jpeg"
:autoCrop="true"
:autoCropWidth="cropWidth"
:autoCropHeight="cropHeight"
:fixedBox="false"
:fixed="fixedRatio"
:fixedNumber="fixedNumber"
:centerBox="true"
:canMoveBox="true"
:full="false"
:maxImgSize="1200"
:original="true"
@realTime="handleRealTime"
></VueCropper>
</div>

<!-- 工具栏 -->
<div class="mt-[20px] flex items-center p-[10px] bg-white rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
<el-button-group>
<el-tooltip content="向左旋转">
<el-button @click="rotate(-90)" :icon="RefreshLeft" />
</el-tooltip>
<el-tooltip content="向右旋转">
<el-button @click="rotate(90)" :icon="RefreshRight" />
</el-tooltip>
<el-button :icon="Plus" @click="changeScale(1)"></el-button>
<el-button :icon="Minus" @click="changeScale(-1)"></el-button>
</el-button-group>


<el-select v-model="currentRatio" placeholder="选择比例" class="w-32 ml-4" @change="onCurrentRatio">
<el-option v-for="(item, index) in ratioOptions" :key="index" :label="item.label" :value="index" />
</el-select>
</div>
</div>

<!-- 右侧预览区 -->
<div class="w-[340px]">
<div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
<div class="mb-[15px] text-gray-600">裁剪预览</div>
<div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]"
:style="{'width': previews.w + 'px', 'height': previews.h + 'px'}"
>
<div class="w-full h-full relative overflow-hidden">
<img :src="previews.url" :style="previews.img" alt="" class="max-w-none absolute transition-all duration-300 ease-in-out image-render-pixelated origin-[0_0]" />
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="handleUpload" :loading="uploading"> {{ uploading ? '上传中...' : '上 传' }}
</el-button>
</div>
</template>
</el-dialog>
</template>

<script setup>
import { ref, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue'
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import { getBaseUrl } from '@/utils/format'

defineOptions({
name: 'CropperImage'
})

const emit = defineEmits(['on-success'])

const props = defineProps({
classId: {
type: Number,
default: 0
}
})

const uploadRef = ref(null)
// 响应式数据
const dialogVisible = ref(false)
const imgSrc = ref('')
const cropperRef = ref(null)
const { proxy } = getCurrentInstance()
const previews = ref({})
const uploading = ref(false)

// 缩放控制
const changeScale = (value) => {
proxy.$refs.cropperRef.changeScale(value)
}

// 比例预设
const ratioOptions = ref([
{ label: '1:1', value: [1, 1] },
{ label: '16:9', value: [16, 9] },
{ label: '9:16', value: [9, 16] },
{ label: '4:3', value: [4, 3] },
{ label: '自由比例', value: [] }
])

const fixedNumber = ref([1, 1])
const cropWidth = ref(300)
const cropHeight = ref(300)

const fixedRatio = ref(false)
const currentRatio = ref(4)
const onCurrentRatio = () => {
fixedNumber.value = ratioOptions.value[currentRatio.value].value
switch (currentRatio.value) {
case 0:
cropWidth.value = 300
cropHeight.value = 300
fixedRatio.value = true
break
case 1:
cropWidth.value = 300
cropHeight.value = 300 * 9 / 16
fixedRatio.value = true
break
case 2:
cropWidth.value = 300 * 9 / 16
cropHeight.value = 300
fixedRatio.value = true
break
case 3:
cropWidth.value = 300
cropHeight.value = 300 * 3 / 4
fixedRatio.value = true
break
default:
cropWidth.value = 300
cropHeight.value = 300
fixedRatio.value = false
}
}

// 文件处理
const handleFileChange = (file) => {
const isImage = file.raw.type.includes('image')
if (!isImage) {
ElMessage.error('请选择图片文件')
return
}

if (file.raw.size / 1024 / 1024 > 8) {
ElMessage.error('文件大小不能超过8MB!')
return false
}

const reader = new FileReader()
reader.onload = (e) => {
imgSrc.value = e.target.result
dialogVisible.value = true
}
reader.readAsDataURL(file.raw)
}

// 旋转控制
const rotate = (degree) => {
if (degree === -90) {
proxy.$refs.cropperRef.rotateLeft()
} else {
proxy.$refs.cropperRef.rotateRight()
}
}

// 实时预览
const handleRealTime = (data) => {
previews.value = data
//console.log(data)
}

// 上传处理
const handleUpload = () => {
uploading.value = true
proxy.$refs.cropperRef.getCropBlob((blob) => {
try {
const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' })
uploadRef.value.clearFiles()
uploadRef.value.handleStart(file)
uploadRef.value.submit()

} catch (error) {
uploading.value = false
ElMessage.error('上传失败: ' + error.message)
}
})
}

const handleImageSuccess = (res) => {
const { data } = res
if (data) {
setTimeout(() => {
uploading.value = false
dialogVisible.value = false
previews.value = {}
ElMessage.success('上传成功')
emit('on-success', data.url)
}, 1000)
}
}

</script>

<style scoped>
:deep(.vue-cropper) {
background: transparent;
}
</style>
2 changes: 0 additions & 2 deletions web/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import run from '@/core/gin-vue-admin.js'
import auth from '@/directive/auth'
import { store } from '@/pinia'
import App from './App.vue'
// 消除警告
import 'default-passive-events'

const app = createApp(App)
app.config.productionTip = false
Expand Down
2 changes: 1 addition & 1 deletion web/src/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Nprogress.configure({
})

// 白名单路由
const WHITE_LIST = ['Login', 'Init']
const WHITE_LIST = ['Login', 'Init', 'ScanUpload']

// 处理路由加载
const setupRouter = async (userStore) => {
Expand Down
Loading