diff --git a/.env.production b/.env.production index d08d18a..9161c39 100644 --- a/.env.production +++ b/.env.production @@ -2,4 +2,5 @@ MP_API_BASE_URL=https://ykt.youcan365.com MP_CACHE_PREFIX=production MP_WX_APP_ID=wx6ee11733526b4f04 MP_TIM_SDK_APP_ID=1600136080 -MP_CORP_ID=wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg \ No newline at end of file +MP_CORP_ID=wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg +MP_VERIFY_IM_CORP_ID=YES \ No newline at end of file diff --git a/App.vue b/App.vue index d9ca4e1..bddf8cd 100644 --- a/App.vue +++ b/App.vue @@ -196,6 +196,10 @@ page { padding-top: 10rpx; } +.pt-12 { + padding-top: 24rpx; +} + .pt-15 { padding-top: 30rpx; } @@ -251,6 +255,10 @@ page { margin-bottom: 20rpx; } +.mt-5 { + margin-top: 10rpx; +} + .mt-10 { margin-top: 20rpx; } diff --git a/components/form-template/form-cell/form-positive-find.vue b/components/form-template/form-cell/form-positive-find.vue new file mode 100644 index 0000000..ce04d01 --- /dev/null +++ b/components/form-template/form-cell/form-positive-find.vue @@ -0,0 +1,89 @@ + + + + \ No newline at end of file diff --git a/components/form-template/form-cell/form-upload.vue b/components/form-template/form-cell/form-upload.vue index 2fdcb00..02ade2e 100644 --- a/components/form-template/form-cell/form-upload.vue +++ b/components/form-template/form-cell/form-upload.vue @@ -6,11 +6,12 @@ + - + @@ -48,10 +49,73 @@ const files = computed(() => value.value.map(i => { url: i.url, name: i.name, type: i.type, - isImage: /image/i.test(i.type) + isImage: /image/i.test(i.type), + isPdf: /application\/pdf/i.test(i.type), } })) +function chooseType() { + uni.showActionSheet({ + itemList: ['图片', 'PDF'], + success: (res) => { + if (res.tapIndex === 0) { + addImage() + } else if (res.tapIndex === 1) { + addPdf() + } + } + }) +} + +function addPdf() { + wx.chooseMessageFile({ + count: 1, // 最多选择1个文件 + type: 'all', // 所有类型文件 + success: async (res) => { + const file = res.tempFiles[0]; + const { path, name, size } = file; + const type = checkFileValid(name, size); + // 检查文件类型和大小 + if (!type) return; + loading(); + const result = await upload(path); + hideLoading(); + if (result) { + change([...value.value, { url: result, type }]) + } else { + toast('上传失败') + } + }, + fail: (err) => { + if (/cancel/i.test(err.errMsg)) { + // toast('用户取消选择文件') + } else { + toast('上传失败') + } + } + }) +} + +function checkFileValid(fileName, fileSize) { + // 获取文件扩展名 + const ext = fileName.split('.').pop().toLowerCase(); + // 文件大小限制 (10MB) + const maxSize = 10 * 1024 * 1024; + if (fileSize > maxSize) { + toast('文件大小不能超过10MB') + return false; + } + + if (['jpg', 'jpeg', 'png'].includes(ext)) { + return 'image/png' + } + if (ext === 'pdf') { + return 'application/pdf' + } + toast('仅支持图片或PDF') + return false; +} + function addImage() { uni.chooseImage({ count: 1, @@ -64,6 +128,13 @@ function addImage() { } else { toast('上传失败') } + }, + fail: (err) => { + if (/cancel/i.test(err.errMsg)) { + // toast('用户取消选择文件') + } else { + toast('上传失败') + } } }) } diff --git a/components/form-template/form-cell/index.vue b/components/form-template/form-cell/index.vue index e00418d..d4b9b87 100644 --- a/components/form-template/form-cell/index.vue +++ b/components/form-template/form-cell/index.vue @@ -1,5 +1,5 @@ @@ -190,18 +139,28 @@ import ChatInput from "./components/chat-input.vue"; import SystemMessage from "./components/system-message.vue"; import ConsultCancel from "./components/consult-cancel.vue"; import ConsultApply from "./components/consult-apply.vue"; +import { + checkConversationSubscribeEntryVisible, + requestConversationSubscribeMessage, +} from "@/utils/subscribe-message"; +import { + SUBSCRIBE_MESSAGE_ROLE, + SUBSCRIBE_MESSAGE_SCENE, +} from "@/utils/subscribe-message-config"; const timChatManager = globalTimChatManager; // corpId 从群组信息中获取 const corpId = ref(""); +const showSubscribeEntry = ref(false); // 获取登录状态 const { account, openid, isIMInitialized } = storeToRefs(useAccountStore()); -const { initIMAfterLogin } = useAccountStore(); +const { initIMAfterLogin, login } = useAccountStore(); // 聊天输入组件引用 const chatInputRef = ref(null); +const loginPromise = ref(null); const groupId = ref(""); const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId); @@ -445,7 +404,10 @@ function getBubbleClass(message) { // 页面加载 onLoad(async (options) => { - groupId.value = options.groupID || ""; + loginPromise.value = login(); + await loginPromise.value; + loginPromise.value = null; + groupId.value = decodeURIComponent(options.groupID) || ""; messageList.value = []; isLoading.value = false; if (options.conversationID) { @@ -517,7 +479,7 @@ const initTIMCallbacks = async () => { loadMessageList(); } }); - timChatManager.setCallback("onSDKNotReady", () => {}); + timChatManager.setCallback("onSDKNotReady", () => { }); timChatManager.setCallback("onMessageReceived", (message) => { console.log("页面收到消息:", { @@ -856,7 +818,12 @@ const handleScrollToUpper = async () => { }; // 页面显示 -onShow(() => { +onShow(async () => { + if (loginPromise.value) { + await loginPromise.value; + } + + loadSubscribeEntryState(); if (!account.value || !openid.value) { uni.redirectTo({ url: "/pages/login/login", @@ -908,6 +875,14 @@ onShow(() => { } }); +watch( + () => corpId.value, + () => { + loadSubscribeEntryState(); + }, + { immediate: true } +); + // 页面隐藏 onHide(() => { stopIMMonitoring(); @@ -1063,6 +1038,31 @@ const handleApplyConsult = async () => { } }; +const handleSubscribeReminder = async () => { + await requestConversationSubscribeMessage({ + role: SUBSCRIBE_MESSAGE_ROLE.PATIENT, + scene: SUBSCRIBE_MESSAGE_SCENE.CHAT, + conversationId: chatInfo.value.conversationID || "", + groupId: groupId.value || "", + corpId: corpId.value || "", + patientId: patientId.value || "", + userId: openid.value || account.value?.openid || "", + openid: openid.value || account.value?.openid || "", + unionid: account.value?.unionid || "", + extraData: { + orderStatus: orderStatus.value || "", + page: "pages/message/index", + }, + }); +}; + +const loadSubscribeEntryState = async () => { + showSubscribeEntry.value = await checkConversationSubscribeEntryVisible( + corpId.value || "", + true + ); +}; + // 页面卸载 onUnmounted(() => { clearMessageCache(); diff --git a/pages/message/message.vue b/pages/message/message.vue index 0617fb8..f0389f6 100644 --- a/pages/message/message.vue +++ b/pages/message/message.vue @@ -1,6 +1,5 @@ @@ -65,9 +72,19 @@ import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.j import { globalUnreadListenerManager } from "@/utils/global-unread-listener.js"; import useGroupAvatars from "./hooks/use-group-avatars.js"; import GroupAvatar from "@/components/group-avatar.vue"; +import { + checkConversationSubscribeEntryVisible, + requestConversationSubscribeMessage, +} from "@/utils/subscribe-message"; +import { + SUBSCRIBE_MESSAGE_ROLE, + SUBSCRIBE_MESSAGE_SCENE, +} from "@/utils/subscribe-message-config"; // 获取登录状态 -const { account, openid, isIMInitialized, hasImCorpId } = storeToRefs(useAccountStore()); +const { account, openid, isIMInitialized, hasImCorpId, teams } = storeToRefs( + useAccountStore() +); const { initIMAfterLogin } = useAccountStore(); // 状态 @@ -76,6 +93,7 @@ const loading = ref(false); const loadingMore = ref(false); const hasMore = ref(false); const refreshing = ref(false); +const showSubscribeEntry = ref(false); // 群聊头像管理 const { loadGroupAvatars, getAvatarList } = useGroupAvatars(); @@ -492,14 +510,45 @@ const cleanMessageText = (text) => { return text.replace(/[\r\n]+/g, " ").trim(); }; +const handleSubscribeReminder = async () => { + await requestConversationSubscribeMessage({ + role: SUBSCRIBE_MESSAGE_ROLE.PATIENT, + scene: SUBSCRIBE_MESSAGE_SCENE.LIST, + corpId: teams.value.find((item) => item?.corpId)?.corpId || "", + userId: openid.value || account.value?.openid || "", + openid: openid.value || account.value?.openid || "", + unionid: account.value?.unionid || "", + extraData: { + page: "pages/message/message", + }, + }); +}; + +const loadSubscribeEntryState = async () => { + const currentCorpId = teams.value.find((item) => item?.corpId)?.corpId || ""; + showSubscribeEntry.value = await checkConversationSubscribeEntryVisible( + currentCorpId, + true + ); +}; + // 页面显示 onShow(async () => { // 页面显示时刷新 tabBar 徽章 // if (globalUnreadListenerManager.isInitialized) { // await globalUnreadListenerManager.refreshBadge(); // } + await loadSubscribeEntryState(); }); +watch( + () => teams.value.map((item) => item?.corpId).join(","), + () => { + loadSubscribeEntryState(); + }, + { immediate: true } +); + // 页面隐藏 onHide(() => { console.log("【消息列表页】页面隐藏"); @@ -718,4 +767,32 @@ onUnmounted(() => { color: #999; padding-bottom: 10rpx; } + +.subscribe-entry { + position: fixed; + right: 32rpx; + bottom: 180rpx; + width: 116rpx; + height: 116rpx; + border-radius: 58rpx; + background: #fff; + border: 2rpx solid #3876f6; + box-shadow: 0 8rpx 24rpx rgba(56, 118, 246, 0.16); + display: flex; + align-items: center; + justify-content: center; + z-index: 20; +} + +.subscribe-entry:active { + opacity: 0.85; +} + +.subscribe-entry-text { + width: 56rpx; + font-size: 24rpx; + line-height: 1.4; + color: #3876f6; + text-align: center; +} diff --git a/pages/survey/components/survey-question.vue b/pages/survey/components/survey-question.vue index 13d37be..c6a497e 100644 --- a/pages/survey/components/survey-question.vue +++ b/pages/survey/components/survey-question.vue @@ -56,11 +56,13 @@ - - 提交 + + + 提交 + + diff --git a/static/pdf.svg b/static/pdf.svg new file mode 100644 index 0000000..0cbf510 --- /dev/null +++ b/static/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/store/account.js b/store/account.js index e567b5b..a009629 100644 --- a/store/account.js +++ b/store/account.js @@ -14,11 +14,20 @@ export default defineStore("accountStore", () => { const openid = ref(""); const externalUserId = ref(''); const teams = ref([]); - const hasImCorpId = computed(() => teams.value.some(i => i.corpId === 'wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg')); + const hasImCorpId = computed(() => { + // 正式环境IM账号优先 暂时只有wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg机构的才开启IM账号 + if (env.MP_VERIFY_IM_CORP_ID === 'YES') { + return teams.value.some(i => i.corpId === 'wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg') + } + return true + }); const teamsPromise = ref(null); async function login(phoneCode = '') { if (loading.value) return; + if (account.value && account.value.mobile) { + return account.value + } loading.value = true; try { const { code } = await uni.login({ diff --git a/utils/api.js b/utils/api.js index cf003d4..62475ab 100644 --- a/utils/api.js +++ b/utils/api.js @@ -73,7 +73,10 @@ const urlsConfig = { getGroupListByGroupId: "getGroupListByGroupId", createConsultGroup: "createConsultGroup", cancelConsultApplication: "cancelConsultApplication", - getGroupList: "getGroupList" + getGroupList: "getGroupList", + getConversationSubscribeConfig: "getConversationSubscribeConfig", + saveConversationSubscribeResult: "saveConversationSubscribeResult", + sendConversationSubscribeEvent: "sendConversationSubscribeEvent" }, survery: { getMiniAppReceivedSurveryList: 'getMiniAppReceivedSurveryList', diff --git a/utils/subscribe-message-config.js b/utils/subscribe-message-config.js new file mode 100644 index 0000000..df935c9 --- /dev/null +++ b/utils/subscribe-message-config.js @@ -0,0 +1,64 @@ +const env = __VITE_ENV__; + +export const SUBSCRIBE_MESSAGE_ROLE = { + PATIENT: "patient", + DOCTOR: "doctor", +}; + +export const SUBSCRIBE_MESSAGE_SCENE = { + DEFAULT: "default", + LIST: "list", + CHAT: "chat", +}; + +export const SUBSCRIBE_MESSAGE_EVENT = { + PATIENT_CONSULT_APPLY: "patient_consult_apply", + PATIENT_CHAT_MESSAGE: "patient_chat_message", + DOCTOR_ACCEPT: "doctor_accept", + DOCTOR_REJECT: "doctor_reject", + DOCTOR_CHAT_MESSAGE: "doctor_chat_message", +}; + +export const SUBSCRIBE_MESSAGE_TEMPLATES = { + consultationReply: { + code: "consultationReply", + role: SUBSCRIBE_MESSAGE_ROLE.PATIENT, + id: + env.MP_SUBSCRIBE_TEMPLATE_CONSULT_REPLY || + "VF9AC-7Rr3E1drbxBCrxbC-rLTnidmlNXopKReSAd_w", + name: "咨询回复通知", + events: [ + SUBSCRIBE_MESSAGE_EVENT.DOCTOR_ACCEPT, + SUBSCRIBE_MESSAGE_EVENT.DOCTOR_REJECT, + SUBSCRIBE_MESSAGE_EVENT.DOCTOR_CHAT_MESSAGE, + ], + fields: ["患者姓名", "回复时间", "回复者", "所属机构"], + }, +}; + +export const SUBSCRIBE_MESSAGE_SCENE_TEMPLATE_MAP = { + [SUBSCRIBE_MESSAGE_ROLE.PATIENT]: { + [SUBSCRIBE_MESSAGE_SCENE.DEFAULT]: ["consultationReply"], + [SUBSCRIBE_MESSAGE_SCENE.LIST]: ["consultationReply"], + [SUBSCRIBE_MESSAGE_SCENE.CHAT]: ["consultationReply"], + }, +}; + +export function resolveSubscribeTemplates({ + role, + scene = SUBSCRIBE_MESSAGE_SCENE.DEFAULT, +} = {}) { + const roleMap = SUBSCRIBE_MESSAGE_SCENE_TEMPLATE_MAP[role] || {}; + const keys = + roleMap[scene] || roleMap[SUBSCRIBE_MESSAGE_SCENE.DEFAULT] || []; + const seen = new Set(); + + return keys + .map((key) => SUBSCRIBE_MESSAGE_TEMPLATES[key]) + .filter((item) => item && item.id) + .filter((item) => { + if (seen.has(item.id)) return false; + seen.add(item.id); + return true; + }); +} diff --git a/utils/subscribe-message.js b/utils/subscribe-message.js new file mode 100644 index 0000000..f3091e1 --- /dev/null +++ b/utils/subscribe-message.js @@ -0,0 +1,222 @@ +import api from "@/utils/api"; +import { toast } from "@/utils/widget"; +import { resolveSubscribeTemplates } from "./subscribe-message-config"; + +const SUBSCRIBE_ACCEPT_STATUS = "accept"; +const SUBSCRIBE_REJECT_STATUS = "reject"; +const SUBSCRIBE_BAN_STATUS = "ban"; +const SUBSCRIBE_FILTER_STATUS = "filter"; +const SUBSCRIBE_CANCEL_STATUS = "cancel"; +const SUBSCRIBE_FAILED_STATUS = "failed"; +const subscribeDisplayConfigCache = new Map(); + +function canUseSubscribeMessage() { + return ( + typeof wx !== "undefined" && + typeof wx.requestSubscribeMessage === "function" + ); +} + +function requestSubscribeMessage(tmplIds = []) { + return new Promise((resolve) => { + wx.requestSubscribeMessage({ + tmplIds, + success(res) { + resolve({ ok: true, res }); + }, + fail(err) { + resolve({ ok: false, err }); + }, + }); + }); +} + +function normalizeFailStatus(err = {}) { + const errCode = Number(err.errCode || 0); + const errMsg = String(err.errMsg || "").toLowerCase(); + + if (errCode === 20004 || errMsg.includes("main switch")) { + return SUBSCRIBE_BAN_STATUS; + } + if (errCode === 20005 || errMsg.includes("ban")) { + return SUBSCRIBE_BAN_STATUS; + } + if (errMsg.includes("filter")) { + return SUBSCRIBE_FILTER_STATUS; + } + if (errMsg.includes("cancel")) { + return SUBSCRIBE_CANCEL_STATUS; + } + return SUBSCRIBE_FAILED_STATUS; +} + +function buildTemplateResultRecords(templates = [], requestResult = {}, context = {}) { + const requestedAt = Date.now(); + + if (requestResult.ok) { + const res = requestResult.res || {}; + return templates.map((template) => ({ + role: context.role || "", + scene: context.scene || "", + conversationId: context.conversationId || "", + groupId: context.groupId || "", + corpId: context.corpId || "", + teamId: context.teamId || "", + patientId: context.patientId || "", + doctorId: context.doctorId || "", + userId: context.userId || "", + openid: context.openid || "", + unionid: context.unionid || "", + templateId: template.id, + templateCode: template.code, + templateName: template.name, + eventTypes: template.events, + status: String(res[template.id] || SUBSCRIBE_FAILED_STATUS), + rawResult: res, + requestedAt, + extraData: context.extraData || {}, + })); + } + + const status = normalizeFailStatus(requestResult.err); + return templates.map((template) => ({ + role: context.role || "", + scene: context.scene || "", + conversationId: context.conversationId || "", + groupId: context.groupId || "", + corpId: context.corpId || "", + teamId: context.teamId || "", + patientId: context.patientId || "", + doctorId: context.doctorId || "", + userId: context.userId || "", + openid: context.openid || "", + unionid: context.unionid || "", + templateId: template.id, + templateCode: template.code, + templateName: template.name, + eventTypes: template.events, + status, + rawResult: requestResult.err || {}, + requestedAt, + extraData: context.extraData || {}, + })); +} + +function buildToastMessage(records = [], reportResult = { success: false }) { + const accepted = records.some((item) => item.status === SUBSCRIBE_ACCEPT_STATUS); + + if (accepted && reportResult?.success === false) { + return reportResult?.message || "提醒开启失败,请稍后再试"; + } + + if (records.some((item) => item.status === SUBSCRIBE_ACCEPT_STATUS)) { + return "会话消息提醒开启"; + } + if (records.some((item) => item.status === SUBSCRIBE_BAN_STATUS)) { + return "请先在微信设置中开启订阅消息提醒"; + } + if (records.some((item) => item.status === SUBSCRIBE_FILTER_STATUS)) { + return "当前提醒模板暂不可用"; + } + if (records.some((item) => item.status === SUBSCRIBE_REJECT_STATUS)) { + return "你已拒绝本次提醒订阅"; + } + if (records.some((item) => item.status === SUBSCRIBE_CANCEL_STATUS)) { + return "你已取消本次提醒订阅"; + } + return "提醒订阅请求失败,请稍后再试"; +} + +async function reportSubscribeResult(records = []) { + if (!records.length) return { success: false }; + try { + return await api( + "saveConversationSubscribeResult", + { + records, + }, + false + ); + } catch (error) { + console.error("保存订阅结果失败:", error); + return { success: false, message: error?.message || "保存失败" }; + } +} + +export async function checkConversationSubscribeEntryVisible( + corpId = "", + forceRefresh = false +) { + const normalizedCorpId = String(corpId || "").trim(); + if (!normalizedCorpId) return false; + + if (!forceRefresh && subscribeDisplayConfigCache.has(normalizedCorpId)) { + return subscribeDisplayConfigCache.get(normalizedCorpId); + } + + try { + const result = await api( + "getConversationSubscribeConfig", + { corpId: normalizedCorpId }, + false + ); + const enabled = !!result?.data?.enabled; + subscribeDisplayConfigCache.set(normalizedCorpId, enabled); + return enabled; + } catch (error) { + console.error("获取订阅提醒显示配置失败:", error); + subscribeDisplayConfigCache.set(normalizedCorpId, false); + return false; + } +} + +export async function requestConversationSubscribeMessage(context = {}) { + const templates = resolveSubscribeTemplates({ + role: context.role, + scene: context.scene, + }); + const requestTemplates = templates.slice(0, 1); + + if (!requestTemplates.length) { + await toast("暂未配置提醒模板"); + return { + success: false, + code: "template_missing", + records: [], + }; + } + + if (!canUseSubscribeMessage()) { + await toast("当前微信版本不支持订阅消息"); + return { + success: false, + code: "unsupported", + records: [], + }; + } + + const requestResult = await requestSubscribeMessage( + requestTemplates.map((item) => item.id) + ); + const records = buildTemplateResultRecords( + requestTemplates, + requestResult, + context + ); + + const reportResult = await reportSubscribeResult(records); + await toast(buildToastMessage(records, reportResult)); + + const subscribeSuccess = + records.some((item) => item.status === SUBSCRIBE_ACCEPT_STATUS) && + reportResult?.success !== false; + + return { + success: subscribeSuccess, + reportResult, + records, + acceptedTemplateIds: records + .filter((item) => item.status === SUBSCRIBE_ACCEPT_STATUS) + .map((item) => item.templateId), + }; +}