feat: 员工备注调整
This commit is contained in:
parent
4ef5949f5b
commit
eb6f5a401b
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_IMAGE_URL=https://patient.youcan365.com
|
||||||
MP_CACHE_PREFIX=development
|
MP_CACHE_PREFIX=development
|
||||||
MP_WX_APP_ID=wx1d8337a40c11d66c
|
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
14
pages.json
14
pages.json
@ -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": {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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',
|
removeMedicalRecord: 'removeMedicalRecord',
|
||||||
getCustomerMedicalRecord: 'getCustomerMedicalRecord',
|
getCustomerMedicalRecord: 'getCustomerMedicalRecord',
|
||||||
getMemberInfo: "getMemberInfo",
|
getMemberInfo: "getMemberInfo",
|
||||||
|
getLatestEmployeeNote: 'getLatestEmployeeNote',
|
||||||
|
getEmployeeNotePage: 'getEmployeeNotePage',
|
||||||
|
addEmployeeNote: 'addEmployeeNote',
|
||||||
|
updateEmployeeNote: 'updateEmployeeNote',
|
||||||
|
deleteEmployeeNote: 'deleteEmployeeNote',
|
||||||
// 客户阶段
|
// 客户阶段
|
||||||
getCustomerType: 'getCustomerType',
|
getCustomerType: 'getCustomerType',
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user