feat: 员工备注调整

This commit is contained in:
huxuejian 2026-09-20 17:01:42 +08:00
parent 4ef5949f5b
commit eb6f5a401b
7 changed files with 708 additions and 56 deletions

View File

@ -1,4 +1,4 @@
MP_API_BASE_URL=http://192.168.60.2:8080 MP_API_BASE_URL=http://192.168.31.160:8080
MP_IMAGE_URL=https://patient.youcan365.com MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx1d8337a40c11d66c MP_WX_APP_ID=wx1d8337a40c11d66c

View File

@ -1,6 +1,7 @@
<template> <template>
<template v-for="item in formItems" :key="item.title"> <template v-for="item in formItems" :key="item.title">
<form-cell v-bind="item" :form="formModel" :disableChange="disabledMap[item.title]" @change="change" /> <view v-if="item.title==='notes'"></view>
<form-cell v-else v-bind="item" :form="formModel" :disableChange="disabledMap[item.title]" @change="change" />
</template> </template>
</template> </template>
<script setup> <script setup>

View File

@ -268,6 +268,20 @@
"disableScroll": true "disableScroll": true
} }
}, },
{
"path": "employee-note-list",
"style": {
"navigationBarTitleText": "员工备注",
"disableScroll": true
}
},
{
"path": "employee-note-edit",
"style": {
"navigationBarTitleText": "员工备注",
"disableScroll": true
}
},
{ {
"path": "archive-edit", "path": "archive-edit",
"style": { "style": {

View File

@ -54,18 +54,17 @@
</view> </view>
<!-- 备注 --> <!-- 备注 -->
<view class="info-block border-bottom"> <view class="info-block border-bottom" @click="openEmployeeNotes">
<view class="block-header"> <view class="block-header">
<text class="input-label">备注</text> <text class="input-label">备注</text>
<view class="edit-icon" @click.stop="openNotes"> <view class="edit-icon">
<uni-icons type="compose" size="20" color="#0877F1" /> <uni-icons type="right" size="18" color="#bbb" />
</view> </view>
</view> </view>
<view class="block-content"> <view class="block-content employee-note-summary" :class="{ 'employee-note-summary--active': latestEmployeeNote }">
<text v-if="archive.notes" class="note-content">{{ archive.notes }}</text> <uni-icons v-if="hasEmployeeNoteImage" type="image-filled" size="18" color="#0877F1" />
<view v-else class="empty-click" @click="openNotes"> <uni-icons v-if="hasEmployeeNoteAudio" type="sound-filled" size="18" color="#0877F1" />
<text class="placeholder">添加备注</text> <text class="note-content">{{ latestEmployeeNoteText }}</text>
</view>
</view> </view>
</view> </view>
@ -158,33 +157,6 @@
</view> </view>
</uni-popup> </uni-popup>
<!-- 修改备注(底部弹层) -->
<uni-popup ref="notesPopup" type="bottom">
<view class="sheet">
<view class="sheet-header">
<view class="sheet-header-left" />
<view class="sheet-title">修改备注</view>
<view class="sheet-close" @click="closeNotes">
<uni-icons type="closeempty" size="18" color="#333" />
</view>
</view>
<view class="sheet-body">
<textarea
v-model="notesInput"
class="notes-textarea"
placeholder="请输入备注"
:maxlength="100"
/>
<view class="counter">{{ notesInput.length }}/100</view>
</view>
<view class="sheet-footer">
<button class="primary-btn" @click="saveNotes">保存</button>
</view>
</view>
</uni-popup>
<!-- 选择分组(底部弹层) --> <!-- 选择分组(底部弹层) -->
<uni-popup ref="groupPopup" type="bottom"> <uni-popup ref="groupPopup" type="bottom">
<view class="sheet"> <view class="sheet">
@ -350,6 +322,7 @@ function normalizeArchiveFromApi(raw) {
createTime: r.createTime || '', createTime: r.createTime || '',
creator: r.creator || '', creator: r.creator || '',
notes: r.notes || r.remark || '', notes: r.notes || r.remark || '',
latestEmployeeNote: r.latestEmployeeNote || null,
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [], groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
chatGroupId: normalizeGroupId(r.chatGroupId || r.groupId || r.groupID || r.imGroupId || r.imGroupID || r.consultGroupId || ''), chatGroupId: normalizeGroupId(r.chatGroupId || r.groupId || r.groupID || r.imGroupId || r.imGroupID || r.consultGroupId || ''),
}; };
@ -385,6 +358,7 @@ async function fetchArchive() {
} }
archive.value = { ...archive.value, ...normalized }; archive.value = { ...archive.value, ...normalized };
await fetchLatestEmployeeNote();
saveToStorage(); saveToStorage();
loadTeamMembers(); loadTeamMembers();
await fetchTeamGroups(true); await fetchTeamGroups(true);
@ -618,6 +592,37 @@ const createText = computed(() => {
return ''; return '';
}); });
const latestEmployeeNote = computed(() => archive.value.latestEmployeeNote || null);
const latestEmployeeNoteAttachments = computed(() =>
Array.isArray(latestEmployeeNote.value?.attachments) ? latestEmployeeNote.value.attachments : []
);
const hasEmployeeNoteImage = computed(() =>
latestEmployeeNoteAttachments.value.some((attachment) => attachment?.type === 'image' && attachment.url)
);
const hasEmployeeNoteAudio = computed(() =>
latestEmployeeNoteAttachments.value.some((attachment) => attachment?.type === 'audio' && attachment.url)
);
const latestEmployeeNoteText = computed(() => latestEmployeeNote.value?.content || '暂无员工备注');
async function fetchLatestEmployeeNote() {
const corpId = getCorpId();
if (!corpId || !archiveId.value) {
archive.value.latestEmployeeNote = null;
return;
}
try {
const res = await api('getLatestEmployeeNote', { corpId, customerId: archiveId.value }, false);
archive.value.latestEmployeeNote = res?.success ? (res.data?.data || res.data || null) : null;
} catch (e) {
archive.value.latestEmployeeNote = null;
}
}
function openEmployeeNotes() {
if (!archiveId.value) return;
uni.navigateTo({ url: `/pages/case/employee-note-list?customerId=${encodeURIComponent(archiveId.value)}` });
}
const chatGroupId = ref(''); const chatGroupId = ref('');
const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || '')); const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || ''));
const showGoChat = computed(() => Boolean(currentChatGroupId.value)); const showGoChat = computed(() => Boolean(currentChatGroupId.value));
@ -810,24 +815,6 @@ const saveContact = () => {
updateArchive({ mobile: v }).then((ok) => ok && closeContact()); updateArchive({ mobile: v }).then((ok) => ok && closeContact());
}; };
// ===== 备注 =====
const notesPopup = ref(null);
const notesInput = ref('');
const openNotes = () => {
notesInput.value = archive.value.notes ? String(archive.value.notes) : '';
notesPopup.value?.open();
};
const closeNotes = () => {
notesPopup.value?.close();
};
const saveNotes = () => {
const v = String(notesInput.value || '').trim();
updateArchive({ notes: v }).then((ok) => ok && closeNotes());
};
// ===== 分组 ===== // ===== 分组 =====
const groupPopup = ref(null); const groupPopup = ref(null);
const addGroupPopup = ref(null); const addGroupPopup = ref(null);
@ -1136,6 +1123,17 @@ const saveAddGroup = async () => {
word-break: break-all; word-break: break-all;
} }
.employee-note-summary {
display: flex;
align-items: center;
gap: 10rpx;
min-width: 0;
}
.employee-note-summary--active .note-content {
color: #0877F1;
}
.tags-wrap { .tags-wrap {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -0,0 +1,338 @@
<template>
<view class="page">
<scroll-view scroll-y class="page-scroll">
<view class="editor-card">
<textarea v-model="content" class="editor" placeholder="请输入备注内容" :maxlength="1000" />
<view class="counter">{{ content.length }}/1000</view>
</view>
<view class="attachment-card">
<text class="section-title">附件</text>
<view class="section-label">图片</view>
<view class="image-list">
<view v-for="{ attachment, index } in imageAttachments" :key="attachment.id" class="image-item">
<image class="attachment-image" mode="aspectFill" :src="attachment.url" @click="previewImage(attachment.url)" />
<view class="remove-icon" @click.stop="removeAttachment(index)">×</view>
</view>
<view v-if="attachments.length < 9" class="add-image" @click="chooseImage">
<uni-icons type="image-filled" size="24" color="#9ca3af" />
<text>添加图片</text>
</view>
</view>
<view class="section-label audio-label">语音</view>
<view v-for="{ attachment, index } in audioAttachments" :key="attachment.id" class="audio-item" @click="playAudio(attachment.url)">
<view class="audio-main"><uni-icons type="sound-filled" size="18" color="#0877F1" /><text>{{ attachment.name || '语音附件' }}</text></view>
<uni-icons type="closeempty" size="18" color="#9ca3af" @click.stop="removeAttachment(index)" />
</view>
<view v-if="attachments.length < 9" class="record-button" @click="recording ? stopRecording() : startRecording()">
<uni-icons :type="recording ? 'stop' : 'sound-filled'" size="19" :color="recording ? '#ef4444' : '#0877F1'" />
<text :class="recording ? 'recording-text' : ''">{{ recording ? `结束录音 ${recordingSeconds}s` : '录音' }}</text>
</view>
</view>
</scroll-view>
<view class="footer">
<button v-if="isEditing" class="delete-button" @click="remove">删除</button>
<button class="cancel-button" @click="cancel">取消</button>
<button class="save-button" :loading="saving" @click="save">保存</button>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import { uploadFile } from '@/utils/file';
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 noteId = ref('');
const content = ref('');
const attachments = ref([]);
const saving = ref(false);
const recording = ref(false);
const recordingSeconds = ref(0);
const isEditing = computed(() => Boolean(noteId.value));
const imageAttachments = computed(() =>
attachments.value
.map((attachment, index) => ({ attachment, index }))
.filter(({ attachment }) => attachment.type === 'image')
);
const audioAttachments = computed(() =>
attachments.value
.map((attachment, index) => ({ attachment, index }))
.filter(({ attachment }) => attachment.type === 'audio')
);
let recorderManager = null;
let audioPlayer = null;
let recordTimer = 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 normalizeExistingAttachment(attachment, index) {
return {
id: `existing-${attachment.url}-${index}`,
type: attachment.type,
name: attachment.name || '附件',
url: attachment.url,
mimeType: attachment.mimeType || '',
size: attachment.size || 0,
existing: true,
};
}
function chooseImage() {
const remaining = 9 - attachments.value.length;
if (remaining <= 0) return;
wx.chooseImage({
count: remaining,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const files = Array.isArray(res.tempFilePaths) ? res.tempFilePaths.slice(0, remaining) : [];
attachments.value.push(...files.map((path, index) => ({
id: `image-${Date.now()}-${index}`,
type: 'image',
name: `员工备注图片_${Date.now()}_${index + 1}.jpg`,
url: path,
localPath: path,
})));
},
});
}
function removeAttachment(index) {
attachments.value.splice(index, 1);
}
function previewImage(url) {
wx.previewImage({
current: url,
urls: imageAttachments.value.map(({ attachment }) => attachment.url),
});
}
function playAudio(url) {
if (!url) return;
if (!audioPlayer) audioPlayer = wx.createInnerAudioContext();
audioPlayer.stop();
audioPlayer.src = url;
audioPlayer.play();
}
function clearRecordTimer() {
if (recordTimer) clearInterval(recordTimer);
recordTimer = null;
recordingSeconds.value = 0;
}
function initRecorder() {
if (recorderManager) return;
recorderManager = wx.getRecorderManager();
recorderManager.onStop((res) => {
clearRecordTimer();
recording.value = false;
if (!res?.tempFilePath) return;
attachments.value.push({
id: `audio-${Date.now()}`,
type: 'audio',
name: `员工备注录音_${Date.now()}.aac`,
url: res.tempFilePath,
localPath: res.tempFilePath,
mimeType: 'audio/aac',
});
});
recorderManager.onError(() => {
clearRecordTimer();
recording.value = false;
uni.showToast({ title: '录音失败,请重试', icon: 'none' });
});
}
function startRecording() {
if (attachments.value.length >= 9) return;
initRecorder();
try {
recorderManager.start({ duration: 600000, sampleRate: 44100, numberOfChannels: 1, encodeBitRate: 192000, format: 'aac' });
recording.value = true;
recordingSeconds.value = 0;
recordTimer = setInterval(() => (recordingSeconds.value += 1), 1000);
} catch (e) {
clearRecordTimer();
recording.value = false;
uni.showToast({ title: '录音启动失败', icon: 'none' });
}
}
function stopRecording() {
if (!recorderManager || !recording.value) return;
recorderManager.stop();
}
async function getSubmitAttachments() {
return Promise.all(attachments.value.map(async (attachment) => {
if (attachment.existing) {
return {
type: attachment.type,
name: attachment.name,
url: attachment.url,
mimeType: attachment.mimeType,
size: attachment.size,
};
}
const url = await uploadFile(attachment.localPath);
if (!url) throw new Error('附件上传失败');
return {
type: attachment.type,
name: attachment.name,
url,
mimeType: attachment.mimeType || '',
size: attachment.size || 0,
};
}));
}
async function save() {
if (saving.value) return;
if (!content.value.trim() && !attachments.value.length) {
uni.showToast({ title: '请填写备注内容或添加附件', icon: 'none' });
return;
}
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId || !customerId.value) {
uni.showToast({ title: '员工或档案信息缺失', icon: 'none' });
return;
}
saving.value = true;
try {
const doctor = doctorInfo.value || {};
const payload = {
corpId,
userId,
customerId: customerId.value,
content: content.value.trim(),
attachments: await getSubmitAttachments(),
userName: doctor.anotherName || doctor.name || '当前员工',
departmentName: doctor.departmentName || doctor.deptName || '',
};
const res = await api(isEditing.value ? 'updateEmployeeNote' : 'addEmployeeNote', {
...payload,
...(isEditing.value ? { noteId: noteId.value } : {}),
});
if (!res?.success) throw new Error(res?.message || '保存员工备注失败');
uni.$emit('employee-note-changed');
uni.showToast({ title: isEditing.value ? '员工备注已更新' : '员工备注已保存', icon: 'success' });
setTimeout(() => uni.navigateBack(), 500);
} catch (e) {
uni.showToast({ title: e?.message || '保存员工备注失败', icon: 'none' });
} finally {
saving.value = false;
}
}
async function remove() {
if (saving.value || !noteId.value) return;
const modal = await new Promise((resolve) => {
uni.showModal({
title: '删除备注',
content: '删除后将不再展示,确认删除这条员工备注吗?',
confirmText: '删除',
confirmColor: '#ef4444',
success: resolve,
fail: () => resolve({ confirm: false }),
});
});
if (!modal.confirm) return;
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) {
uni.showToast({ title: '员工信息缺失', icon: 'none' });
return;
}
saving.value = true;
try {
const res = await api('deleteEmployeeNote', { corpId, userId, noteId: noteId.value });
if (!res?.success) throw new Error(res?.message || '删除员工备注失败');
uni.$emit('employee-note-changed');
uni.showToast({ title: '员工备注已删除', icon: 'success' });
setTimeout(() => uni.navigateBack(), 500);
} catch (e) {
uni.showToast({ title: e?.message || '删除员工备注失败', icon: 'none' });
} finally {
saving.value = false;
}
}
function cancel() {
uni.navigateBack();
}
onLoad(async (options) => {
customerId.value = String(options?.customerId || '');
noteId.value = String(options?.noteId || '');
await getDoctorInfo();
if (!noteId.value) return;
const note = uni.getStorageSync(EDIT_STORAGE_KEY);
if (!note || String(note._id || '') !== noteId.value) {
uni.showToast({ title: '备注数据已失效,请返回后重试', icon: 'none' });
return;
}
content.value = note.content || '';
attachments.value = (Array.isArray(note.attachments) ? note.attachments : []).map(normalizeExistingAttachment);
uni.removeStorageSync(EDIT_STORAGE_KEY);
});
onUnload(() => {
if (recording.value) recorderManager?.stop();
clearRecordTimer();
audioPlayer?.stop();
audioPlayer?.destroy();
audioPlayer = null;
});
</script>
<style lang="scss" scoped>
.page { height: 100vh; display: flex; flex-direction: column; background: #f5f6f8; }
.page-scroll { flex: 1; min-height: 0; }
.editor-card, .attachment-card { padding: 28rpx; background: #fff; }
.attachment-card { margin-top: 20rpx; }
.editor { width: 100%; min-height: 280rpx; font-size: 30rpx; line-height: 44rpx; color: #333; }
.counter { font-size: 24rpx; color: #999; text-align: right; }
.section-title { display: block; font-size: 30rpx; font-weight: 600; color: #333; }
.section-label { margin-top: 24rpx; font-size: 26rpx; color: #666; }
.image-list { display: flex; flex-wrap: wrap; margin-top: 16rpx; }
.image-item { position: relative; width: 140rpx; height: 140rpx; margin: 0 16rpx 16rpx 0; }
.attachment-image { width: 140rpx; height: 140rpx; border-radius: 8rpx; }
.remove-icon { position: absolute; top: -12rpx; right: -12rpx; display: flex; align-items: center; justify-content: center; width: 36rpx; height: 36rpx; color: #fff; font-size: 30rpx; background: rgba(0, 0, 0, .55); border-radius: 50%; }
.add-image { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 136rpx; height: 136rpx; border: 2rpx dashed #cbd5e1; border-radius: 8rpx; color: #999; font-size: 22rpx; }
.audio-label { margin-top: 20rpx; }
.audio-item, .record-button { display: flex; align-items: center; justify-content: space-between; min-height: 76rpx; padding: 0 20rpx; margin-top: 16rpx; background: #f8fafc; border-radius: 8rpx; }
.audio-main { display: flex; align-items: center; min-width: 0; color: #0877F1; }
.audio-main text { max-width: 520rpx; margin-left: 12rpx; overflow: hidden; font-size: 26rpx; text-overflow: ellipsis; white-space: nowrap; }
.record-button { justify-content: center; color: #0877F1; border: 2rpx dashed #93c5fd; }
.record-button text { margin-left: 12rpx; font-size: 26rpx; }
.recording-text { color: #ef4444; }
.footer { display: flex; gap: 20rpx; padding: 22rpx 28rpx calc(22rpx + env(safe-area-inset-bottom)); background: #fff; border-top: 2rpx solid #eee; }
.delete-button, .cancel-button, .save-button { flex: 1; height: 84rpx; line-height: 84rpx; font-size: 30rpx; border-radius: 10rpx; }
.delete-button { color: #ef4444; background: #fff; border: 2rpx solid #ef4444; }
.cancel-button { color: #0877F1; background: #fff; border: 2rpx solid #0877F1; }
.save-button { color: #fff; background: #0877F1; }
</style>

View File

@ -0,0 +1,296 @@
<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)}&noteId=${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>

View File

@ -113,6 +113,11 @@ const urlsConfig = {
removeMedicalRecord: 'removeMedicalRecord', removeMedicalRecord: 'removeMedicalRecord',
getCustomerMedicalRecord: 'getCustomerMedicalRecord', getCustomerMedicalRecord: 'getCustomerMedicalRecord',
getMemberInfo: "getMemberInfo", getMemberInfo: "getMemberInfo",
getLatestEmployeeNote: 'getLatestEmployeeNote',
getEmployeeNotePage: 'getEmployeeNotePage',
addEmployeeNote: 'addEmployeeNote',
updateEmployeeNote: 'updateEmployeeNote',
deleteEmployeeNote: 'deleteEmployeeNote',
// 客户阶段 // 客户阶段
getCustomerType: 'getCustomerType', getCustomerType: 'getCustomerType',
}, },