mp:素材管理,增加【图片】

This commit is contained in:
YunaiV 2023-01-14 15:46:35 +08:00
parent d1cbe0c3fd
commit 5a1d02dffd
11 changed files with 564 additions and 28 deletions

View File

@ -32,8 +32,10 @@ public interface ErrorCodeConstants {
ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1006003001, "更新用户标签失败,原因:{}");
// ========== 公众号素材 1006004000============
ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1006004000, "上传素材失败,原因:{}");
ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1006004000, "上传图片失败,原因:{}");
ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1006004000, "素材不存在");
ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1006004001, "上传素材失败,原因:{}");
ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1006004002, "上传图片失败,原因:{}");
ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1006004003, "删除素材失败,原因:{}");
// ========== 公众号消息 1006005000============
ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1006005000, "发送消息失败,原因:{}");

View File

@ -7,12 +7,11 @@ import cn.iocoder.yudao.module.mp.convert.material.MpMaterialConvert;
import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@ -20,8 +19,6 @@ import java.io.IOException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
// TODO @芋艿权限
@Api(tags = "管理后台 - 公众号素材")
@RestController
@RequestMapping("/mp/material")
@ -33,6 +30,7 @@ public class MpMaterialController {
@ApiOperation("上传临时素材")
@PostMapping("/upload-temporary")
@PreAuthorize("@ss.hasPermission('mp:material:upload-temporary')")
public CommonResult<MpMaterialUploadRespVO> uploadTemporaryMaterial(
@Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException {
MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO);
@ -41,14 +39,25 @@ public class MpMaterialController {
@ApiOperation("上传永久素材")
@PostMapping("/upload-permanent")
@PreAuthorize("@ss.hasPermission('mp:material:upload-permanent')")
public CommonResult<MpMaterialUploadRespVO> uploadPermanentMaterial(
@Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException {
MpMaterialDO material = mpMaterialService.uploadPermanentMaterial(reqVO);
return success(MpMaterialConvert.INSTANCE.convert(material));
}
@ApiOperation("删除素材")
@DeleteMapping("/delete-permanent")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('mp:material:delete')")
public CommonResult<Boolean> deleteMaterial(@RequestParam("id") Long id) {
mpMaterialService.deleteMaterial(id);
return success(true);
}
@ApiOperation("上传图文内容中的图片")
@PostMapping("/upload-news-image")
@PreAuthorize("@ss.hasPermission('mp:material:upload-news-image')")
public CommonResult<String> uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO)
throws IOException {
return success(mpMaterialService.uploadNewsImage(reqVO));
@ -56,10 +65,10 @@ public class MpMaterialController {
@ApiOperation("获得素材分页")
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('mp:material:query')")
public CommonResult<PageResult<MpMaterialRespVO>> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) {
PageResult<MpMaterialDO> pageResult = mpMaterialService.getMaterialPage(pageReqVO);
return success(MpMaterialConvert.INSTANCE.convertPage(pageResult));
}
}

View File

@ -22,7 +22,8 @@ public interface MpMaterialMapper extends BaseMapperX<MpMaterialDO> {
return selectPage(pageReqVO, new LambdaQueryWrapperX<MpMaterialDO>()
.eq(MpMaterialDO::getAccountId, pageReqVO.getAccountId())
.eqIfPresent(MpMaterialDO::getPermanent, pageReqVO.getPermanent())
.eqIfPresent(MpMaterialDO::getType, pageReqVO.getType()));
.eqIfPresent(MpMaterialDO::getType, pageReqVO.getType())
.orderByDesc(MpMaterialDO::getId));
}
default List<MpMaterialDO> selectListByMediaId(Collection<String> mediaIds) {

View File

@ -74,4 +74,11 @@ public interface MpMaterialService {
*/
List<MpMaterialDO> getMaterialListByMediaId(Collection<String> mediaIds);
/**
* 删除素材
*
* @param id 编号
*/
void deleteMaterial(Long id);
}

View File

@ -32,8 +32,7 @@ import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MATERIAL_IMAGE_UPLOAD_FAIL;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MATERIAL_UPLOAD_FAIL;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
/**
* 公众号素材 Service 接口
@ -172,6 +171,27 @@ public class MpMaterialServiceImpl implements MpMaterialService {
return mpMaterialMapper.selectListByMediaId(mediaIds);
}
@Override
public void deleteMaterial(Long id) {
MpMaterialDO material = mpMaterialMapper.selectById(id);
if (material == null) {
throw exception(MATERIAL_NOT_EXISTS);
}
// 第一步从公众号删除
if (material.getPermanent()) {
WxMpService mpService = mpServiceFactory.getRequiredMpService(material.getAppId());
try {
mpService.getMaterialService().materialDelete(material.getMediaId());
} catch (WxErrorException e) {
throw exception(MATERIAL_DELETE_FAIL, e.getError().getErrorMsg());
}
}
// 第二步从数据库中删除
mpMaterialMapper.deleteById(id);
}
/**
* 下载微信媒体文件的内容并上传到文件服务
*

View File

@ -8,3 +8,11 @@ export function getMaterialPage(query) {
params: query
})
}
// 删除公众号永久素材
export function deletePermanentMaterial(id) {
return request({
url: '/mp/material/delete-permanent?id=' + id,
method: 'delete'
})
}

View File

@ -19,9 +19,6 @@
</div>
</div>
<!-- 分页组件 -->
<div v-if="list.length <= 0 && !loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getMaterialPage"/>
</div>
@ -92,9 +89,6 @@
</div>
</div>
<!-- 分页组件 -->
<div v-if="list.length <= 0 && !loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getMaterialPage"/>
</div>

View File

@ -64,9 +64,6 @@ SOFTWARE.
</div>
</div>
<!-- 分页记录 -->
<div v-if="list.length <= 0 && !loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
@ -218,7 +215,6 @@ export default {
this.accounts = response.data;
//
if (this.accounts.length > 0) {
this.queryParams.accountId = this.accounts[0].id;
this.setAccountId(this.accounts[0].id);
}
//
@ -241,7 +237,7 @@ export default {
}
this.loading = true
getDraftPage((this.queryParams)).then(response => {
getDraftPage(this.queryParams).then(response => {
// thumbUrl picUrl wx-news
response.data.list.forEach(item => {
const newsItem = item.content.newsItem;

View File

@ -51,9 +51,6 @@ SOFTWARE.
</div>
</div>
<!-- 分页组件 -->
<div v-if="list.length <=0 && !loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
</div>

View File

@ -0,0 +1,502 @@
<!--
MIT License
Copyright (c) 2020 www.joolun.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<template>
<div class="app-container">
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="公众号" prop="accountId">
<el-select v-model="queryParams.accountId" placeholder="请选择公众号">
<el-option v-for="item in accounts" :key="parseInt(item.id)" :label="item.name" :value="parseInt(item.id)" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-tabs v-model="type" @tab-click="handleClick">
<!-- tab 1图片 -->
<el-tab-pane name="image">
<span slot="label"><i class="el-icon-picture"></i> 图片</span>
<div class="add_but" v-hasPermi="['mp:material:upload-permanent']">
<el-upload :action="actionUrl" :headers="headers" multiple :limit="1" :file-list="fileList" :data="uploadData"
:before-upload="beforeImageUpload" :on-success="handleUploadSuccess">
<el-button size="mini" type="primary">点击上传</el-button>
<sapn slot="tip" class="el-upload__tip" style="margin-left: 5px">支持 bmp/png/jpeg/jpg/gif 格式大小不超过 2M</sapn>
</el-upload>
</div>
<div class="waterfall" v-loading="loading">
<div class="waterfall-item" v-for="item in list" :key='item.id'>
<a target="_blank" :href="item.url">
<img class="material-img" :src="item.url">
<div class="item-name">{{item.name}}</div>
</a>
<el-row class="ope-row">
<el-button type="danger" icon="el-icon-delete" circle @click="handleDelete(item)"
v-hasPermi="['mp:material:delete']"/>
</el-row>
</div>
</div>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
</el-tab-pane>
<el-tab-pane name="voice">
<span slot="label"><i class="el-icon-microphone"></i> 语音</span>
<div class="add_but">
<el-upload
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:on-success="handleUploadSuccess"
:file-list="fileList"
:before-upload="beforeVoiceUpload"
:data="uploadData">
<el-button size="mini" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">
格式支持mp3/wma/wav/amr文件大小不超过2M播放长度不超过60s
</div>
</el-upload>
</div>
<el-table
:data="list"
stripe
border
v-loading="loading">
<el-table-column
prop="mediaId"
label="media_id">
</el-table-column>
<el-table-column
prop="name"
label="名称">
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间">
</el-table-column>
<el-table-column
fixed="right"
label="操作">
<template slot-scope="scope">
<el-button type="text"
icon="el-icon-download"
size="small"
plain
@click="handleDown(scope.row)">下载</el-button>
<el-button type="text"
icon="el-icon-delete"
size="small"
plain
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChange"
@current-change="currentChange"
:current-page.sync="queryParams.currentPage"
:page-sizes="[10, 20]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="queryParams.total"
class="pagination"
>
</el-pagination>
</el-tab-pane>
<el-tab-pane name="video">
<span slot="label"><i class="el-icon-video-play"></i> 视频</span>
<div class="add_but">
<el-button size="mini" type="primary" @click="handleAddVideo">新建</el-button>
</div>
<el-dialog title="新建视频" :visible.sync="dialogVideoVisible" v-loading="addMaterialLoading">
<el-upload
ref="uploadVideo"
:action="actionUrl"
:headers="headers"
multiple
:limit="1"
:on-success="handleUploadSuccess"
:file-list="fileList"
:before-upload="beforeVideoUpload"
:auto-upload="false"
:data="uploadData">
<el-button slot="trigger" size="mini" type="primary">选择视频</el-button>
<div class="el-upload__tip">
格式支持MP4文件大小不超过10MB
</div>
</el-upload>
<el-form :model="uploadData"
:rules="uploadRules"
ref="uploadForm">
<el-form-item label="标题" prop="title">
<el-input v-model="uploadData.title" placeholder="标题将展示在相关播放页面,建议填写清晰、准确、生动的标题"></el-input>
</el-form-item>
<el-form-item label="描述" prop="introduction">
<el-input :rows="3" type="textarea" v-model="uploadData.introduction" placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVideoVisible = false"> </el-button>
<el-button type="primary" @click="subVideo"> </el-button>
</div>
</el-dialog>
<el-table
:data="list"
stripe
border
v-loading="loading">
<el-table-column
prop="mediaId"
label="media_id">
</el-table-column>
<el-table-column
prop="name"
label="名称">
</el-table-column>
<el-table-column
prop="updateTime"
label="更新时间">
</el-table-column>
<el-table-column
fixed="right"
label="操作">
<template slot-scope="scope">
<el-button type="text"
icon="el-icon-view"
size="small"
plain
@click="handleInfo(scope.row)">查看</el-button>
<el-button type="text"
icon="el-icon-delete"
size="small"
plain
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChange"
@current-change="currentChange"
:current-page.sync="queryParams.currentPage"
:page-sizes="[10, 20]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="queryParams.total"
class="pagination"
>
</el-pagination>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import WxVoicePlayer from '@/views/mp/components/wx-voice-play/main.vue';
import WxVideoPlayer from '@/views/mp/components/wx-video-play/main.vue';
import { getSimpleAccounts } from "@/api/mp/account";
import { getMaterialPage, deletePermanentMaterial } from "@/api/mp/material";
import { getAccessToken } from '@/utils/auth'
export default {
name: 'mpMaterial',
components: {
WxVoicePlayer,
WxVideoPlayer
},
data() {
return {
type: 'image',
//
loading: false,
//
showSearch: true,
//
total: 0,
//
list: [],
//
queryParams: {
pageNo: 1,
pageSize: 10,
accountId: undefined,
},
actionUrl: process.env.VUE_APP_BASE_API + '/admin-api/mp/material/upload-permanent',
headers: { Authorization: "Bearer " + getAccessToken() }, //
fileList:[],
dialogVideoVisible:false,
dialogNewsVisible:false,
addMaterialLoading:false,
uploadData: {
"type": 'image',
"title":'',
"introduction":''
},
uploadRules:{
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
introduction: [
{ required: true, message: '请输入描述', trigger: 'blur' }
],
},
//
accounts: [],
}
},
created() {
getSimpleAccounts().then(response => {
this.accounts = response.data;
//
if (this.accounts.length > 0) {
this.setAccountId(this.accounts[0].id);
}
//
this.getList();
})
},
methods: {
// ======================== ========================
/** 设置账号编号 */
setAccountId(accountId) {
console.log('奥特曼!')
this.queryParams.accountId = accountId;
this.uploadData.accountId = accountId;
},
/** 查询列表 */
getList() {
//
if (!this.queryParams.accountId) {
this.$message.error('未选中公众号,无法查询草稿箱')
return false
}
this.loading = true
getMaterialPage({
...this.queryParams,
type: this.type
}).then(response => {
this.list = response.data.list
this.total = response.data.total
}).finally(() => {
this.loading = false
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNo = 1
//
if (this.queryParams.accountId) {
this.setAccountId(this.queryParams.accountId)
}
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
//
if (this.accounts.length > 0) {
this.setAccountId(this.accounts[0].id)
}
this.handleQuery()
},
handleClick(tab, event) {
// type
this.uploadData.type = tab.name
//
this.handleQuery();
},
// ======================== ========================
handleInfo(row){
this.loading = true
getMaterialVideo({
mediaId:row.mediaId
}).then((response) => {
this.loading = false
if(response.code == 200){
let downUrl = response.data.downUrl
window.open(downUrl, '_blank');
}else{
this.$message.error('获取微信视频素材出错:' + response.data.msg)
}
}).catch(() => {
this.loading = false
})
},
handleDown(row){
this.loading = true
getMaterialOther({
mediaId:row.mediaId,
fileName:row.name
}).then(response => {
this.loading = false
let url = window.URL.createObjectURL(new Blob([response.data]))
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', row.name)
document.body.appendChild(link)
link.click()
}).catch(() => {
this.loading = false
})
},
subVideo(){
this.$refs['uploadForm'].validate((valid) => {
if (valid) {
this.$refs.uploadVideo.submit()
} else {
return false
}
})
},
handleAddVideo(){
this.dialogVideoVisible = true
},
beforeImageUpload(file) {
const isType = file.type === 'image/jpeg'
|| file.type === 'image/png'
|| file.type === 'image/gif'
|| file.type === 'image/bmp'
|| file.type === 'image/jpg';
if (!isType) {
this.$message.error('上传图片格式不对!')
this.loading = false
return false;
}
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
this.$message.error('上传图片大小不能超过 2M!')
this.loading = false
return false;
}
this.loading = true
return true;
},
beforeVoiceUpload(file){
const isType = file.type === 'audio/mp3' || file.type === 'audio/wma' || file.type === 'audio/wav' || file.type === 'audio/amr';
const isLt = file.size / 1024 / 1024 < 2
if (!isType) {
this.$message.error('上传语音格式不对!')
}
if (!isLt) {
this.$message.error('上传语音大小不能超过2M!')
}
this.loading = false
return isType && isLt;
},
beforeVideoUpload(file){
this.addMaterialLoading = true
const isType = file.type === 'video/mp4'
const isLt = file.size / 1024 / 1024 < 10
if (!isType) {
this.$message.error('上传视频格式不对!')
}
if (!isLt) {
this.$message.error('上传视频大小不能超过10M!')
}
this.addMaterialLoading = false
return isType && isLt;
},
handleUploadSuccess(response, file, fileList) {
this.loading = false
this.addMaterialLoading = false
if (response.code !== 0) {
this.$message.error('上传出错:' + response.msg)
return false;
}
//
this.dialogVideoVisible = false
this.fileList = []
this.uploadData.title = ''
this.uploadData.introduction = ''
//
this.getList()
},
// ======================== ========================
handleDelete(item) {
const id = item.id
this.$modal.confirm('此操作将永久删除该文件, 是否继续?').then(function() {
return deletePermanentMaterial(id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
}
}
</script>
<style lang="scss" scoped>
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap:10px;
column-count: 5;
margin-top: 10px; /* 芋道源码:增加 10px避免顶着上面 */
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
.material-img {
width: 100%;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color:red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
</style>