This commit is contained in:
Vinjor 2025-10-15 15:30:32 +08:00
parent 71ec511819
commit 48aaeb8ac2
9 changed files with 492 additions and 88 deletions

View File

@ -21,10 +21,12 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDictDataService;
import com.ruoyi.utils.OssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
@ -75,6 +77,8 @@ public class WebController extends BaseController {
private IBaseAppService appService;
@Autowired
private GoogleConfig googleConfig;
@Autowired
private OssUtil ossUtil;
/**
* 导航栏接口--所有分类
@ -474,46 +478,17 @@ public class WebController extends BaseController {
return R.ok(appService.selectNewApp());
}
@SneakyThrows
@ApiOperation("下载APK文件")
@GetMapping("/downloadApk")
public void downloadApk(HttpServletResponse response) {
try {
// APK文件路径
File file = new File(googleConfig.getAppDownload());
// 检查文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().write("文件不存在");
return;
}
// 设置响应头
response.setContentType("application/vnd.android.package-archive");
response.setHeader("Content-Disposition", "attachment; filename=truck.apk");
response.setHeader("Content-Length", String.valueOf(file.length()));
// 写入响应流
FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
fis.close();
os.flush();
os.close();
} catch (Exception e) {
try {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("文件下载失败: " + e.getMessage());
} catch (Exception ex) {
logger.error("下载APK文件时发生错误", ex);
}
public void downloadApk(HttpServletResponse response,@RequestParam(required = false) String id) {
BaseApp appVersion;
if(StringUtils.isNotEmpty(id)){
appVersion = appService.getById(id);
}else{
appVersion = appService.selectNewApp();
}
ossUtil.downloadFile(response, appVersion.getApkUrl(), appVersion.getVersion());
}
/**

View File

@ -33,6 +33,10 @@ public class BaseApp extends DlBaseEntity
@Excel(name = "版本")
private String version;
/** app包下载地址 */
@Excel(name = "app包下载地址")
private String apkUrl;
/** 本次升级描述 */
@Excel(name = "本次升级描述")
private String content;

View File

@ -0,0 +1,82 @@
package com.ruoyi.utils;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyuncs.utils.IOUtils;
import com.ruoyi.common.config.OssConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@Slf4j
@Component
public class OssUtil {
@Autowired
private OssConfig ossConfig;
/**
* 上传文件(fileKey-InputStream)
*/
public void uploadFile(String fileKey, InputStream stream) throws IOException {
ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
OSS client = new OSSClientBuilder().build(ossConfig.getEndPoint(), ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret(), conf);
try {
client.putObject(ossConfig.getBucketName(), fileKey, stream);
} finally {
stream.close();
if (client != null) {
client.shutdown();
}
IOUtils.closeQuietly(stream);
}
}
/**
* 文件下载
*/
public void downloadFile(HttpServletResponse response, String fileKey, String fileName) throws IOException {
fileKey = fileKey.replace("https://"+ossConfig.getBucketName()+"."+ossConfig.getEndPoint()+"/","");
InputStream stream = null;
BufferedInputStream bufferedInputStream = null;
ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
OSS client = new OSSClientBuilder().build(ossConfig.getEndPoint(), ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret(), conf);
try {
stream = client.getObject(ossConfig.getBucketName(), fileKey).getObjectContent();
response.reset();
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachment;filename=CDbay-" + URLEncoder.encode(fileName, "UTF-8")
.replace("+", "%20")+".apk");
OutputStream outputStream = response.getOutputStream();
bufferedInputStream = new BufferedInputStream(stream);
byte[] buf = new byte[16384];
int bytesRead;
while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) {
outputStream.write(buf, 0, bytesRead);
outputStream.flush();
}
} finally {
if (client != null) {
client.shutdown();
}
if (bufferedInputStream != null || stream != null) {
try {
bufferedInputStream.close();
stream.close();
} catch (IOException e) {
log.error("Oss Download IOException,fileKey:" + fileKey, e);
}
}
}
}
}

View File

