Ver código fonte

新增随访导出excel, 患者随访可以上传图片,视频,语音

lsw 3 meses atrás
pai
commit
2cd9d98a14

+ 15 - 0
admin-ui/src/assets/styles/extend.scss

@@ -450,6 +450,21 @@
     }
   }
 }
+.imgList{
+  overflow: hidden;
+  .img{
+    width:60px;
+    height: 60px;
+    border-radius: 5px;
+    float: left;
+    margin-right: 5px;
+    background-color: aliceblue;
+  }
+  video{
+    width: 200px;
+    height: 120px;
+  }
+}
 /* 定义滚动条样式 */
 ::-webkit-scrollbar {
 	width: 6px;

+ 15 - 1
admin-ui/src/views/work/follow/record/detail.vue

@@ -11,11 +11,20 @@
         </div>
         <div class="mts">
           <input v-if="item.input == '填空'" v-model="item.s_value" :disabled="true" />
+          <div class="imgList" v-if="item.input == '图片'">
+            <el-image :fit="'contain'" class="img" :z-index="50000" :src="baseUrl + item" :preview-src-list="[baseUrl + item]" v-for="(item, index) in item.s_value" :key="index"></el-image>
+          </div>
+          <div class="imgList" v-if="item.input == '视频'">
+            <video :src="baseUrl + item" v-for="(item, index) in item.s_value" :key="index" controls></video>
+          </div>
+          <div class="imgList" v-if="item.input == '录音'">
+            <audio :src="baseUrl + item" v-for="(item, index) in item.s_value" :key="index" controls></audio>
+          </div>
           <textarea v-if="item.input == '多行文本'" :disabled="true" v-model="item.s_value"></textarea>
           <input type="number" v-if="item.input == '数字'" v-model="item.s_value" :disabled="true" />
           <div class="ops">
             <div v-for="(op, i) in item.selects" :key="op.name">
-              <div class="op" :class="{active:op.check}">{{ op.name }}</div>
+              <div class="op" :class="{ active: op.check }">{{ op.name }}</div>
             </div>
           </div>
         </div>
@@ -40,6 +49,11 @@ export default {
       this.ajax({ url: '/work/record/detail/' + this.param.id }).then((response) => {
         this.form = response.data;
         this.op = JSON.parse(response.data.op);
+        this.op.forEach((item) => {
+          if (item.input == '图片' || item.input == '视频' || item.input == '录音') {
+            item.s_value = item.s_value ? item.s_value.split(',') : [];
+          }
+        });
       });
     }
   },

+ 6 - 0
admin-ui/src/views/work/follow/record/edit.vue

@@ -40,6 +40,12 @@
                     <span class="tm">{{ item.name }} ({{ item.input }})</span>
                   </div>
                   <div class="mts">
+                    <el-button style="width: 100%" v-if="item.input == '图片' || item.input == '视频' || item.input == '录音'">
+                      <i class="el-icon-picture" v-if="item.input == '图片'"></i>
+                      <i class="el-icon-video-camera-solid" v-if="item.input == '视频'"></i>
+                      <i class="el-icon-microphone" v-if="item.input == '录音'"></i>
+                      <span>{{ item.input }}</span>
+                    </el-button>
                     <input v-if="item.input == '填空'" placeholder="请输入" :disabled="true" />
                     <textarea v-if="item.input == '多行文本'" :disabled="true" placeholder="请输入"></textarea>
                     <input type="number" v-if="item.input == '数字'" placeholder="请输入" :disabled="true" />

+ 6 - 1
admin-ui/src/views/work/follow/record/index.vue

@@ -14,6 +14,7 @@
     </el-form>
     <el-row :gutter="10" class="mb8">
       <el-button type="primary" icon="el-icon-plus" :disabled="ids.length > 0" @click="op('add')" v-hasPermi="['work:record:add', 'work:up:add']">{{ queryParams.type == 0 ? '新增提醒' : '新增回访' }}</el-button>
+      <el-button type="primary" icon="el-icon-download" @click="handleExport" v-hasPermi="['work:record:export', 'work:up:add']">导出</el-button>
       <el-button type="danger" icon="el-icon-delete" :disabled="ids.length == 0" @click="del" v-hasPermi="['work:record:remove', 'work:up:remove']">删除{{ ids.length > 0 ? '(' + ids.length + ')' : '' }}</el-button>
     </el-row>
 
@@ -97,7 +98,7 @@ export default {
         this.iframe({ obj: edit, param: { id: row.id, type: this.queryParams.type, patientName: row.patientName }, title: '编辑', width: '57%', height: '85%' });
       }
       if (tag == 'detail') {
-        this.iframe({ obj: detail, param: { id: row.id, detail: true, patientName: row.patientName }, title: '查看详情', width: '45%', height: '70%' });
+        this.iframe({ obj: detail, param: { id: row.id, detail: true, patientName: row.patientName }, title: '查看详情', width: '35%', height: '85%' });
       }
     },
     del(row) {
@@ -107,6 +108,10 @@ export default {
           this.getList();
         });
       });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.download('/work/record/export', { ...this.queryParams }, '随访信息.xlsx');
     }
   }
 };

+ 7 - 1
admin-ui/src/views/work/follow/template/edit.vue

@@ -60,6 +60,12 @@
                   <span class="tm">{{ item.name }} ({{ item.input }})</span>
                 </div>
                 <div class="mts">
+                  <el-button style="width: 100%" v-if="item.input == '图片' || item.input == '视频' || item.input == '录音'">
+                    <i class="el-icon-picture" v-if="item.input == '图片'"></i>
+                    <i class="el-icon-video-camera-solid" v-if="item.input == '视频'"></i>
+                    <i class="el-icon-microphone" v-if="item.input == '录音'"></i>
+                    <span>{{ item.input }}</span>
+                  </el-button>
                   <input v-if="item.input == '填空'" placeholder="请输入" :disabled="true" />
                   <textarea v-if="item.input == '多行文本'" :disabled="true" placeholder="请输入"></textarea>
                   <input type="number" v-if="item.input == '数字'" placeholder="请输入" :disabled="true" />
