Merge remote-tracking branch 'origin/fixbug-260920' into dev-bug-1012
This commit is contained in:
commit
87da5c69e6
2
.env.ip
2
.env.ip
@ -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_CACHE_PREFIX=development
|
||||
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<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>
|
||||
<script setup>
|
||||
|
||||
16
pages.json
16
pages.json
@ -268,6 +268,20 @@
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "employee-note-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "员工备注",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "employee-note-edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "员工备注",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "archive-edit",
|
||||
"style": {
|
||||
@ -395,4 +409,4 @@
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,18 +54,17 @@
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="info-block border-bottom">
|
||||
<view class="info-block border-bottom" @click="openEmployeeNotes">
|
||||
<view class="block-header">
|
||||
<text class="input-label">备注</text>
|
||||
<view class="edit-icon" @click.stop="openNotes">
|
||||
<uni-icons type="compose" size="20" color="#0877F1" />
|
||||
<view class="edit-icon">
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="block-content">
|
||||
<text v-if="archive.notes" class="note-content">{{ archive.notes }}</text>
|
||||
<view v-else class="empty-click" @click="openNotes">
|
||||
<text class="placeholder">添加备注</text>
|
||||
</view>
|
||||
<view class="block-content employee-note-summary" :class="{ 'employee-note-summary--active': latestEmployeeNote }">
|
||||
<uni-icons v-if="hasEmployeeNoteImage" type="image-filled" size="18" color="#0877F1" />
|
||||
<uni-icons v-if="hasEmployeeNoteAudio" type="sound-filled" size="18" color="#0877F1" />
|
||||
<text class="note-content">{{ latestEmployeeNoteText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -158,33 +157,6 @@
|
||||
</view>
|
||||
</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">
|
||||
<view class="sheet">
|
||||
@ -350,6 +322,7 @@ function normalizeArchiveFromApi(raw) {
|
||||
createTime: r.createTime || '',
|
||||
creator: r.creator || '',
|
||||
notes: r.notes || r.remark || '',
|
||||
latestEmployeeNote: r.latestEmployeeNote || null,
|
||||
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
|
||||
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 };
|
||||
await fetchLatestEmployeeNote();
|
||||
saveToStorage();
|
||||
loadTeamMembers();
|
||||
await fetchTeamGroups(true);
|
||||
@ -618,6 +592,37 @@ const createText = computed(() => {
|
||||
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 currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || ''));
|
||||
const showGoChat = computed(() => Boolean(currentChatGroupId.value));
|
||||
@ -810,24 +815,6 @@ const saveContact = () => {
|
||||
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 addGroupPopup = ref(null);
|
||||
@ -1136,6 +1123,17 @@ const saveAddGroup = async () => {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
338
pages/case/employee-note-edit.vue
Normal file
338
pages/case/employee-note-edit.vue
Normal 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>
|
||||
296
pages/case/employee-note-list.vue
Normal file
296
pages/case/employee-note-list.vue
Normal 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)}¬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>
|
||||
@ -113,6 +113,11 @@ const urlsConfig = {
|
||||
removeMedicalRecord: 'removeMedicalRecord',
|
||||
getCustomerMedicalRecord: 'getCustomerMedicalRecord',
|
||||
getMemberInfo: "getMemberInfo",
|
||||
getLatestEmployeeNote: 'getLatestEmployeeNote',
|
||||
getEmployeeNotePage: 'getEmployeeNotePage',
|
||||
addEmployeeNote: 'addEmployeeNote',
|
||||
updateEmployeeNote: 'updateEmployeeNote',
|
||||
deleteEmployeeNote: 'deleteEmployeeNote',
|
||||
// 客户阶段
|
||||
getCustomerType: 'getCustomerType',
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user