@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="BaseApp" id="BaseAppResult">
<result property="id" column="id" />
<result property="version" column="version" />
<result property="apkUrl" column="apk_url" />
<result property="content" column="content" />
<result property="creator" column="creator" />
<result property="createTime" column="create_time" />
@ -16,7 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectBaseAppVo">
select id, version, content, creator, create_time, updater, update_time, del_flag from dl_base_app
select id, version, apk_url, content, creator, create_time, updater, update_time, del_flag from dl_base_app
</sql>
<select id="queryListPage" parameterType="BaseApp" resultMap="BaseAppResult">
@ -24,6 +25,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<where>
<if test="entity.version != null "> and version like concat('%', #{entity.version}, '%')</if>
</where>
order by version DESC,create_time desc
order by create_time desc
</select>
</mapper>

View File

@ -1,15 +1,34 @@
<template>
<div class="content">
<!-- 压缩比例设置 -->
<div class="compression-setting">
<div class="compression-info">
图片压缩比例: {{ compressionRatio.toFixed(1) }}x
<el-tooltip class="item" effect="dark" content="值越小压缩率越高,图片质量越低"
placement="bottom"
>
<i class="el-icon-question"></i>
</el-tooltip>
</div>
<el-slider
v-model="compressionRatio"
:min="0.1"
:max="1.0"
:step="0.1"
:show-stops="true"
:tooltip-format="formatCompressionRatio"
></el-slider>
</div>
<vue-ueditor-wrap v-model="value"
:destroy="true"
:config="editorConfig"
:before-upload="handleBeforeUpload"
:editorDependencies="['ueditor.config.js','ueditor.all.js']"
/>
</div>
</template>
<script>
import VueUeditorWrap from 'vue-ueditor-wrap';
import VueUeditorWrap from 'vue-ueditor-wrap'
export default {
components: {
@ -19,12 +38,12 @@ export default {
/* 编辑器的内容 */
value: {
type: String,
default: "",
default: ''
}
},
watch: {
value(newVaue, oldValue) {
this.$emit('input', newVaue);
this.$emit('input', newVaue)
}
},
mounted() {
@ -39,8 +58,127 @@ export default {
//**231218**
UEDITOR_CORS_URL: '/UEditor/',
zIndex: 999,//z
//
imageMaxSize: 5 * 1024 * 1024, // 5MB
imageAllowFiles: ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
},
// (0.1-1.0)
compressionRatio: 0.9
}
},
methods: {
//
formatCompressionRatio(value) {
return `${value.toFixed(1)}x`
},
/**
* 上传前处理图片压缩
* @param {File} file 原始文件
* @param {Function} callback 回调函数
*/
handleBeforeUpload(file, callback) {
debugger
//
if (!file.type.match(/image\/\w+/)) {
return callback(file, true) //
}
// 100KB
if (file.size < 100 * 1024) {
return callback(file, true)
}
//
this.compressImage(file).then(compressedFile => {
callback(compressedFile, true)
}).catch(err => {
console.error('图片压缩失败:', err)
callback(file, true) //
})
},
/**
* 图片压缩处理
* @param {File} file 原始图片文件
* @returns {Promise<File>} 压缩后的文件
*/
compressImage(file) {
return new Promise((resolve, reject) => {
const img = new Image()
const reader = new FileReader()
reader.onload = (e) => {
img.src = e.target.result
}
img.onload = () => {
// canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
//
let width = img.width
let height = img.height
// canvas
canvas.width = width
canvas.height = height
// canvas
ctx.drawImage(img, 0, 0, width, height)
// canvasBlob
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('压缩失败'))
return
}
// File
const compressedFile = new File(
[blob],
file.name,
{
type: blob.type || file.type,
lastModified: Date.now()
}
)
resolve(compressedFile)
},
file.type || 'image/jpeg',
this.compressionRatio
)
}
img.onerror = (err) => {
reject(err)
}
reader.readAsDataURL(file)
})
}
}
}
</script>
<style scoped lang="scss">
//
.compression-setting {
width: 300px;
margin-bottom: 15px;
padding: 0 10px 8px 10px;
background-color: #f5f7fa;
border-radius: 4px;
.compression-info {
margin-top: 8px;
font-size: 12px;
color: #606266;
}
}
::v-deep .el-slider__runway {
margin: 0 !important;
}
</style>

View File