@@ -91,7 +97,7 @@ export default {
     return {
       form: { state: 0, op: [] },
       nulls: ['必填', '非必填'],
-      selects: ['填空', '单选', '多选', '判断', '数字', '多行文本'],
+      selects: ['填空', '单选', '多选', '判断', '图片', '视频', '录音', '数字', '多行文本'],
       rules: {
         title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
         op: [{ required: true, message: '内容不能为空', trigger: 'blur' }],

+ 1 - 1
app/App.vue

@@ -31,7 +31,7 @@ button::after {
 /**挂载iconfont字体图标*/
 @font-face {
 	font-family: 'iconfont';
-	src: url('https://at.alicdn.com/t/c/font_4620946_c5ea6r77n3b.ttf?t=1724749291287') format('truetype');
+	src: url('https://at.alicdn.com/t/c/font_4620946_uet099em3e.ttf?t=1740057793342') format('truetype');
 	/* src: url('~@/static/font/iconfont.ttf') format('truetype'); */
 }
 .icon {

+ 284 - 0
app/components/images/images.vue

@@ -0,0 +1,284 @@
+<template>
+	<view class="imgs">
+		<view v-if="type === '图片' || type === '视频'">
+			<view class="photo" v-for="(item, index) in value" :key="index" :style="{ width: size, height: size }">
+				<image :src="ip + item" mode="aspectFill" :style="{ width: size, height: size }" v-if="type === '图片'" @click.stop="preview(item)"></image>
+				<video :src="ip + item" v-if="type === '视频'" controls class="video" :style="{ width: size, height: size }"></video>
+				<text class="icon del" @click.stop="del(item)" v-if="!read">&#xe8b6;</text>
+			</view>
+		</view>
+		<audio :src="ip + value[0]" v-if="type === '录音' && read" name="录音文件" controls></audio>
+		<view class="uploads" v-if="type != '录音' && value.length < 3 && !read" @click.stop="chooseImage()">
+			<view class="bw" v-if="type == '图片'">
+				<view class="icon">&#xe696;</view>
+				<view class="text">上传图片</view>
+			</view>
+			<view class="bw" v-if="type == '视频'">
+				<view class="icon">&#xe622;</view>
+				<view class="text">上传视频</view>
+			</view>
+		</view>
+		<view v-if="type == '录音' && !read" class="audio">
+			<button @click="startRecord()" :disabled="stop">
+				<text class="icon">&#xe618;</text>
+				<text>开始</text>
+			</button>
+			<button @click="endRecord()" :disabled="!stop">
+				<text class="icon">&#xe611;</text>
+				<text>停止</text>
+			</button>
+			<button @click="playVoice()" :disabled="stop">
+				<text class="icon">&#xe628;</text>
+				<text>播放</text>
+			</button>
+			<button @click="again()">
+				<text class="icon">&#xe607;</text>
+				<text>重新</text>
+			</button>
+		</view>
+	</view>
+</template>
+
+<script>
+const recorderManager = uni.getRecorderManager();
+const innerAudioContext = uni.createInnerAudioContext();
+export default {
+	name: 'images',
+	props: {
+		value: {
+			type: Array
+		},
+		read: {
+			type: Boolean,
+			default: false
+		},
+		type: {
+			type: String,
+			default: 'img'
+		},
+		size: {
+			type: String,
+			default: '80px'
+		}
+	},
+	data() {
+		return {
+			stop: false,
+			ip: this.http.ip,
+			voicePath: ''
+		};
+	},
+	mounted() {
+		if (this.type === '录音') {
+			recorderManager.onStop((res) => {
+				this.voicePath = res.tempFilePath;
+				uni.uploadFile({
+					url: this.ip + '/app/common/upload',
+					filePath: res.tempFilePath,
+					name: 'file',
+					header: { Authorization: this.getUser().token },
+					success: (res) => {
+						let data = JSON.parse(res.data);
+						if (data.code === 200) {
+							this.value.push(data.fileName);
+							this.$emit('input', this.value);
+							this.$forceUpdate();
+						} else {
+							uni.showModal({ content: data.msg, showCancel: false });
+						}
+						uni.hideLoading();
+					},
+					fail: (res) => {
+						uni.hideLoading();
+						uni.showModal({ content: '上传失败', showCancel: false });
+					}
+				});
+			});
+		}
+	},
+	methods: {
+		chooseImage() {
+			//照片选择
+			if (this.type === '图片') {
+				uni.chooseImage({
+					count: 3, //默认9
+					sizeType: ['compressed'], //可以指定是原图还是压缩图,默认二者都有
+					success: (res) => {
+						res.tempFilePaths.forEach((path) => {
+							uni.showLoading({ title: '正在上传图片', mask: true });
+							uni.uploadFile({
+								url: this.ip + '/app/common/upload',
+								filePath: path,
+								name: 'file',
+								header: { Authorization: this.getUser().token },
+								success: (res) => {
+									let data = JSON.parse(res.data);
+									if (data.code === 200) {
+										if (this.value.length < 3) {
+											this.value.push(data.fileName);
+											this.$emit('input', this.value);
+											this.$forceUpdate();
+										} else {
+											uni.showModal({ content: '最多只能上传5个附件', showCancel: false });
+										}
+									} else {
+										uni.showModal({ content: data.msg, showCancel: false });
+									}
+									uni.hideLoading();
+								},
+								fail: (res) => {
+									uni.hideLoading();
+									uni.showModal({ content: '图片上传失败', showCancel: false });
+								}
+							});
+						});
+					}
+				});
+			} else if (this.type === '视频') {
+				uni.chooseVideo({
+					count: 3,
+					sourceType: ['camera', 'album'],
+					maxDuration: 30,
+					success: (path) => {
+						uni.showLoading({ title: '正在上传...', mask: true });
+						uni.uploadFile({
+							url: this.ip + '/app/common/upload',
+							filePath: path.tempFilePath,
+							name: 'file',
+							header: { Authorization: this.getUser().token },
+							success: (res) => {
+								uni.hideLoading();
+								let data = JSON.parse(res.data);
+								if (data.code === 200) {
+									if (this.value.length < 3) {
+										this.value.push(data.fileName);
+										this.$emit('input', this.value);
+										this.$forceUpdate();
+									} else {
+										uni.showModal({ content: '最多只能上传5个附件', showCancel: false });
+									}
+								} else {
+									uni.showModal({ content: data.msg, showCancel: false });
+								}
+								uni.hideLoading();
+							},
+							fail: (res) => {
+								uni.hideLoading();
+								uni.showModal({ content: '图片上传失败', showCancel: false });
+							}
+						});
+					}
+				});
+			} else if (this.type === '录音') {
+			}
+		},
+		startRecord() {
+			console.log('开始录音');
+			this.stop = true;
+			recorderManager.start();
+		},
+		endRecord() {
+			console.log('录音结束');
+			this.stop = false;
+			recorderManager.stop();
+		},
+		again() {
+			this.voicePath = '';
+			this.startRecord();
+		},
+		playVoice() {
+			console.log('播放录音');
+			if (this.voicePath) {
+				innerAudioContext.src = this.voicePath;
+				innerAudioContext.play();
+			}
+		},
+		preview(item) {
+			let urls = [];
+			this.value.forEach((item) => {
+				urls.push(this.ip + item);
+			});
+			// 预览图片
+			uni.previewImage({
+				urls: urls,
+				current: this.ip + item,
+				success: (res) => {}
+			});
+		},
+		del(item) {
+			this.value.splice(this.value.indexOf(item), 1);
+			this.$emit('input', this.value);
+			this.$forceUpdate();
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.imgs {
+	overflow: hidden;
+	.uploads {
+		float: left;
+		width: 80px;
+		height: 80px;
+		text-align: center;
+		border-radius: 10px;
+		margin: 5px 5px 5px 5px;
+		overflow: hidden;
+		background-color: #f8f8f8;
+		.bw {
+			padding-top: 10px;
+			.icon {
+				font-size: 30px;
+				display: block;
+				float: none;
+			}
+			.text {
+				font-size: 13px;
+				color: #444444;
+				padding-top: 5px;
+			}
+		}
+	}
+	.photo {
+		float: left;
+		margin: 5px 5px 5px 5px;
+		position: relative;
+		overflow: hidden;
+		border-radius: 8px;
+
+		image {
+			border-radius: 5px;
+		}
+		.video {
+			border-radius: 5px;
+		}
+		.audio {
+		}
+		.del {
+			position: absolute;
+			top: 0px;
+			right: 0px;
+			background-color: #00000078;
+			color: white;
+			padding: 3px 8px;
+			font-size: 12px;
+			border-radius: 0px 0px 0px 10px;
+		}
+	}
+	.audio {
+		background-color: #eeeeee;
+		border-radius: 20px;
+		overflow: hidden;
+		padding: 5px;
+		button {
+			float: left;
+			background-color: #eeeeee;
+			font-size: 14px;
+			.icon {
+				padding-right: 3px;
+			}
+		}
+	}
+}
+</style>

+ 4 - 1
app/pages/follow/detail.vue

@@ -15,6 +15,9 @@
 			</view>
 			<view class="mts">
 				<input v-if="item.input == '填空'" v-model="item.s_value" placeholder="请输入" :disabled="look" />
+				<images v-model="item.s_value" type="图片" v-if="item.input == '图片'" :read="look"></images>
+				<images v-model="item.s_value" type="视频" v-if="item.input == '视频'" :read="look"></images>
+				<images v-model="item.s_value" type="录音" v-if="item.input == '录音'" :read="look"></images>
 				<input v-if="item.input == '数字'" type="number" v-model="item.s_value" placeholder="请输入" :disabled="look" />
 				<textarea v-if="item.input == '多行文本'" v-model="item.s_value" placeholder="请输入" :disabled="look" />
 				<view class="ops" v-if="item.input == '单选' || item.input == '多选' || item.input == '判断'">
@@ -33,7 +36,7 @@ export default {
 	data() {
 		return {
 			look: false,
-			item: {}
+			item: { op: { s_value: [] } }
 		};
 	},
 	onLoad(e) {

+ 1 - 0
app/pages/index/index.vue

@@ -260,6 +260,7 @@ page {
 				width: 80px;
 				height: 105px;
 				border-radius: 5px;
+				background-color: aliceblue;
 			}
 			.title {
 				padding-top: 3px;

+ 1 - 1
app/pages/user/index.vue

@@ -9,7 +9,7 @@
 				</view>
 				<view v-else>
 					<view class="nickName">
-						<text v-if="user.bindUserList.length > 0">{{ user.patientName ? user.patientName : '未设置当前就诊人' }}</text>
+						<text v-if="user.bindUserList&&user.bindUserList.length > 0">{{ user.patientName ? user.patientName : '未设置当前就诊人' }}</text>
 						<text v-else>还未绑定就诊人</text>
 						<text v-if="user.relationship" class="relationship">({{ user.relationship }})</text>
 						<text class="icon" v-if="user.bindUserList && user.bindUserList.length > 0" @click.stop="show = true">&#xe6a7;切换就诊人</text>

+ 18 - 4
ruoyi-admin/src/main/java/com/ruoyi/web/work/api/Api_CommonController.java

@@ -1,11 +1,13 @@
 package com.ruoyi.web.work.api;
 
 import com.google.code.kaptcha.Producer;
+import com.ruoyi.common.config.RuoYiConfig;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.entity.SysDictData;
 import com.ruoyi.common.core.redis.RedisCache;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
 import com.ruoyi.common.utils.sign.Base64;
 import com.ruoyi.common.utils.uuid.IdUtils;
 import com.ruoyi.system.service.ISysDictTypeService;
@@ -13,10 +15,8 @@ import com.ruoyi.web.work.api.config.BaseController;
 import com.ruoyi.web.work.domain.Introduction;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.FastByteArrayOutputStream;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
 
 import javax.annotation.Resource;
 import javax.imageio.ImageIO;
@@ -88,5 +88,19 @@ public class Api_CommonController extends BaseController {
         ajax.put("img", "data:image/gif;base64," + Base64.encode(os.toByteArray()));
         return AjaxResult.success(ajax);
     }
+
+    @PostMapping("/upload")
+    public AjaxResult upload(MultipartHttpServletRequest multiReq) {
+        try {
+            AjaxResult ajax = AjaxResult.success();
+            // 上传文件路径
+            String filePath = RuoYiConfig.getUploadPath();
+            String fileName = FileUploadUtils.upload(filePath, multiReq.getFile("file"));
+            ajax.put("fileName", fileName);
+            return ajax;
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
 }
 

+ 13 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/work/controller/FollowRecordController.java

@@ -5,6 +5,8 @@ import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.PageUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.web.work.domain.FollowRecord;
 import com.ruoyi.web.work.domain.FollowTemplate;
 import com.ruoyi.web.work.domain.dto.VisitDto;
@@ -16,6 +18,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
 import java.util.Arrays;
 import java.util.List;
 
@@ -83,4 +86,14 @@ public class FollowRecordController extends BaseController {
     public AjaxResult remove(@PathVariable Long[] ids) {
         return toAjax(followRecordService.removeByIds(Arrays.asList(ids)));
     }
+
+    @Log(title = "回访记录", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasAnyPermi('work:record:export,work:up:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, FollowRecord followRecord) {
+        PageUtils.orderBy("k.id desc");
+        List<FollowRecord> list = followRecordService.selectList(followRecord);
+        ExcelUtil<FollowRecord> util = new ExcelUtil<FollowRecord>(FollowRecord.class);
+        util.exportExcel(response, list, "回访记录");
+    }
 }

+ 7 - 2
ruoyi-admin/src/main/java/com/ruoyi/web/work/domain/FollowRecord.java

@@ -2,6 +2,7 @@ package com.ruoyi.web.work.domain;
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.common.annotation.Excel;
 import com.ruoyi.common.core.domain.BaseData;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -35,18 +36,22 @@ public class FollowRecord extends BaseData {
     @ApiModelProperty(value = "患者id")
     private Long patientId;
 
-    @ApiModelProperty(value = "患者姓名")
-    private String patientName;
 
     @NotBlank(message = "模板名称或提醒标题不能为空")
     @ApiModelProperty(value = "模板名称")
+    @Excel(name = "模板名称",sort =1)
     private String templateName;
 
+    @ApiModelProperty(value = "患者姓名")
+    @Excel(name = "患者姓名",sort =2)
+    private String patientName;
+
     @NotBlank(message = "回访内容或者提醒内容不能为空")
     @ApiModelProperty(value = "回访内容")
     private String op;
 
     @ApiModelProperty(value = "状态:0=未回访,1=已回访")
+    @Excel(name = "账户状态", readConverterExp = "0=未回访,1=已回访",sort =3)
     private Integer state;
 
     @TableField(exist = false)

+ 2 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseData.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.FieldFill;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.ruoyi.common.annotation.Excel;
 import lombok.Data;
 import lombok.experimental.Accessors;
 
@@ -47,6 +48,7 @@ public class BaseData {
 
     @TableField(fill = FieldFill.INSERT)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间",dateFormat = "yyyy-MM-dd HH:mm",sort =100,width=30)
     private Date createTime;
 
     @TableField(fill = FieldFill.UPDATE)

+ 2 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java

@@ -28,6 +28,8 @@ public class MimeTypeUtils {
     public static final String[] DEFAULT_ALLOWED_EXTENSION = {
             // 图片
             "bmp", "gif", "jpg", "jpeg", "png",
+            // 音频
+            "aac", "m4a",".caf",
             // word excel powerpoint
             "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
             // 压缩文件