297 lines
12 KiB
Vue
297 lines
12 KiB
Vue
<template>
|
|
<view class="page">
|
|
<view class="page-header">
|
|
<text class="page-title">员工备注</text>
|
|
<button class="add-button" @click="addNote">新增备注</button>
|
|
</view>
|
|
<scroll-view scroll-y class="page-scroll" lower-threshold="80" @scrolltolower="loadMore">
|
|
<view v-if="loading && !notes.length" class="empty-text">加载中...</view>
|
|
<view v-else-if="notes.length" class="note-list">
|
|
<view v-for="note in notes" :key="note._id" class="note-card">
|
|
<view class="note-content-row">
|
|
<text class="note-content">{{ note.content || '暂无备注内容' }}</text>
|
|
<view v-if="canEdit(note)" class="edit-button" @click="editNote(note)">
|
|
<uni-icons type="compose" size="19" color="#0877F1" />
|
|
</view>
|
|
</view>
|
|
<view v-if="getImageAttachments(note).length" class="image-list">
|
|
<image
|
|
v-for="attachment in getImageAttachments(note)"
|
|
:key="attachment.url"
|
|
class="note-image"
|
|
mode="aspectFill"
|
|
:src="attachment.url"
|
|
@click="previewImage(note, attachment.url)"
|
|
/>
|
|
</view>
|
|
<view v-if="getAudioAttachments(note).length" class="audio-list">
|
|
<view
|
|
v-for="(attachment, index) in getAudioAttachments(note)"
|
|
:key="attachment.url"
|
|
class="audio-item"
|
|
@click="openAudioPlayer(getAudioAttachments(note), index)"
|
|
>
|
|
<uni-icons type="sound-filled" size="16" color="#0877F1" />
|
|
<text class="audio-name">{{ attachment.name || `音频 ${index + 1}` }}</text>
|
|
</view>
|
|
</view>
|
|
<text class="note-meta">{{ getNoteMeta(note) }}</text>
|
|
</view>
|
|
<view v-if="notes.length < total" class="load-more" @click="loadMore">
|
|
{{ loading ? '加载中...' : '加载更多' }}
|
|
</view>
|
|
</view>
|
|
<view v-else class="empty-text">暂无员工备注</view>
|
|
</scroll-view>
|
|
|
|
<uni-popup ref="audioPopup" type="center" @change="handleAudioPopupChange">
|
|
<view class="audio-popup">
|
|
<view class="audio-popup-header">
|
|
<view class="audio-popup-title"><uni-icons type="sound-filled" size="16" color="#fff" /><text>音频播放</text></view>
|
|
<uni-icons type="closeempty" size="22" color="#606266" @click="closeAudioPlayer" />
|
|
</view>
|
|
<view v-if="currentAudio" class="audio-popup-body">
|
|
<text class="current-audio-name">{{ currentAudio.name || '音频' }}</text>
|
|
<view class="play-button" @click="toggleAudio">
|
|
<view v-if="audioPlaying" class="pause-icon"><view></view><view></view></view>
|
|
<view v-else class="play-icon"></view>
|
|
</view>
|
|
<text class="play-tip">{{ audioPlaying ? '正在播放,点击暂停' : '点击播放' }}</text>
|
|
<view v-if="playerAudios.length > 1" class="audio-options">
|
|
<view
|
|
v-for="(audio, index) in playerAudios"
|
|
:key="`${audio.url}-${index}`"
|
|
class="audio-option"
|
|
:class="{ active: activeAudioIndex === index }"
|
|
@click="selectAudio(index)"
|
|
>
|
|
{{ audio.name || `音频 ${index + 1}` }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</uni-popup>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, nextTick, ref } from 'vue';
|
|
import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app';
|
|
import dayjs from 'dayjs';
|
|
import { storeToRefs } from 'pinia';
|
|
import api from '@/utils/api';
|
|
import useAccountStore from '@/store/account';
|
|
|
|
const EDIT_STORAGE_KEY = 'ykt_case_employee_note_edit';
|
|
const accountStore = useAccountStore();
|
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
|
const { getDoctorInfo } = accountStore;
|
|
const customerId = ref('');
|
|
const notes = ref([]);
|
|
const total = ref(0);
|
|
const page = ref(1);
|
|
const loading = ref(false);
|
|
const audioPopup = ref(null);
|
|
const playerAudios = ref([]);
|
|
const activeAudioIndex = ref(0);
|
|
const audioPlaying = ref(false);
|
|
const currentAudio = computed(() => playerAudios.value[activeAudioIndex.value] || null);
|
|
let initialized = false;
|
|
let audioPlayer = null;
|
|
|
|
function getUserId() {
|
|
const doctor = doctorInfo.value || {};
|
|
const currentAccount = account.value || {};
|
|
return String(doctor.userid || doctor.userId || doctor.corpUserId || currentAccount.userid || currentAccount.userId || '');
|
|
}
|
|
|
|
function getCorpId() {
|
|
const doctor = doctorInfo.value || {};
|
|
const currentAccount = account.value || {};
|
|
return String(doctor.corpId || currentAccount.corpId || '');
|
|
}
|
|
|
|
function getImageAttachments(note) {
|
|
return (note?.attachments || []).filter((attachment) => attachment?.type === 'image' && attachment.url);
|
|
}
|
|
|
|
function getAudioAttachments(note) {
|
|
return (note?.attachments || []).filter((attachment) => attachment?.type === 'audio' && attachment.url);
|
|
}
|
|
|
|
function getNoteMeta(note) {
|
|
const time = dayjs(note?.createTime).isValid() ? dayjs(note.createTime).format('YYYY-MM-DD HH:mm') : '';
|
|
return [note?.creatorDepartmentName, note?.creatorUserName, time].filter(Boolean).join(' · ');
|
|
}
|
|
|
|
function canEdit(note) {
|
|
const userId = getUserId();
|
|
return Boolean(userId && String(note?.creatorUserId) === userId);
|
|
}
|
|
|
|
async function load(reset = false) {
|
|
if (!customerId.value || loading.value) return;
|
|
if (reset) {
|
|
page.value = 1;
|
|
notes.value = [];
|
|
total.value = 0;
|
|
}
|
|
const corpId = getCorpId();
|
|
if (!corpId) return;
|
|
loading.value = true;
|
|
try {
|
|
const res = await api('getEmployeeNotePage', {
|
|
corpId,
|
|
customerId: customerId.value,
|
|
page: page.value,
|
|
pageSize: 20,
|
|
}, false);
|
|
if (!res?.success) throw new Error(res?.message || '获取员工备注失败');
|
|
const result = res.data?.data || res.data || {};
|
|
const list = Array.isArray(result.list) ? result.list : [];
|
|
notes.value = reset ? list : [...notes.value, ...list];
|
|
total.value = Number(result.total) || 0;
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '获取员工备注失败', icon: 'none' });
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function loadMore() {
|
|
if (loading.value || notes.value.length >= total.value) return;
|
|
page.value += 1;
|
|
load();
|
|
}
|
|
|
|
function previewImage(note, url) {
|
|
wx.previewImage({ current: url, urls: getImageAttachments(note).map((attachment) => attachment.url) });
|
|
}
|
|
|
|
function ensureAudioPlayer() {
|
|
if (!audioPlayer) {
|
|
audioPlayer = wx.createInnerAudioContext();
|
|
audioPlayer.onPlay(() => (audioPlaying.value = true));
|
|
audioPlayer.onPause(() => (audioPlaying.value = false));
|
|
audioPlayer.onStop(() => (audioPlaying.value = false));
|
|
audioPlayer.onEnded(() => (audioPlaying.value = false));
|
|
}
|
|
return audioPlayer;
|
|
}
|
|
|
|
function playCurrentAudio() {
|
|
const audio = currentAudio.value;
|
|
if (!audio?.url) return;
|
|
const player = ensureAudioPlayer();
|
|
player.stop();
|
|
player.src = audio.url;
|
|
player.play();
|
|
}
|
|
|
|
function openAudioPlayer(audios, index) {
|
|
playerAudios.value = audios.filter((audio) => audio?.url);
|
|
activeAudioIndex.value = Math.min(Math.max(index, 0), Math.max(playerAudios.value.length - 1, 0));
|
|
nextTick(() => {
|
|
audioPopup.value?.open();
|
|
playCurrentAudio();
|
|
});
|
|
}
|
|
|
|
function toggleAudio() {
|
|
const player = ensureAudioPlayer();
|
|
if (audioPlaying.value) player.pause();
|
|
else player.play();
|
|
}
|
|
|
|
function selectAudio(index) {
|
|
if (index === activeAudioIndex.value && audioPlaying.value) return;
|
|
activeAudioIndex.value = index;
|
|
nextTick(playCurrentAudio);
|
|
}
|
|
|
|
function stopAudio() {
|
|
if (!audioPlayer) return;
|
|
audioPlayer.stop();
|
|
audioPlaying.value = false;
|
|
}
|
|
|
|
function closeAudioPlayer() {
|
|
stopAudio();
|
|
audioPopup.value?.close();
|
|
}
|
|
|
|
function handleAudioPopupChange(event) {
|
|
if (!event.show) stopAudio();
|
|
}
|
|
|
|
function addNote() {
|
|
uni.navigateTo({ url: `/pages/case/employee-note-edit?customerId=${encodeURIComponent(customerId.value)}` });
|
|
}
|
|
|
|
function editNote(note) {
|
|
if (!canEdit(note)) return;
|
|
uni.setStorageSync(EDIT_STORAGE_KEY, note);
|
|
uni.navigateTo({
|
|
url: `/pages/case/employee-note-edit?customerId=${encodeURIComponent(customerId.value)}¬eId=${encodeURIComponent(note._id)}`,
|
|
});
|
|
}
|
|
|
|
onLoad(async (options) => {
|
|
customerId.value = String(options?.customerId || '');
|
|
await getDoctorInfo();
|
|
initialized = true;
|
|
load(true);
|
|
});
|
|
|
|
onShow(() => {
|
|
if (initialized) load(true);
|
|
});
|
|
|
|
onHide(() => {
|
|
closeAudioPlayer();
|
|
});
|
|
|
|
onUnload(() => {
|
|
stopAudio();
|
|
audioPlayer?.destroy();
|
|
audioPlayer = null;
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.page { height: 100vh; display: flex; flex-direction: column; background: #f5f6f8; }
|
|
.page-header { display: flex; align-items: center; justify-content: space-between; padding: 22rpx 28rpx; background: #fff; border-bottom: 2rpx solid #f0f0f0; }
|
|
.page-title { font-size: 32rpx; font-weight: 600; color: #222; }
|
|
.add-button { margin: 0; padding: 0 24rpx; line-height: 64rpx; font-size: 26rpx; color: #fff; background: #0877F1; border-radius: 8rpx; }
|
|
.page-scroll { flex: 1; min-height: 0; }
|
|
.note-list { padding: 20rpx 24rpx; }
|
|
.note-card { padding: 24rpx; background: #fff; border-radius: 12rpx; }
|
|
.note-card + .note-card { margin-top: 20rpx; }
|
|
.note-content-row { display: flex; align-items: flex-start; }
|
|
.note-content { flex: 1; min-width: 0; font-size: 28rpx; line-height: 44rpx; color: #333; word-break: break-all; }
|
|
.edit-button { display: flex; justify-content: center; align-items: center; width: 44rpx; height: 44rpx; margin-left: 16rpx; }
|
|
.image-list { display: flex; flex-wrap: wrap; margin-top: 16rpx; }
|
|
.note-image { width: 140rpx; height: 140rpx; margin: 0 16rpx 16rpx 0; border-radius: 8rpx; }
|
|
.audio-list { margin-top: 12rpx; }
|
|
.audio-item { display: flex; align-items: center; min-height: 48rpx; color: #0877F1; }
|
|
.audio-name { max-width: 500rpx; margin-left: 10rpx; overflow: hidden; font-size: 26rpx; text-overflow: ellipsis; white-space: nowrap; }
|
|
.note-meta { display: block; margin-top: 12rpx; font-size: 24rpx; color: #999; }
|
|
.empty-text, .load-more { padding: 80rpx 0; font-size: 28rpx; color: #999; text-align: center; }
|
|
.load-more { padding: 28rpx 0; color: #0877F1; }
|
|
.audio-popup { width: 620rpx; padding: 30rpx; background: #fff; border-radius: 16rpx; box-shadow: 0 16rpx 48rpx rgba(15, 23, 42, .16); }
|
|
.audio-popup-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: 20rpx; border-bottom: 2rpx solid #eee; }
|
|
.audio-popup-title { display: flex; align-items: center; font-size: 30rpx; font-weight: 600; color: #222; }
|
|
.audio-popup-title :deep(.uni-icons) { display: flex; align-items: center; justify-content: center; width: 40rpx; height: 40rpx; margin-right: 12rpx; background: #0877F1; border-radius: 50%; }
|
|
.audio-popup-body { padding-top: 32rpx; }
|
|
.current-audio-name { display: block; padding: 18rpx 20rpx; overflow: hidden; font-size: 26rpx; color: #334155; text-align: center; text-overflow: ellipsis; white-space: nowrap; background: #f8fafc; border-radius: 10rpx; }
|
|
.play-button { display: flex; align-items: center; justify-content: center; width: 112rpx; height: 112rpx; margin: 32rpx auto 16rpx; background: linear-gradient(135deg, #60a5fa, #2563eb); border-radius: 50%; box-shadow: 0 12rpx 24rpx rgba(37, 99, 235, .28); }
|
|
.play-icon { width: 0; height: 0; margin-left: 8rpx; border-top: 18rpx solid transparent; border-bottom: 18rpx solid transparent; border-left: 28rpx solid #fff; }
|
|
.pause-icon { display: flex; gap: 12rpx; }
|
|
.pause-icon view { width: 10rpx; height: 34rpx; background: #fff; border-radius: 6rpx; }
|
|
.play-tip { display: block; font-size: 26rpx; color: #666; text-align: center; }
|
|
.audio-options { margin-top: 28rpx; }
|
|
.audio-option { padding: 18rpx 20rpx; overflow: hidden; font-size: 26rpx; color: #475569; text-overflow: ellipsis; white-space: nowrap; border: 2rpx solid #dbe4ef; border-radius: 8rpx; }
|
|
.audio-option + .audio-option { margin-top: 12rpx; }
|
|
.audio-option.active { color: #0877F1; background: #eff6ff; border-color: #0877F1; }
|
|
</style>
|