@ -1,5 +1,25 @@
<template>
<div class="component-upload-image">
<!-- 压缩比例设置 -->
<div class="compression-setting" v-if="showCompressionSetting">
<div class="compression-info">
压缩比例: {{ compressionRatio.toFixed(1) }}x
<el-tooltip class="item" effect="dark" content="值越小压缩率越高,图片质量越低"
placement="bottom"
>
<i class="el-icon-question"></i>
</el-tooltip>
</div>
<el-slider
v-model="compressionRatio"
:min="0.1"
:max="1.0"
:step="0.1"
:show-stops="true"
:tooltip-format="formatCompressionRatio"
></el-slider>
</div>
<el-upload
multiple
:action="uploadImgUrl"
@ -17,6 +37,7 @@
:on-preview="handlePictureCardPreview"
:disabled="disabled"
:class="{hide: this.fileList.length >= this.limit}"
:http-request="customUpload"
>
<i class="el-icon-plus"></i>
</el-upload>
@ -64,7 +85,7 @@ export default {
type: Boolean,
default: false,
},
// , ['png', 'jpg', 'jpeg']
// , ['png', 'jpeg', 'jpg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
@ -73,6 +94,16 @@ export default {
isShowTip: {
type: Boolean,
default: true
},
//
showCompression: {
type: Boolean,
default: true
},
//
defaultCompressionRatio: {
type: Number,
default: 0.9
}
},
data() {
@ -87,9 +118,21 @@ export default {
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
fileList: [],
// (0.1-1.0)
compressionRatio: this.defaultCompressionRatio
};
},
computed: {
//
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
//
showCompressionSetting() {
return this.showCompression && !this.disabled && this.fileList.length < this.limit;
}
},
watch: {
value: {
handler(val) {
@ -114,26 +157,32 @@ export default {
},
deep: true,
immediate: true
},
//
defaultCompressionRatio: {
handler(val) {
this.compressionRatio = val;
},
immediate: true
}
},
computed: {
//
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// loading
handleBeforeUpload(file) {
//
formatCompressionRatio(value) {
return `${value.toFixed(1)}x`;
},
//
async handleBeforeUpload(file) {
let isImg = false;
if (this.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1).toLowerCase();
}
isImg = this.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
if (fileExtension && fileExtension === type.toLowerCase()) return true;
return false;
});
} else {
@ -144,28 +193,151 @@ export default {
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
this.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.$modal.loading("正在上传图片,请稍候...");
//
this.$modal.loading("正在处理图片,请稍候...");
this.number++;
return true;
},
//
customUpload(options) {
const file = options.file;
//
this.compressImage(file, this.compressionRatio)
.then(compressedFile => {
// FormData
const formData = new FormData();
formData.append('file', compressedFile, file.name);
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('post', this.uploadImgUrl, true);
//
Object.keys(this.headers).forEach(key => {
xhr.setRequestHeader(key, this.headers[key]);
});
//
xhr.upload.addEventListener('progress', (e) => {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
options.onProgress(e);
});
//
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
options.onSuccess(response);
} else {
options.onError(xhr.responseText);
}
});
//
xhr.addEventListener('error', () => {
options.onError(xhr.responseText);
});
//
xhr.send(formData);
})
.catch(error => {
this.$modal.msgError('图片压缩失败: ' + error.message);
this.$modal.closeLoading();
this.number--;
options.onError(error);
});
},
//
compressImage(file, quality = 0.8) {
return new Promise((resolve, reject) => {
//
if (!file.type.match(/image.*/)) {
resolve(file);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
// canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// canvas
canvas.width = img.width;
canvas.height = img.height;
// canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
//
const mimeType = file.type || 'image/jpeg';
// canvasBlob
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('无法压缩图片'));
return;
}
// BlobFile
const compressedFile = new File([blob], file.name, {
type: mimeType,
lastModified: Date.now()
});
resolve(compressedFile);
}, mimeType, quality);
};
img.onerror = (error) => {
reject(new Error('无法加载图片进行压缩'));
};
//
img.src = e.target.result;
};
reader.onerror = (error) => {
reject(new Error('无法读取图片文件'));
};
//
reader.readAsDataURL(file);
});
},
//
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
//
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.name, url: res.fileName,size:res.size,width:res.width,height:res.height });
this.uploadList.push({
name: res.name,
url: res.fileName,
size: res.size,
width: res.width,
height: res.height
});
this.uploadedSuccessfully();
} else {
this.number--;
@ -175,6 +347,7 @@ export default {
this.uploadedSuccessfully();
}
},
//
handleDelete(file) {
const findex = this.fileList.map(f => f.name).indexOf(file.name);
@ -183,27 +356,31 @@ export default {
this.$emit("input", this.listToString(this.fileList));
}
},
//
handleUploadError() {
this.$modal.msgError("上传图片失败,请重试");
this.$modal.closeLoading();
},
//
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.$emit('uploadedImg',this.fileList)
this.$emit('uploadedImg', this.fileList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
//
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
//
listToString(list, separator) {
let strs = "";
@ -213,7 +390,7 @@ export default {
strs += list[i].url.replace(this.baseUrl, "") + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
return strs !== '' ? strs.substr(0, strs.length - 1) : '';
}
}
};
@ -223,6 +400,7 @@ export default {
::v-deep.hide .el-upload--picture-card {
display: none;
}
//
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {
@ -233,6 +411,21 @@ export default {
opacity: 0;
transform: translateY(0);
}
//
.compression-setting {
margin-bottom: 15px;
padding: 0 10px 8px 10px;
background-color: #f5f7fa;
border-radius: 4px;
.compression-info {
margin-top: 8px;
font-size: 12px;
color: #606266;
}
}
::v-deep .el-slider__runway{
margin:0 !important;
}
</style>

View File

@ -45,7 +45,7 @@
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['base:app:edit']"
>修改</el-button>
>下载</el-button>
<el-button
size="mini"
type="text"
@ -69,7 +69,10 @@
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="版本" prop="version">
<el-input v-model="form.version" placeholder="请输入版本" />
<el-input type="input" v-model="form.version" placeholder="请输入版本" />
</el-form-item>
<el-form-item label="app包" prop="apkUrl">
<file-upload v-model="form.apkUrl" :limit="1" :fileType="fileType" :fileSize="50"/>
</el-form-item>
<el-form-item label="本次升级描述" prop="content">
<el-input v-model="form.content" type="textarea" placeholder="请输入内容" />
@ -122,7 +125,11 @@ export default {
version: [
{ required: true, message: '请输入版本号', trigger: 'blur' }
],
}
apkUrl: [
{ required: true, message: '请上传程序包', trigger: 'blur' }
],
},
baseUrl: process.env.VUE_APP_BASE_API,
};
},
created() {
@ -148,6 +155,7 @@ export default {
this.form = {
id: null,
version: null,
apkUrl: null,
content: null,
creator: null,
createTime: null,
@ -179,15 +187,18 @@ export default {
this.open = true;
this.title = "添加app版本管理";
},
/** 修改按钮操作 */
/** 下载 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids
getApp(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改app版本管理";
});
let a = document.createElement("a"); // a
a.style.display = "none";
a.href = this.baseUrl + "/web/downloadApk?id="+row.id;
a.setAttribute(
"download",
a.href.split("/")[a.href.split("/").length - 1]
); // adownload
document.body.appendChild(a); //
a.click();
document.body.removeChild(a);
},
/** 提交按钮 */
submitForm() {

View File

@ -105,7 +105,7 @@
<el-form-item label="所属分类" prop="catgId">
<div class="dl-flex-column">
<treeselect style="width: 200px" v-model="form.catgId" :options="catgOptions" :normalizer="normalizer"
:noResultsText="'暂无数据'" placeholder="请选择新闻分类"
:noResultsText="'暂无数据'" placeholder="请选择新闻分类" :disable-branch-nodes="true"
/>
<div class="dl-add-catg" @click="goCatgView">添加新闻分类</div>
</div>
@ -184,7 +184,7 @@
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-col :span="9">
<el-form-item label="新闻图" prop="mainPic">
<el-tag v-if="!form.mainPic" style="cursor: pointer" @click="choosePic('mainPic',1)">图片库选择</el-tag>
<image-upload @uploadedImg="uploadedImg" v-model="form.mainPic" :limit="1"/>

View File

@ -102,7 +102,7 @@
<el-form-item label="所属分类" prop="catgId">
<div class="dl-flex-column">
<treeselect style="width: 200px" v-model="form.catgId" :options="catgOptions" :normalizer="normalizer"
:noResultsText="'暂无数据'" placeholder="请选择产品分类"
:noResultsText="'暂无数据'" placeholder="请选择产品分类" :disable-branch-nodes="true"
/>
<div class="dl-add-catg" @click="goCatgView">添加产品分类</div>
</div>