Commit 3b4e440d authored by songrui's avatar songrui

随访详情

parent 8e6ad985
......@@ -78,7 +78,9 @@
// 字体大小
.text-16 { font-size: 16px; }
.text-12 { font-size: 12px; }
.text-start { text-align: start; }
.text-center { text-align: center; }
.text-end { text-align: end; }
.text-black { color: #000; }
.text-green { color: #52C41A; }
.text-red { color: #FF4D4F; }
......
<svg width="10" height="6" viewBox="0 0 10 6" xmlns="http://www.w3.org/2000/svg">
<path d="M4.99999 3.78145L8.29999 0.481445L9.24266 1.42411L4.99999 5.66678L0.757324 1.42411L1.69999 0.481445L4.99999 3.78145Z"/>
</svg>
<svg width="8" height="9" viewBox="0 0 8 9" xmlns="http://www.w3.org/2000/svg">
<rect x="0.158203" y="0.900391" width="2.68262" height="7.70361" rx="1"/>
<rect x="5.1582" y="0.900391" width="2.68262" height="7.70361" rx="1"/>
</svg>
\ No newline at end of file
<svg width="8" height="9" viewBox="0 0 8 9" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6432 3.90404C7.26987 4.29571 7.26987 5.20837 6.6432 5.60004L1.53 8.79579C0.863951 9.21207 0 8.73323 0 7.94779V1.55629C0 0.770851 0.863951 0.292008 1.53 0.708288L6.6432 3.90404Z"/>
</svg>
<template>
<div class="doc-image">
<div v-if="isPdf" class="p-2 flex items-center view-pdf">
<doc-icon type="doc-PDF" style="font-size: .48rem" class="shrink-0"/>
<span class="grow px-4 text-ellipsis">{{name}}</span>
<span class="close-btn" @click.stop="removeBtn" v-if="remove">
<doc-icon type="close-circle" />
</span>
</div>
<div class="view-img" v-if="status==='success' && !isPdf">
<img :src="src" :alt="name"
:loading="loading" >
</div>
<template v-if="!isPdf">
<div class="doc-image-wrapper" v-if="status==='loading'">
<slot name="placeholder">
<div class="doc-image-default">加载中...</div>
</slot>
</div>
<div class="doc-image-wrapper" v-if="status==='fail'">
<slot name="fail">
<div class="doc-image-default">加载失败</div>
</slot>
</div>
<span class="close-btn" @click.stop="removeBtn" v-if="remove">
<doc-icon type="close-circle" />
</span>
</template>
</div>
</template>
<script>
export default {
name: 'doc-image',
props: {
src: String,
// 浏览器应当如何加载该图像
// img标签中的属性 需要浏览器支持
// https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/img
// eager 立即加载图像
// lazy 延迟加载图像,直到它和视口接近到一个计算得到的距离
loading: String,
// 开启删除功能
remove: Boolean,
// 处理pdf预览
isPdf: Boolean,
name: String
},
emits: ['removeBtn'],
data() {
return {
// 图片加载状态 loading | fail | success
status: 'loading',
imgStyle: {},
// viewer显示
visible: false,
// 按键界面显示
btVisible: false
}
},
methods: {
loadImage() {
if (this.isPdf) {
this.status = 'success'
return
}
const image = new Image()
image.onload = () => {
this.show = true
this.status = 'success'
}
image.onerror = (err) => {
console.warn('doc-image', err)
this.show = false
this.status = 'fail'
}
this.status = 'loading'
image.src = this.src
},
removeBtn() {
this.btVisible = false
this.$emit('onRemove', this.src)
}
},
watch: {
src: {
handler(val) {
if (val) {
this.loadImage()
}
},
immediate: true
}
}
}
</script>
<style lang="less" scoped>
.doc-image {
display: inline-block;
width: 100%;
height: 100%;
position: relative;
.view-img {
width: 100%;
height: 100%;
min-height: 90px;
// border: 1px dashed #d9d9d9;
// background: #fafafa;
background: #EFF2F7;
img {
width: 100%;
height: 90px;
object-fit: contain;
}
}
.view-pdf {
width: 100%;
min-height: 48px;
overflow-y: hidden;
border-radius: 8px;
border: 1px solid #EEEEEE;
.close-btn {
top: 50%;
right: 8px;
transform: translateY(-50%);
}
}
.close-btn {
position: absolute;
font-size: 16px;
top: -8px;
right: -8px;
}
}
.doc-image-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.doc-image-default {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
background: #f5faf7;
color: #a8abb2;
vertical-align: middle;
user-select: none;
}
</style>
<template>
<div class="px-3 py-3 flex items-center doc-nav-bar">
<div class="shrink-0 left">
<slot name="left">
<span class="back-btn" @click="goBack" v-if="!hideBack">
<doc-icon type="doc-left" style="color: #262626"/>
</span>
</slot>
</div>
<div class="grow text-center font-semibold title">
<slot>
{{title}}
</slot>
</div>
<div class="shrink-0 right">
<slot name="right"></slot>
</div>
</div>
</template>
<script>
import { backHome } from '@/utils/common.js'
export default {
name: 'DocNavBar',
props: {
title: String,
// 是否首页
home: Boolean,
// 替换返回函数
backFunc: Function,
// 隐藏返回按键
hideBack: Boolean
},
methods: {
goBack() {
if (this.backFunc) {
this.backFunc()
return
}
if (this.home) {
backHome()
return
}
this.$router.back()
}
}
}
</script>
<style lang="less" scoped>
.doc-nav-bar {
background: transparent;
border-bottom: 1px solid #00000019;
.left {
min-width: 24px;
}
.title {
font-size: 16px;
}
}
</style>
<template>
<div>
<div class='list gap-x-2.5 gap-y-1 flex items-center flex-wrap'>
<div v-for="(url, index) in imgList" :key="index" @click='toPreview(index)'
class="flex gap-x-2.5">
<!-- <img :style="{width: imgSize.width, height: imgSize.height}" :src="url.trueDownloadUrl" /> -->
<DocImage :style="{width: imgSize.width, height: imgSize.height}" :src="url.trueDownloadUrl"/>
</div>
</div>
<van-overlay :show='imgShow' @click='imgShow = false'>
<div class='wrapper'>
<van-swipe class='block' :initial-swipe='initSwipe'>
<van-swipe-item v-for='image in imgList' :key='image'>
<img :src='image.trueDownloadUrl' style='width: 100%;height: 100%'/>
</van-swipe-item>
</van-swipe>
</div>
</van-overlay>
</div>
</template>
<script>
import DocImage from '../docImage/DocImage.vue'
export default {
name: 'ImagePreview',
components: {
DocImage
},
props: {
imgList: Array,
imgSize: {
default: () => {
return {
width: '.97rem',
height: '.97rem'
}
}}
},
data() {
return {
imgShow: false,
initSwipe: 0
}
},
methods: {
//图片预览
toPreview(index) {
this.initSwipe = index
this.imgShow = true
},
}
}
</script>
<style scoped lang='less'>
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.block {
width: 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="p-1 flex flex-col mp3">
<div v-if="file.annexName" class="text-12 mb-1 text-ellipsis">{{ file.annexName }}</div>
<div class="flex items-center justify-between gap-x-2.5">
<div class="shrink-0 play-bt" @click.stop="start(file)">
<doc-icon type="doc-play" v-if="!player.playing"/>
<doc-icon type="doc-pause" v-else/>
</div>
<span class="shrink-0 time">{{timeFormat(player.duration)}}</span>
<div class="grow progress">
<van-slider v-model="player.currentTime" :max="sliderMax" :bar-height="6" :button-size="0"
@change="onProgress" />
<!-- <div :style="`width: ${progress}%`"></div> -->
</div>
<span class="shrink-0 time" :style="`opacity: ${player.currentTime ? 1 : 0}`">
{{ timeFormat(player.currentTime) }}</span>
<span class="text-16 shrink-0 close-btn" @click.stop="removeBtn" v-if="remove">
<doc-icon type="close-circle" />
</span>
</div>
<audio ref="audio" type="audio/mpeg" crossOrigin="anonymous" preload="metadata" style="display: none"></audio>
</div>
</template>
<script>
import { musicPlayer } from './mp3.js'
import { showToast } from 'vant'
import { useStore } from '@/tumour/store/index.js'
export default {
props: {
file: { default: () => ({}) },
activeMediaUrl: { default: '' },
remove: Boolean
},
emits: ['play', 'onRemove'],
data() {
return {
store: useStore(),
player: {},
// 正在播放的items
activeAudio: {},
}
},
computed: {
sliderMax() {
return this.player.duration ? Math.floor(this.player.duration) : 100
},
progress() {
if (!this.player.currentTime) return 0
const temp = this.player.currentTime / this.player.duration
return Math.round(temp * 1000) / 10
}
},
mounted() {
this.init()
},
methods: {
init() {
this.player = new musicPlayer(this.$refs.audio)
this.player.init()
this.player.setSrc(this.file.annexUrl)
this.player.audioEl.onended = () => {
console.log('播放结束')
this.stop()
}
},
start(item) {
if (!item || !item.annexUrl) {
showToast('文件获取失败')
return
}
if (!this.player.audioCtx) {
this.player.init()
this.player.setSrc(item.annexUrl)
}
if (this.player.playing) {
this.stop()
} else {
this.player.audioEl.play()
this.activeAudio = item
this.player.playing = true
this.$emit('play', item)
console.log('this.player.duration', this.player.audioEl.duration)
}
console.log('this.player', this.player)
},
stop() {
this.player.audioEl.pause()
this.player.playing = false
},
timeFormat(value) {
if (!value || value == Infinity) {
return '00:00'
}
let date = Math.ceil(parseFloat(value))
let minutes = Math.floor(date / 60)
let seconds = date % 60
let format = `${minutes >= 10 ? minutes : '0' + minutes.toString()}:${seconds >= 10 ? seconds : '0' + seconds.toString()}`
return format
},
onProgress(value) {
this.player.progressChange(value)
this.player.playing = true
},
removeBtn() {
this.$emit('onRemove', this.file)
}
},
watch: {
activeMediaUrl(val) {
if (val !== this.file.annexUrl) {
this.stop()
}
},
'store.documentHidden': {
handler(val) {
if (val && this.activeMediaUrl == this.file.annexUrl) {
this.stop()
}
}
}
}
}
</script>
<style lang="less" scoped>
.mp3 {
background-color: #fff;
.play-bt {
display: inline-flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #E4F2F0;
font-size: 10px;
padding-left: 2px;
color: var(--van-primary-color);
}
.progress {
// height: 6px;
// border-radius: 4px;
// background: #EFF2F7;
// overflow: hidden;
// text-align: left;
// >div {
// height: 100%;
// background: #54CCBD;
// }
}
}
</style>
<template>
<div class="mp4">
<div class="flex flex-wrap justify-between video-list">
<div v-for="item in files" :key="item.annexId" @click.stop="start(item)">
<div class="item">
<div class="shrink-0 play-bt" >
<doc-icon type="doc-play" />
</div>
<span class="close-btn" @click.stop="removeBtn(item)" v-if="remove">
<doc-icon type="close-circle" />
</span>
</div>
<div v-if="item.annexName" class="text-12 my-1 text-ellipsis">{{ item.annexName }}</div>
</div>
</div>
<!-- <van-popup v-model:show="visible" :close-on-click-overlay="false" closeable
close-icon-position="top-right"
close-icon="clear">
<video controls v-if="visible" style="width: calc(100vw - var(--van-padding-md) * 2)">
<source :src="activeVideo.annexUrl" type="video/mp4" />
播放失败!
</video>
</van-popup> -->
<van-overlay :show="visible">
<div class="h-full flex items-center justify-center wrapper" @click.stop>
<video controls v-if="visible" ref="video">
<source :src="activeVideo.annexUrl" type="video/mp4" />
播放失败!
</video>
<van-icon name="close" class="close-icon" @click="visible = false"/>
</div>
</van-overlay>
</div>
</template>
<script>
import { useStore } from '@/tumour/store/index.js'
export default {
props: {
files: { default: () => [] },
activeMediaUrl: { default: '' },
remove: Boolean
},
emits: ['play', 'onRemove'],
data() {
return {
visible: false,
activeVideo: {},
store: useStore()
}
},
methods: {
start(item) {
this.activeVideo = item
this.visible = true
this.$emit('play', item)
},
removeBtn(item) {
this.$emit('onRemove', item)
}
},
watch: {
activeMediaUrl(val) {
if (val !== this.activeVideo.annexUrl) {
const dom = this.$refs.video
dom && dom.pause()
}
},
'store.documentHidden': {
handler(val) {
if (val && this.activeMediaUrl == this.activeVideo.annexUrl) {
const dom = this.$refs.video
dom && dom.pause()
}
}
}
}
}
</script>
<style lang="less" scoped>
.video-list {
>div {
width: calc(50% - 5px);
.item {
position: relative;
background: url('@/assets/image/video-default.png') no-repeat;
background-size: 100%;
height: .84rem;
display: flex;
align-items: center;
justify-content: center;
}
}
.play-bt {
display: inline-flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #E4F2F0;
font-size: 15px;
padding-left: 2px;
color: var(--van-primary-color);
}
.close-btn {
position: absolute;
font-size: 16px;
top: -8px;
right: -8px;
}
}
.wrapper {
position: relative;
.close-icon {
position: absolute;
top: 16px;
right: 16px;
color: #ccc;
font-size: 24px;
}
video {
width: calc(100vw - var(--van-padding-md) * 2);
background-color: #fff;
}
}
</style>
export class musicPlayer {
constructor(audioEl) {
this.audioEl = audioEl
// 音频上下文
this.audioCtx = null
// 音源
this.audioSource = null
// 时长
this.duration = 0
// 当前播放时间
this.currentTime = 0
// 播放进度
this.progress = 0
// 声音
this.volume = 20
// AnalyserNode 接口表示了一个可以提供实时频域和时域分析信息的节点
this.analyser = null
// 音量节点
this.gainNode = null
// 缓冲进度
this.timeRange = null
// 是否播放中
this.playing = false
}
init() {
// 音频上下文
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
if (!this.audioCtx) {
throw new Error('audioCtx is null')
}
// 获取音频数据的节点
this.analyser = this.audioCtx.createAnalyser()
// 音量节点
this.gainNode = this.audioCtx.createGain()
this.gainNode.gain.value = 1
this.audioEl.volume = this.volume / 100
// 从<audio>或<video>元素生成的音频源
this.audioSource = this.audioCtx.createMediaElementSource(this.audioEl)
this.audioEl.ontimeupdate = () => {
this.currentTime = this.audioEl.currentTime
if (this.progress >= this.duration) return
this.progress = this.currentTime
}
this.audioEl.onloadedmetadata = () => {
// 时长
this.duration = this.audioEl.duration
console.log('onloadedmetadata', this.duration)
}
this.audioEl.onprogress = () => {
// 当浏览器正在下载音频/视频时
this.timeRange = this.audioEl.buffered
if (this.timeRange && this.timeRange.length > 0) {
console.log(
'buffered',
this.audioEl.buffered.start(0),
this.audioEl.buffered.end(0)
)
}
}
this.audioEl.oncanplay = () => {
console.log('可播放')
setTimeout(() => {
this.getAudioSource()
}, 100)
}
this.audioEl.onerror = (e) => {
console.error('加载出现错误', e)
this.onerror(e)
}
}
onerror() {}
onended() {}
setSrc(src) {
this.progress = 0
this.audioEl.src = src
this.audioEl.load()
}
// 调整进度
progressChange(value) {
this.audioEl.currentTime = value
}
// 调整音量
volumeChange(value) {
this.volume = value
this.audioEl.volume = this.volume / 100
}
// 音频波形处理
getAudioSource() {
// 节点链接到音源
this.audioSource.connect(this.analyser)
// 链接音量节点
this.analyser.connect(this.gainNode)
this.gainNode.connect(this.audioCtx.destination)
return
// 使用快速傅立叶变换(Fast Fourier Transform (FFT) )来捕获音频数据
// this.analyser.fftSize = 2048
this.analyser.fftSize = 256
let bufferLength = this.analyser.frequencyBinCount
let dataArray = new Uint8Array(bufferLength)
let c = this.boardEl
let canvasWidth = c.width
let canvasHeight = c.height
let ctx = c.getContext('2d')
let left = this
ctx.strokeStyle = 'rgba(81,167,255, .5)'
drawBar()
function drawBar() {
// 波形绘制
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
let barWidth = (canvasWidth / bufferLength) * 1
let barHeight = 0
let x = 0
left.analyser.getByteFrequencyData(dataArray)
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2
let tempColor = barHeight * 3 > 255 ? 255 : barHeight * 3
ctx.fillStyle = 'rgba(' + tempColor + ', 160, 255, .5)'
ctx.fillRect(x, canvasHeight - barHeight / 2, barWidth, barHeight)
x += barWidth + 0.1
}
requestAnimationFrame(drawBar)
}
}
}
......@@ -14,4 +14,30 @@ dayjs.locale('cn')
export function registe(app) {
// 自定义组件
app.use(DocIcon)
app.config.globalProperties.$idCardHide = idCardHide
app.config.globalProperties.$phoneHide = phoneHide
app.config.globalProperties.$addrJoin = addrJoin
}
// idCard 脱敏
function idCardHide(idCard) {
if (!idCard || idCard.length < 18) {
return idCard
}
return idCard.substring(0, 6) + '******' + idCard.substring(14)
}
// phone 脱敏
function phoneHide(phone) {
if (!phone || phone.length < 11) {
return phone
}
return phone.substring(0, 3) + '******' + phone.substring(9)
}
// 地址拼接显示
function addrJoin(str1 = '', str2 = '') {
if (!str1 && !str2) return '-'
return (str1 ?? '') + (str2 ?? '')
}
......@@ -9,7 +9,12 @@ const routes = [
{
path: 'screening/simple/form',
name: 'tumour-screening-simple-form',
component: () => import(/* webpackChunkName: "page-tumour" */ '@/tumour/screening/simpleV2/form/Index.vue')
component: () => import(/* webpackChunkName: "page-tumour-screening" */ '@/tumour/screening/simpleV2/form/Index.vue')
},
{
path: 'visit/detail',
name: 'tumour-visit-detail',
component: () => import(/* webpackChunkName: "page-tumour-visit" */ '@/tumour/visit/detail/Detail.vue')
}
]
}
......
......@@ -15,6 +15,8 @@ import { useStore } from './store/index.js'
export default {
created() {
this.init()
// 监听页面是否隐藏
document.addEventListener('visibilitychange', this.visibilitychange)
},
setup() {
const store = useStore()
......@@ -25,7 +27,17 @@ export default {
getDict().then(res => {
this.store.$patch({ dict: res.data })
})
},
visibilitychange() {
if (document.hidden) {
this.store.onDocumentHidden(true)
} else {
this.store.onDocumentHidden(false)
}
}
},
beforeUnmount() {
document.removeEventListener('visibilitychange', this.visibilitychange)
}
}
</script>
......
import { fetchBase } from '@/utils/fetch.js'
// 简易筛查 新增
export function getVisitById(id, loading = true) {
return fetchBase({ url: `/tumour-admin/v1/h5-app/visit-details/${id}`, loading })
}
......@@ -194,7 +194,7 @@ import { useStore } from '@/tumour/store/index.js'
import { getResidentInfo } from '@/tumour/api/base.js'
import { getDictValue } from '@/tumour/utils/dictionaries.js'
import { showToast } from 'vant'
import DocAddress from '@/tumour/utils/docAddress/DocAddress.vue'
import DocAddress from '@/components/docAddress/DocAddress.vue'
const defaultForm = (info = {}) => {
const form = {
......
......@@ -4,7 +4,9 @@ export const useStore = defineStore('tumour', {
state: () => {
return {
// 字典
dict: []
dict: [],
// 页面是否处于隐藏状态
documentHidden: false
}
},
getters: {},
......@@ -12,6 +14,9 @@ export const useStore = defineStore('tumour', {
getDict(val) {
if (!val) return []
return this.dict[val] || []
},
onDocumentHidden(value) {
this.documentHidden = value
}
}
})
This diff is collapsed.
......@@ -87,3 +87,72 @@ export function getInfoByIdCard(idCard) {
age
}
}
//原生方法调用 当前方法传参数两种 字符串或者map
export function callMobile(handlerInterface, parameters) {
let classStr = Object.prototype.toString.call(parameters);
if (classStr === '[object String]' || classStr === '[object Object]') {
let param = parameters;
if (classStr === "[object Object]") { //判断传参是str ,还是object
//handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
param = JSON.stringify(parameters);//参数必须转化成json格式
}
try {
if (isIOSWebKit()) {//ios
if (window.webkit !== undefined) {
if (param == '{}') {
window.webkit.messageHandlers[handlerInterface].postMessage(null);
} else {
window.webkit.messageHandlers[handlerInterface].postMessage(param);
}
}
} else if (isAndroid()) {
//安卓传输不了js json对象,只能传输string
if (param == '{}') {
window['function'][handlerInterface]();
} else {
window['function'][handlerInterface](param);
}
}
} catch (e) {
}
}
}
// 判断是否是ios
export function isIOSWebKit() {
const aa = window.navigator.userAgent;
if (!!aa.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {// ios端
return true;
}
}
// 判断是否安卓
export function isAndroid() {
const aa = window.navigator.userAgent;
if (aa.indexOf('Android') !== -1 || aa.indexOf('Adr') !== -1) {
return true
}
}
// 判断是否在微信中
export function isWeiXin() {
const ua = window.navigator.userAgent.toLowerCase()
return /micromessenger/.test(ua)
}
//关闭页面
export function backHome() {
if (isIOSWebKit()) { //ios
console.log('ios')
callMobile("onBack", {})
} else if (isAndroid()) {
console.log('Android')
callMobile("closePage", {})
}
if (isWeiXin()) { // 微信中
console.log('weixin')
WeixinJSBridge.call('closeWindow')
}
}
\ No newline at end of file
......@@ -22,7 +22,7 @@ module.exports = defineConfig({
// 设置代理
proxy: {
'/tumour-admin': {
target: 'http://192.168.1.118:8081',
target: 'http://192.168.1.174:8081',
// target: 'https://beta-tumour.zmnyjk.com',
changOrigin: true,
pathRewrite: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment