Merge remote-tracking branch 'origin/dev-wdb' into dev-hjf

This commit is contained in:
Jafeng 2026-01-27 17:53:58 +08:00
commit 16b179c147
52 changed files with 4752 additions and 804 deletions

View File

@ -1,4 +1,5 @@
MP_API_BASE_URL=http://192.168.137.1:8080 MP_API_BASE_URL=http://localhost:8080
MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb

View File

@ -1,4 +1,5 @@
MP_API_BASE_URL=http://localhost:8080 MP_API_BASE_URL=http://localhost:8080
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600072268 MP_TIM_SDK_APP_ID=1600072268

View File

@ -48,6 +48,9 @@ page {
.relative { .relative {
position: relative; position: relative;
} }
.absolute{
position: absolute;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;

View File

@ -1,5 +1,5 @@
<template> <template>
<view v-if="showCancel || showConfirm" class="relative px-15 py-12 bg-white text-center" <view v-if="showCancel || showConfirm" class="relative flex px-15 py-12 bg-white text-center"
:class="hidedenShadow ? '' : 'shadow-up'"> :class="hidedenShadow ? '' : 'shadow-up'">
<view v-if="showCancel" class="flex-grow py-10 text-base border-primary rounded text-primary rounded" <view v-if="showCancel" class="flex-grow py-10 text-base border-primary rounded text-primary rounded"
@click="cancel()"> @click="cancel()">

View File

@ -49,6 +49,7 @@ const displayRange = computed(() => {
} }
return props.range; return props.range;
}) })
const value = computed(() => { const value = computed(() => {
if (!props.form) return ''; if (!props.form) return '';
const currentValue = props.form[props.title]; const currentValue = props.form[props.title];
@ -65,7 +66,7 @@ function change(e) {
const selectedValue = props.range[e.detail.value]; const selectedValue = props.range[e.detail.value];
emits('change', { emits('change', {
title: props.title, title: props.title,
value: selectedValue value: typeof selectedValue === 'object' ? selectedValue.value : selectedValue
}) })
} }
</script> </script>

View File

@ -5,14 +5,15 @@
</view> </view>
<view class="mt-10"> <view class="mt-10">
<textarea <textarea
:auto-height="autoHeight"
:disabled="disableChange" :disabled="disableChange"
:value="value" :value="value"
class="form-textarea" class="form-textarea"
:class="border ? 'form-textarea--border' : ''"
:style="textareaStyle" :style="textareaStyle"
:placeholder="placeholder" :placeholder="placeholder"
placeholder-class="form__placeholder" placeholder-class="form__placeholder"
:maxlength="wordLimit" :maxlength="wordLimit"
:auto-height="autoHeight"
@input="change($event)" /> @input="change($event)" />
<view v-if="wordLimit > 0" class="form-textarea__count"> <view v-if="wordLimit > 0" class="form-textarea__count">
{{ value && value.length ? value.length : 0 }} / {{ wordLimit }} {{ value && value.length ? value.length : 0 }} / {{ wordLimit }}
@ -25,6 +26,10 @@ import { computed } from 'vue';
const emits = defineEmits(['change']); const emits = defineEmits(['change']);
const props = defineProps({ const props = defineProps({
border: {
type: Boolean,
default: true
},
form: { form: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
@ -98,12 +103,15 @@ function change(e) {
.form-textarea { .form-textarea {
width: 100%; width: 100%;
font-size: 28rpx; font-size: 28rpx;
border: 1px solid #eee;
padding: 20rpx; padding: 20rpx;
border-radius: 8rpx; border-radius: 8rpx;
box-sizing: border-box; box-sizing: border-box;
} }
.form-textarea--border {
border: 1px solid #eee;
}
.form-textarea__count { .form-textarea__count {
padding-top: 20rpx; padding-top: 20rpx;
text-align: right; text-align: right;

View File

@ -1,9 +1,9 @@
<template> <template>
<view class="full-page" :style="pageStyle"> <view class="full-page" :class="pageClass" :style="pageStyle">
<view v-if="hasHeader" class="page-header"> <view v-if="hasHeader" class="page-header">
<slot name="header"></slot> <slot name="header"></slot>
</view> </view>
<view class="page-main" :style="mainStyle"> <view class="page-main" :class="mainClass" :style="mainStyle">
<view v-if="customScroll" class="page-scroll"> <view v-if="customScroll" class="page-scroll">
<slot></slot> <slot></slot>
</view> </view>
@ -16,7 +16,7 @@
<slot name="footer"></slot> <slot name="footer"></slot>
</view> </view>
<!-- #ifdef MP--> <!-- #ifdef MP-->
<view class="safeareaBottom"></view> <view v-if="showSafeArea" class="safeareaBottom"></view>
<!-- #endif --> <!-- #endif -->
</view> </view>
</template> </template>
@ -27,8 +27,11 @@ import useDebounce from '@/utils/useDebounce';
const emits = defineEmits(['reachBottom']); const emits = defineEmits(['reachBottom']);
const props = defineProps({ const props = defineProps({
customScroll: { type: Boolean, default: false }, customScroll: { type: Boolean, default: false },
mainClass: { type: String, default: '' },
mainStyle: { default: '' }, mainStyle: { default: '' },
pageStyle: { default: '' } pageClass: { type: String, default: '' },
pageStyle: { default: '' },
showSafeArea: { type: Boolean, default: true }
}); });
const slots = useSlots(); const slots = useSlots();
const hasHeader = computed(() => !!slots.header); const hasHeader = computed(() => !!slots.header);

View File

@ -39,8 +39,10 @@ export default function useGuard() {
async function triggleShowEvents() { async function triggleShowEvents() {
await promise; await promise;
if (account.value && account.value.openid) {
onShowEvents.value.forEach(fn => fn(onShowOptions.value)) onShowEvents.value.forEach(fn => fn(onShowOptions.value))
} }
}
function useShow(fn) { function useShow(fn) {
onShowEvents.value.push(fn) onShowEvents.value.push(fn)
@ -53,8 +55,8 @@ export default function useGuard() {
const route = routes.find(i => page && i.path === page.route); const route = routes.find(i => page && i.path === page.route);
const requireLogin = route && route.meta && route.meta.login; const requireLogin = route && route.meta && route.meta.login;
if (requireLogin && !account.value) { if (requireLogin && !account.value) {
const res = await login() await login()
if (res) { if (account.value) {
resolve() resolve()
} else { } else {
return toLoginPage(opts, page.route); return toLoginPage(opts, page.route);
@ -64,7 +66,6 @@ export default function useGuard() {
}) })
onShow(opts => { onShow(opts => {
console.log('onShow')
onShowOptions.value = { ...opts }; onShowOptions.value = { ...opts };
triggleShowEvents() triggleShowEvents()
}) })

View File

@ -27,9 +27,21 @@
} }
}, },
{ {
"path": "pages/case/case", "path": "pages/message/common-phrases",
"style": { "style": {
"navigationBarTitleText": "病例" "navigationBarTitleText": "常用语"
}
},
{
"path": "pages/message/article-list",
"style": {
"navigationBarTitleText": "宣教文章"
}
},
{
"path": "pages/message/survey-list",
"style": {
"navigationBarTitleText": "问卷列表"
} }
}, },
{ {
@ -158,6 +170,12 @@
"navigationBarTitleText": "工作台" "navigationBarTitleText": "工作台"
} }
}, },
{
"path": "pages/case/case",
"style": {
"navigationBarTitleText": "病例"
}
},
{ {
"path": "pages/work/profile", "path": "pages/work/profile",
"style": { "style": {
@ -169,6 +187,24 @@
"style": { "style": {
"navigationBarTitleText": "选择科室" "navigationBarTitleText": "选择科室"
} }
},
{
"path": "pages/work/verify/assistant",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/work/verify/doctor",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@ -1,27 +1,11 @@
<template> <template>
<view v-if="team" class="pt-lg px-15 flex flex-col items-center text-center"> <view class="pt-lg px-15 flex flex-col items-center text-center">
<group-avatar :avatarList="team.avatars" />
<view class="mt-15 text-base font-semibold text-dark">{{
team.teamName
}}</view>
<view class="mt-12 text-sm text-gray">{{ team.corpName }}</view>
<view class="mt-15 text-lg text-dark font-semibold"
>为您提供团队个性化专属服务</view
>
</view>
<view v-else class="pt-lg px-15 flex flex-col items-center text-center">
<image src="/static/logo-plain.png" class="logo"></image> <image src="/static/logo-plain.png" class="logo"></image>
<view class="mt-15 text-xl font-semibold text-dark">柚健康</view> <view class="mt-15 text-xl font-semibold text-dark">柚健康</view>
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view> <view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view> </view>
<view class="login-btn-wrap"> <view class="login-btn-wrap">
<button <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
v-if="checked"
class="login-btn"
type="primary"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
手机号快捷登录 手机号快捷登录
</button> </button>
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()"> <!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
@ -31,10 +15,7 @@
手机号快捷登录 手机号快捷登录
</button> </button>
</view> </view>
<view <view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked">
class="flex items-center justify-center mt-12 px-15"
@click="checked = !checked"
>
<checkbox :checked="checked" style="transform: scale(0.7)" /> <checkbox :checked="checked" style="transform: scale(0.7)" />
<view class="text-sm text-gray">我已阅读并同意</view> <view class="text-sm text-gray">我已阅读并同意</view>
<view class="text-sm text-primary">用户协议</view> <view class="text-sm text-primary">用户协议</view>
@ -44,16 +25,16 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { get } from "@/utils/cache"; import { get } from "@/utils/cache";
import { toast } from "@/utils/widget"; import { toast } from "@/utils/widget";
import groupAvatar from "@/components/group-avatar.vue";
const team = ref(true); const team = ref(true);
const checked = ref(false); const checked = ref(false);
const redirectUrl = ref(""); const redirectUrl = ref("");
const { doctorInfo } = storeToRefs(useAccountStore());
const { login } = useAccountStore(); const { login } = useAccountStore();
function attempRedirect(url) { function attempRedirect(url) {
@ -82,7 +63,7 @@ function remind() {
function toHome() { function toHome() {
uni.switchTab({ uni.switchTab({
url: "/pages/message/message", url: "/pages/work/work",
}); });
} }
@ -92,6 +73,10 @@ async function getPhoneNumber(e) {
const res = await login(phoneCode); const res = await login(phoneCode);
if (res && redirectUrl.value) { if (res && redirectUrl.value) {
await attempToPage(redirectUrl.value); await attempToPage(redirectUrl.value);
} else if (res && !(doctorInfo.value && doctorInfo.value.anotherName)) {
uni.redirectTo({
url: '/pages/work/profile'
})
} else if (res) { } else if (res) {
toHome(); toHome();
} }
@ -109,7 +94,6 @@ onLoad((opts) => {
if (opts.source === "teamInvite") { if (opts.source === "teamInvite") {
team.value = get("invite-team-info"); team.value = get("invite-team-info");
redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`; redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
console.log("redirectUrl", redirectUrl.value);
return; return;
} }
if (opts.redirect) { if (opts.redirect) {

View File

@ -0,0 +1,503 @@
<template>
<view class="article-page">
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input
class="search-input"
v-model="searchTitle"
placeholder="输入内容名称搜索"
@input="handleSearch"
/>
</view>
</view>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === (cate._id || 'all') }"
@click="selectCategory(cate)"
>
{{ cate.name }}
</view>
</scroll-view>
</view>
<view class="article-list">
<scroll-view
scroll-y
class="article-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
>
<view
v-if="loading && articleList.length === 0"
class="loading-container"
>
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="articleList.length === 0" class="empty-container">
<empty-data title="暂无文章" />
</view>
<view v-else>
<view
v-for="article in articleList"
:key="article._id"
class="article-item"
>
<view class="article-content" @click="previewArticle(article)">
<text class="article-title">{{ article.title }}</text>
<text class="article-date">创建时间{{ article.date }}</text>
</view>
<view class="article-action">
<button
class="send-btn"
size="mini"
type="primary"
@click="sendArticle(article)"
>
发送
</button>
</view>
</view>
<view v-if="loading && articleList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view
v-if="!loading && articleList.length >= total"
class="no-more"
>
没有更多了
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 文章预览弹窗 -->
<uni-popup ref="previewPopup" type="bottom" :safe-area="false">
<view class="preview-container">
<view class="preview-header">
<text class="preview-title">{{ previewArticleData.title }}</text>
<view class="preview-close" @click="closePreview">
<uni-icons type="closeempty" size="24" color="#333" />
</view>
</view>
<scroll-view scroll-y class="preview-content">
<rich-text :nodes="previewArticleData.content"></rich-text>
</scroll-view>
<view class="preview-footer">
<button class="preview-close-btn" @click="closePreview">关闭</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getArticleCateList, getArticleList, getArticle } from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const accountStore = useAccountStore();
const corpId = env.MP_CORP_ID;
const userId = ref("");
//
const searchTitle = ref("");
let searchTimer = null;
//
const categoryList = ref([{ _id: "", name: "全部" }]);
const currentCateId = ref("");
//
const articleList = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
//
const previewArticleData = ref({
title: "",
content: "",
});
const previewPopup = ref(null);
//
const getCategoryList = async () => {
try {
const res = await getArticleCateList({ corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", name: "全部" }, ...cates];
}
} catch (error) {
console.error("获取分类列表失败:", error);
}
};
//
const selectCategory = (cate) => {
currentCateId.value = cate._id || "";
page.value = 1;
articleList.value = [];
loadArticleList();
};
//
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
page.value = 1;
articleList.value = [];
loadArticleList();
}, 500);
};
//
const loadArticleList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
corpId: corpId,
page: page.value,
pageSize: pageSize,
enable: true,
title: searchTitle.value,
};
// ID
if (currentCateId.value) {
params.cateIds = [currentCateId.value];
}
const res = await getArticleList(params);
if (res.success && res.list) {
const { list = [], total: count = 0 } = res;
const formattedList = list.map((item) => {
//
let date = "";
if (item.createTime) {
const d = new Date(item.createTime);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
date = `${year}-${month}-${day}`;
}
return {
...item,
date,
};
});
if (page.value === 1) {
articleList.value = formattedList;
} else {
articleList.value = [...articleList.value, ...formattedList];
}
total.value = count;
} else {
uni.showToast({
title: res.message || "获取文章列表失败",
icon: "none",
});
}
} catch (error) {
console.error("加载文章列表失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
//
const loadMore = () => {
if (loading.value || articleList.value.length >= total.value) return;
page.value += 1;
loadArticleList();
};
//
const previewArticle = async (article) => {
try {
uni.showLoading({ title: "加载中..." });
const res = await getArticle({ id: article._id, corpId: corpId });
uni.hideLoading();
if (res.success && res.data ) {
previewArticleData.value = {
title: res.data.title || article.title,
content: res.data.content || "",
};
previewPopup.value?.open();
} else {
uni.showToast({
title: res.message || "预览文章失败",
icon: "none",
});
}
} catch (error) {
uni.hideLoading();
console.error("预览文章失败:", error);
uni.showToast({
title: "预览失败,请重试",
icon: "none",
});
}
};
//
const closePreview = () => {
previewPopup.value?.close();
};
//
const sendArticle = (article) => {
//
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage) {
//
const doctorInfo = accountStore.doctorInfo;
userId.value = doctorInfo?.userid || accountStore.openid;
// 线
uni.$emit("sendArticle", {
article: article,
corpId: corpId,
userId: userId.value,
});
uni.showToast({
title: "已选择文章",
icon: "success",
});
//
setTimeout(() => {
uni.navigateBack();
}, 500);
}
};
//
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
getCategoryList();
loadArticleList();
});
</script>
<style scoped lang="scss">
.article-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 16rpx 24rpx;
}
.search-input {
flex: 1;
margin-left: 16rpx;
font-size: 28rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-scroll {
height: 100%;
}
.category-item {
padding: 32rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
}
.category-item.active {
background-color: #fff;
color: #1890ff;
font-weight: bold;
border-left: 4rpx solid #1890ff;
}
.article-list {
flex: 1;
background-color: #fff;
}
.article-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.article-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
}
.article-title {
font-size: 28rpx;
color: #333;
line-height: 1.5;
word-break: break-all;
margin-bottom: 12rpx;
}
.article-date {
font-size: 24rpx;
color: #999;
}
.article-action {
flex-shrink: 0;
}
.send-btn {
padding: 8rpx 32rpx;
font-size: 26rpx;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
.footer {
background-color: #fff;
padding: 20rpx;
border-top: 1px solid #eee;
}
.cancel-btn {
width: 100%;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
/* 预览弹窗样式 */
.preview-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
height: 80vh;
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #eee;
}
.preview-title {
flex: 1;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.preview-close {
padding: 10rpx;
}
.preview-content {
flex: 1;
padding: 30rpx;
overflow-y: auto;
}
.preview-footer {
padding: 20rpx;
border-top: 1px solid #eee;
}
.preview-close-btn {
width: 100%;
background-color: #1890ff;
color: #fff;
}
</style>

View File

@ -1249,3 +1249,111 @@ $primary-color: #0877F1;
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); } 50% { transform: translateY(-10rpx); }
} }
/* 文章卡片样式 */
.article-card {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
max-width: 500rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
min-width: 0;
}
.article-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.4;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-desc {
font-size: 24rpx;
color: #999;
line-height: 1.3;
}
.article-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
/* 文章卡片在不同消息流中的样式 */
.message-right .article-card {
background-color: #e8f4ff;
}
.message-left .article-card {
background-color: #fff;
}
/* 问卷卡片样式 */
.survey-card {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
max-width: 500rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.survey-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
min-width: 0;
}
.survey-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.4;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.survey-desc {
font-size: 24rpx;
color: #999;
line-height: 1.3;
}
.survey-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
/* 问卷卡片在不同消息流中的样式 */
.message-right .survey-card {
background-color: #e8f4ff;
}
.message-left .survey-card {
background-color: #fff;
}

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@
</template> </template>
<script setup> <script setup>
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'; import { computed, ref, onMounted, onUnmounted, nextTick } from "vue";
import { import {
chooseImage, chooseImage,
takePhoto as takePhotoUtil, takePhoto as takePhotoUtil,
@ -64,19 +64,19 @@ import {
sendCustomMessage as sendCustomMessageUtil, sendCustomMessage as sendCustomMessageUtil,
sendMessage as sendMessageUtil, sendMessage as sendMessageUtil,
checkRecordingDuration, checkRecordingDuration,
validateBeforeSend validateBeforeSend,
} from '@/utils/chat-utils.js'; } from "@/utils/chat-utils.js";
// Props // Props
const props = defineProps({ const props = defineProps({
timChatManager: { type: Object, required: true }, timChatManager: { type: Object, required: true },
patientInfo: { type: Object, default: () => ({}) }, patientInfo: { type: Object, default: () => ({}) },
chatRoomBusiness: { type: Object, default: () => ({}) }, chatRoomBusiness: { type: Object, default: () => ({}) },
formatTime: { type: Function, required: true } formatTime: { type: Function, required: true },
}); });
// Emits // Emits
const emit = defineEmits(['messageSent', 'scrollToBottom']); const emit = defineEmits(["messageSent", "scrollToBottom"]);
// //
const inputText = ref(""); const inputText = ref("");
@ -85,9 +85,12 @@ const showMorePanel = ref(false);
const isRecording = ref(false); const isRecording = ref(false);
const recordingText = ref("录音中..."); const recordingText = ref("录音中...");
const cloudCustomData = computed(() => { const cloudCustomData = computed(() => {
const arr = [props.chatRoomBusiness.businessType, props.chatRoomBusiness.businessId]; const arr = [
return arr.filter(Boolean).join('|'); props.chatRoomBusiness.businessType,
}) props.chatRoomBusiness.businessId,
];
return arr.filter(Boolean).join("|");
});
// + // +
const recordingDuration = ref(0); const recordingDuration = ref(0);
@ -112,10 +115,12 @@ const initRecorderManager = () => {
} }
// //
if (!checkRecordingDuration(res, () => { if (
!checkRecordingDuration(res, () => {
isRecording.value = false; isRecording.value = false;
recordingText.value = "录音中..."; recordingText.value = "录音中...";
})) { })
) {
return; return;
} }
@ -141,19 +146,36 @@ const initRecorderManager = () => {
const sendTextMessage = async () => { const sendTextMessage = async () => {
if (!inputText.value.trim()) return; if (!inputText.value.trim()) return;
await sendMessage('text', inputText.value); await sendMessage("text", inputText.value);
inputText.value = ""; inputText.value = "";
}; };
//
const sendTextMessageFromPhrase = async (content) => {
if (!content.trim()) return;
await sendMessage("text", content);
//
nextTick(() => {
emit("scrollToBottom");
});
};
//
defineExpose({
sendTextMessageFromPhrase
});
// //
const sendImageMessage = async (imageFile) => { const sendImageMessage = async (imageFile) => {
console.log('chat-input sendImageMessage 被调用,参数:', imageFile); console.log("chat-input sendImageMessage 被调用,参数:", imageFile);
await sendMessage('image', imageFile); await sendMessage("image", imageFile);
}; };
// //
const sendVoiceMessage = async (voiceFile, duration) => { const sendVoiceMessage = async (voiceFile, duration) => {
await sendMessage('voice', { file: voiceFile, duration }); await sendMessage("voice", { file: voiceFile, duration });
}; };
// //
@ -166,7 +188,7 @@ const sendMessage = async (messageType, data) => {
() => { () => {
showMorePanel.value = false; showMorePanel.value = false;
// //
emit('messageSent'); emit("messageSent");
}, },
cloudCustomData.value cloudCustomData.value
); );
@ -180,7 +202,7 @@ const sendCustomMessage = async (messageData) => {
() => validateBeforeSend(false, false, props.timChatManager), () => validateBeforeSend(false, false, props.timChatManager),
() => { () => {
showMorePanel.value = false; showMorePanel.value = false;
emit('messageSent'); emit("messageSent");
} }
); );
}; };
@ -200,17 +222,22 @@ const toggleMorePanel = () => {
const showImagePicker = () => { const showImagePicker = () => {
chooseImage( chooseImage(
(file) => { (file) => {
console.log('选择图片成功,文件对象:', file); console.log("选择图片成功,文件对象:", file);
// //
sendImageMessage(file); sendImageMessage(file);
}, },
(err) => { (err) => {
console.error('选择图片失败:', err); console.error("选择图片失败:", err);
if (!err.errMsg?.includes('permission') && !err.errMsg?.includes('auth') && !err.errMsg?.includes('拒绝') && !err.errMsg?.includes('未授权')) { if (
!err.errMsg?.includes("permission") &&
!err.errMsg?.includes("auth") &&
!err.errMsg?.includes("拒绝") &&
!err.errMsg?.includes("未授权")
) {
uni.showToast({ uni.showToast({
title: '选择图片失败,请重试', title: "选择图片失败,请重试",
icon: 'none', icon: "none",
duration: 2000 duration: 2000,
}); });
} }
} }
@ -220,17 +247,22 @@ const showImagePicker = () => {
const takePhoto = () => { const takePhoto = () => {
takePhotoUtil( takePhotoUtil(
(file) => { (file) => {
console.log('拍照成功,文件对象:', file); console.log("拍照成功,文件对象:", file);
// //
sendImageMessage(file); sendImageMessage(file);
}, },
(err) => { (err) => {
console.error('拍照失败:', err); console.error("拍照失败:", err);
if (!err.errMsg?.includes('permission') && !err.errMsg?.includes('auth') && !err.errMsg?.includes('拒绝') && !err.errMsg?.includes('未授权')) { if (
!err.errMsg?.includes("permission") &&
!err.errMsg?.includes("auth") &&
!err.errMsg?.includes("拒绝") &&
!err.errMsg?.includes("未授权")
) {
uni.showToast({ uni.showToast({
title: '拍照失败,请重试', title: "拍照失败,请重试",
icon: 'none', icon: "none",
duration: 2000 duration: 2000,
}); });
} }
} }
@ -303,12 +335,11 @@ const cancelRecord = () => {
stopRecordUtil(recorderManager); stopRecordUtil(recorderManager);
}; };
// //
const sendSurveyMessage = async () => { const sendSurveyMessage = async () => {
const surveyMessage = createCustomMessage("survey", { const surveyMessage = createCustomMessage(
"survey",
{
content: "医生发送了问卷调查", content: "医生发送了问卷调查",
surveyTitle: "治疗效果评估", surveyTitle: "治疗效果评估",
surveyDescription: "您好,为了帮助了解您的病情变化,请您如实填写问卷。", surveyDescription: "您好,为了帮助了解您的病情变化,请您如实填写问卷。",
@ -316,26 +347,68 @@ const sendSurveyMessage = async () => {
estimatedTime: "约3-5分钟", estimatedTime: "约3-5分钟",
reward: "积分奖励10分", reward: "积分奖励10分",
note: "问卷内容涉及您的症状变化、用药情况等,请根据实际情况填写。", note: "问卷内容涉及您的症状变化、用药情况等,请根据实际情况填写。",
}, props.formatTime); },
props.formatTime
);
await sendCustomMessage(surveyMessage); await sendCustomMessage(surveyMessage);
}; };
// //
const goToCommonPhrases = () => {
uni.navigateTo({
url: '/pages/message/common-phrases'
});
};
//
const goToArticleList = () => {
uni.navigateTo({
url: '/pages/message/article-list'
});
};
//
const goToSurveyList = () => {
uni.navigateTo({
url: '/pages/message/survey-list'
});
};
const morePanelButtons = [ const morePanelButtons = [
{ text: '照片', icon: '/static/home/photo.png', action: showImagePicker }, { text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker },
{ text: '拍摄', icon: '/static/home/video.png', action: takePhoto }, {
// { text: '', icon: '/static/home/avatar.svg', action: sendSymptomMessage }, text: "回访任务",
// { text: '', icon: '/static/home/avatar.svg', action: sendPrescriptionMessage }, icon: "/static/icon/zhaopian.png",
// { text: '', icon: '/static/home/avatar.svg', action: sendRefillMessage }, action: showImagePicker,
// { text: '', icon: '/static/home/avatar.svg', action: sendSurveyMessage } },
{
text: "常用语",
icon: "/static/icon/changyongyu.png",
action: goToCommonPhrases,
},
{
text: "宣教",
icon: "/static/icon/xuanjiaowenzhang.png",
action: goToArticleList,
},
{
text: "问卷",
icon: "/static/icon/wenjuan.png",
action: goToSurveyList,
},
{
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png",
action: showImagePicker,
},
]; ];
function handleInputFocus() { function handleInputFocus() {
console.log('handleInputFocus') console.log("handleInputFocus");
nextTick().then(() => { nextTick().then(() => {
emit('scrollToBottom') emit("scrollToBottom");
}) });
} }
onMounted(() => { onMounted(() => {
@ -343,14 +416,14 @@ onMounted(() => {
initRecorderManager(); initRecorderManager();
// //
uni.$on('closeMorePanel', () => { uni.$on("closeMorePanel", () => {
showMorePanel.value = false; showMorePanel.value = false;
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
// //
uni.$off('closeMorePanel'); uni.$off("closeMorePanel");
clearDurationTimer(); clearDurationTimer();
}); });
</script> </script>

View File

@ -0,0 +1,88 @@
<template>
<view class="consult-accept-container">
<view class="accept-card">
<view class="accept-content">
<text class="accept-text">患者已发起咨询申请请及时接诊</text>
</view>
<view class="accept-actions">
<button class="btn-cancel" @click="handleReject">暂不接受</button>
<button class="btn-confirm" @click="handleAccept">接受咨询</button>
</view>
</view>
</view>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['accept', 'reject']);
const handleAccept = () => {
emit('accept');
};
const handleReject = () => {
emit('reject');
};
</script>
<style scoped lang="scss">
.consult-accept-container {
width: 100%;
padding: 20rpx 32rpx;
background-color: #f5f5f5;
box-sizing: border-box;
}
.accept-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.accept-content {
margin-bottom: 32rpx;
}
.accept-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.accept-actions {
display: flex;
gap: 24rpx;
}
.btn-cancel,
.btn-confirm {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.btn-cancel::after {
border: none;
}
.btn-confirm {
background-color: #1677ff;
color: #fff;
}
.btn-confirm::after {
border: none;
}
</style>

View File

@ -3,6 +3,7 @@
<text v-if="message.type === 'TIMTextElem'" class="message-text"> <text v-if="message.type === 'TIMTextElem'" class="message-text">
{{ message.payload.text }} {{ message.payload.text }}
</text> </text>
<!-- 图片消息 --> <!-- 图片消息 -->
<image <image
v-else-if="message.type === 'TIMImageElem'" v-else-if="message.type === 'TIMImageElem'"
@ -61,9 +62,47 @@
</view> </view>
</view> </view>
<!-- 自定义消息卡片 -->
<!-- <template v-else-if="message.type === 'TIMCustomElem'"> <template v-else-if="message.type === 'TIMCustomElem'">
<!-- 文章消息 -->
<view <view
v-if="getCustomMessageType(message) === 'article'"
class="article-card"
@click="handleArticleClick(message)"
>
<view class="article-content">
<view class="article-title">{{ getArticleData(message).title }}</view>
<view class="article-desc">{{ getArticleData(message).desc }}</view>
</view>
<image
v-if="getArticleData(message).imgUrl"
class="article-image"
:src="getArticleData(message).imgUrl"
mode="aspectFill"
/>
</view>
<!-- 问卷消息 -->
<view
v-else-if="getCustomMessageType(message) === 'survey'"
class="survey-card"
@click="handleSurveyClick(message)"
>
<view class="survey-content">
<view class="survey-title">{{ getSurveyData(message).title }}</view>
<view class="survey-desc">{{ getSurveyData(message).desc }}</view>
</view>
<image
v-if="getSurveyData(message).imgUrl"
class="survey-image"
:src="getSurveyData(message).imgUrl"
mode="aspectFill"
/>
</view>
<!-- 其他自定义消息 -->
<!-- <view
v-else
class="card-avatar-row" class="card-avatar-row"
@click="() => console.log('点击头像', message)" @click="() => console.log('点击头像', message)"
> >
@ -73,8 +112,8 @@
:flow="message.flow" :flow="message.flow"
@viewDetail="$emit('viewDetail', $event)" @viewDetail="$emit('viewDetail', $event)"
/> />
</view> </view> -->
</template> --> </template>
</template> </template>
<script setup> <script setup>
@ -102,6 +141,8 @@ const isPlaying = computed(() => {
// //
const getImageStyle = (imageInfo) => { const getImageStyle = (imageInfo) => {
// 使 // 使
imageInfo.width = imageInfo.width || imageInfo.Width;
imageInfo.height = imageInfo.height || imageInfo.Height;
if (!imageInfo || !imageInfo.width || !imageInfo.height) { if (!imageInfo || !imageInfo.width || !imageInfo.height) {
return { return {
width: "400rpx", width: "400rpx",
@ -160,6 +201,90 @@ const getVoiceStyle = (duration) => {
width: width + "rpx", width: width + "rpx",
}; };
}; };
//
const getCustomMessageType = (message) => {
try {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return data.type || '';
}
} catch (error) {
console.error('解析自定义消息失败:', error);
}
return '';
};
//
const getArticleData = (message) => {
try {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return {
title: data.title || '宣教文章',
desc: data.desc || '宣教文章',
url: data.url || '',
imgUrl: data.imgUrl || ''
};
}
} catch (error) {
console.error('解析文章数据失败:', error);
}
return {
title: '宣教文章',
desc: '宣教文章',
url: '',
imgUrl: ''
};
};
//
const handleArticleClick = (message) => {
const articleData = getArticleData(message);
if (articleData.url) {
//
//
console.log('打开文章:', articleData.url);
// uni.navigateTo({
// url: `/pages/article/detail?url=${encodeURIComponent(articleData.url)}`
// });
}
};
//
const getSurveyData = (message) => {
try {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return {
title: data.title || '填写问卷',
desc: data.desc || '请填写问卷',
url: data.url || '',
imgUrl: data.imgUrl || ''
};
}
} catch (error) {
console.error('解析问卷数据失败:', error);
}
return {
title: '填写问卷',
desc: '请填写问卷',
url: '',
imgUrl: ''
};
};
//
const handleSurveyClick = (message) => {
const surveyData = getSurveyData(message);
if (surveyData.url) {
//
console.log('打开问卷:', surveyData.url);
// uni.navigateTo({
// url: `/pages/survey/fill?url=${encodeURIComponent(surveyData.url)}`
// });
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -0,0 +1,297 @@
<template>
<view v-if="visible" class="modal-overlay" @click="handleCancel">
<view class="modal-container" @click.stop>
<view class="modal-header">
<text class="modal-title">选择暂不接受咨询原因供患者知晓</text>
</view>
<view class="modal-content">
<!-- 预设原因选项 -->
<view class="reason-options">
<view
v-for="(option, index) in reasonOptions"
:key="index"
class="reason-option"
:class="{ active: selectedReason === option }"
@click="selectReason(option)"
>
<text class="option-text">{{ option }}</text>
<view class="option-icon" :class="{ checked: selectedReason === option }">
<text v-if="selectedReason === option" class="check-mark"></text>
</view>
</view>
<!-- 自定义输入选项 -->
<view
class="reason-option custom-option"
:class="{ active: isCustomInput }"
@click="selectCustomInput"
>
<text class="option-text">填写拒诊理由</text>
<view class="option-icon arrow">
<text class="arrow-icon"></text>
</view>
</view>
</view>
<!-- 自定义输入框 -->
<view v-if="isCustomInput" class="custom-input-container">
<textarea
class="custom-textarea"
v-model="customReason"
placeholder="请输入理由,供患者知晓"
maxlength="200"
:auto-height="true"
/>
<text class="char-count">{{ customReason.length }}/200</text>
</view>
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button class="btn-confirm" @click="handleConfirm">确定</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['confirm', 'cancel']);
//
const reasonOptions = ref([
'临时有紧急事务',
'患者病情复杂,需线下就诊',
]);
const selectedReason = ref('');
const isCustomInput = ref(false);
const customReason = ref('');
//
const selectReason = (option) => {
selectedReason.value = option;
isCustomInput.value = false;
customReason.value = '';
};
//
const selectCustomInput = () => {
selectedReason.value = '';
isCustomInput.value = true;
};
//
const handleCancel = () => {
//
selectedReason.value = '';
isCustomInput.value = false;
customReason.value = '';
emit('cancel');
};
//
const handleConfirm = () => {
let reason = '';
if (isCustomInput.value) {
reason = customReason.value.trim();
if (!reason) {
uni.showToast({
title: '请输入拒绝原因',
icon: 'none',
});
return;
}
} else {
reason = selectedReason.value;
if (!reason) {
uni.showToast({
title: '请选择拒绝原因',
icon: 'none',
});
return;
}
}
emit('confirm', reason);
//
selectedReason.value = '';
isCustomInput.value = false;
customReason.value = '';
};
</script>
<style scoped lang="scss">
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-container {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.modal-header {
padding: 32rpx 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
line-height: 1.5;
}
.modal-content {
padding: 24rpx 32rpx;
max-height: 600rpx;
overflow-y: auto;
}
.reason-options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.reason-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&.active {
background-color: #e6f4ff;
border-color: #1677ff;
}
}
.option-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.option-icon {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&.checked {
background-color: #1677ff;
border-color: #1677ff;
}
&.arrow {
border: none;
}
}
.check-mark {
font-size: 24rpx;
color: #fff;
font-weight: bold;
}
.arrow-icon {
font-size: 40rpx;
color: #999;
font-weight: 300;
}
.custom-input-container {
margin-top: 16rpx;
padding: 16rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
}
.custom-textarea {
width: 100%;
min-height: 120rpx;
font-size: 28rpx;
color: #333;
line-height: 1.6;
background-color: transparent;
border: none;
outline: none;
box-sizing: border-box;
}
.char-count {
display: block;
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.modal-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
}
.btn-cancel,
.btn-confirm {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.btn-cancel::after {
border: none;
}
.btn-confirm {
background-color: #1677ff;
color: #fff;
}
.btn-confirm::after {
border: none;
}
</style>

View File

@ -1,9 +1,9 @@
<template> <template>
<view v-if="notifyText" class="notify-bar"> <!-- <view v-if="notifyText" class="notify-bar">
<view class="notify-line"></view> <view class="notify-line"></view>
<view class="notify-text">{{ notifyText }}</view> <view class="notify-text">{{ notifyText }}</view>
<view class="notify-line"></view> <view class="notify-line"></view>
</view> </view> -->
<view class="system-message"> <view class="system-message">
<view class="system-text">{{ text }}</view> <view class="system-text">{{ text }}</view>
</view> </view>
@ -21,17 +21,111 @@ const props = defineProps({
const payload = computed(() => props.message?.payload || {}); const payload = computed(() => props.message?.payload || {});
const extension = computed(() => { //
const systemMessageData = computed(() => {
try { try {
return JSON.parse(payload.value.extension); // payload.data
} catch (e) { if (payload.value.data) {
return {}; const data = typeof payload.value.data === 'string'
? JSON.parse(payload.value.data)
: payload.value.data;
if (data.type === 'system_message') {
return data;
} }
}
} catch (e) {
console.error('解析系统消息失败:', e);
}
return null;
}); });
const text = computed(() => extension.value.patient || payload.value.data || '') //
const extension = computed(() => {
try {
if (payload.value.extension) {
return typeof payload.value.extension === 'string'
? JSON.parse(payload.value.extension)
: payload.value.extension;
}
} catch (e) {
console.error('解析扩展信息失败:', e);
}
return {};
});
const notifyText = computed(() => extension.value.notifyText || '') //
const text = computed(() => {
//
if (systemMessageData.value?.text) {
return systemMessageData.value.text;
}
//
if (systemMessageData.value?.messageType) {
const messageType = systemMessageData.value.messageType;
switch (messageType) {
case 'consult_pending':
return '患者已发起咨询申请,请及时接诊';
case 'consult_accepted':
return '医生已接诊';
case 'consult_rejected':
return '医生暂时无法接诊';
case 'consult_ended':
return '问诊已结束';
case 'consult_timeout':
return '问诊已超时';
default:
return systemMessageData.value.content || '[系统消息]';
}
}
// extension
if (extension.value.patient) {
return extension.value.patient;
}
// payload.data
if (payload.value.data && typeof payload.value.data === 'string') {
// data JSON
try {
JSON.parse(payload.value.data);
} catch {
return payload.value.data;
}
}
return '[系统消息]';
});
//
const notifyText = computed(() => {
//
if (extension.value.notifyText) {
return extension.value.notifyText;
}
//
if (systemMessageData.value) {
const messageType = systemMessageData.value.messageType;
switch (messageType) {
case 'consult_pending':
return '待接诊';
case 'consult_rejected':
return '已拒绝';
case 'consult_timeout':
return '已超时';
case 'consult_accepted':
return '已接诊';
case 'consult_ended':
return '已结束';
default:
return '';
}
}
return '';
});
</script> </script>

View File

@ -54,7 +54,9 @@
<image <image
v-if="message.flow === 'in'" v-if="message.flow === 'in'"
class="doctor-msg-avatar" class="doctor-msg-avatar"
:src="chatMember[message.from]?.avatar || '/static/default-avatar.png'" :src="
chatMember[message.from]?.avatar || '/static/default-avatar.png'
"
mode="aspectFill" mode="aspectFill"
/> />
@ -62,7 +64,9 @@
<image <image
v-if="message.flow === 'out'" v-if="message.flow === 'out'"
class="user-msg-avatar" class="user-msg-avatar"
:src="chatMember[message.from]?.avatar || '/static/home/avatar.svg'" :src="
chatMember[message.from]?.avatar || '/static/home/avatar.svg'
"
mode="aspectFill" mode="aspectFill"
/> />
@ -108,9 +112,24 @@
</view> </view>
</scroll-view> </scroll-view>
<!-- 接受问诊组件 -->
<ConsultAccept
v-if="showConsultAccept"
@accept="handleAcceptConsult"
@reject="handleRejectConsult"
/>
<!-- 拒绝原因对话框 -->
<RejectReasonModal
:visible="showRejectReasonModal"
@confirm="handleRejectReasonConfirm"
@cancel="handleRejectReasonCancel"
/>
<!-- 聊天输入组件 --> <!-- 聊天输入组件 -->
<ChatInput <ChatInput
v-if="!isEvaluationPopupOpen" v-if="!isEvaluationPopupOpen && !showConsultAccept"
ref="chatInputRef"
:timChatManager="timChatManager" :timChatManager="timChatManager"
:formatTime="formatTime" :formatTime="formatTime"
@scrollToBottom="() => scrollToBottom(true)" @scrollToBottom="() => scrollToBottom(true)"
@ -142,10 +161,13 @@ import {
handleViewDetail, handleViewDetail,
checkIMConnectionStatus, checkIMConnectionStatus,
} from "@/utils/chat-utils.js"; } from "@/utils/chat-utils.js";
import { sendConsultRejectedMessage } from "@/utils/api.js";
import useGroupChat from "./hooks/use-group-chat"; import useGroupChat from "./hooks/use-group-chat";
import MessageTypes from "./components/message-types.vue"; import MessageTypes from "./components/message-types.vue";
import ChatInput from "./components/chat-input.vue"; import ChatInput from "./components/chat-input.vue";
import SystemMessage from "./components/system-message.vue"; import SystemMessage from "./components/system-message.vue";
import ConsultAccept from "./components/consult-accept.vue";
import RejectReasonModal from "./components/reject-reason-modal.vue";
const timChatManager = globalTimChatManager; const timChatManager = globalTimChatManager;
@ -153,16 +175,16 @@ const timChatManager = globalTimChatManager;
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore()); const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
const { initIMAfterLogin } = useAccountStore(); const { initIMAfterLogin } = useAccountStore();
//
const chatInputRef = ref(null);
const groupId = ref(""); const groupId = ref("");
const { const { chatMember, getGroupInfo } = useGroupChat(groupId);
chatMember,
getGroupInfo
} = useGroupChat(groupId);
// //
const updateNavigationTitle = () => { const updateNavigationTitle = () => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: "群聊" title: "群聊",
}); });
}; };
@ -176,6 +198,12 @@ const chatInfo = ref({
// //
const isEvaluationPopupOpen = ref(false); const isEvaluationPopupOpen = ref(false);
//
const showConsultAccept = ref(false);
//
const showRejectReasonModal = ref(false);
// //
const messageList = ref([]); const messageList = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
@ -188,10 +216,73 @@ const lastFirstMessageId = ref("");
// //
function isSystemMessage(message) { function isSystemMessage(message) {
const description = message.payload?.description; if (message.type !== "TIMCustomElem") {
return ( return false;
message.type === "TIMCustomElem" && description === "SYSTEM_NOTIFICATION" }
);
try {
// payload.data
if (message.payload?.data) {
const data = typeof message.payload.data === 'string'
? JSON.parse(message.payload.data)
: message.payload.data;
//
if (data.type === 'system_message') {
return true;
}
}
// description
if (message.payload?.description === '系统消息标记') {
return true;
}
//
if (message.payload?.description === 'SYSTEM_NOTIFICATION') {
return true;
}
} catch (error) {
console.error('判断系统消息失败:', error);
}
return false;
}
//
function checkConsultPendingStatus() {
//
for (let i = messageList.value.length - 1; i >= 0; i--) {
const message = messageList.value[i];
if (message.type === "TIMCustomElem" && message.payload?.data) {
try {
const data = typeof message.payload.data === 'string'
? JSON.parse(message.payload.data)
: message.payload.data;
// consult_pending
if (data.type === 'system_message' && data.messageType === 'consult_pending') {
showConsultAccept.value = true;
return;
}
//
if (data.type === 'system_message' &&
(data.messageType === 'consult_accepted' ||
data.messageType === 'consult_ended' ||
data.messageType === 'consult_rejected')) {
showConsultAccept.value = false;
return;
}
} catch (error) {
console.error('解析系统消息失败:', error);
}
}
}
//
showConsultAccept.value = false;
} }
// //
@ -200,7 +291,6 @@ function getBubbleClass(message) {
if (message.type === "TIMImageElem") { if (message.type === "TIMImageElem") {
return "image-bubble"; return "image-bubble";
} }
if (message.type === "TIMCustomElem") { if (message.type === "TIMCustomElem") {
return message.flow === "out" ? "user-bubble" : "doctor-bubble-blue"; return message.flow === "out" ? "user-bubble" : "doctor-bubble-blue";
} }
@ -284,6 +374,10 @@ const initTIMCallbacks = async () => {
if (!existingMessage) { if (!existingMessage) {
messageList.value.push(message); messageList.value.push(message);
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length); console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
//
checkConsultPendingStatus();
// 使 // 使
nextTick(() => { nextTick(() => {
scrollToBottom(true); scrollToBottom(true);
@ -342,6 +436,9 @@ const initTIMCallbacks = async () => {
isCompleted.value = data.isCompleted || false; isCompleted.value = data.isCompleted || false;
isLoadingMore.value = false; isLoadingMore.value = false;
//
checkConsultPendingStatus();
nextTick(() => { nextTick(() => {
if (data.isRefresh) { if (data.isRefresh) {
console.log("后台刷新完成,保持当前滚动位置"); console.log("后台刷新完成,保持当前滚动位置");
@ -484,13 +581,13 @@ const scrollToBottom = (immediate = false) => {
if (immediate) { if (immediate) {
// //
scrollIntoView.value = ''; scrollIntoView.value = "";
nextTick(() => { nextTick(() => {
scrollIntoView.value = targetId; scrollIntoView.value = targetId;
}); });
} else { } else {
// 使DOM // 使DOM
scrollIntoView.value = ''; scrollIntoView.value = "";
setTimeout(() => { setTimeout(() => {
scrollIntoView.value = targetId; scrollIntoView.value = targetId;
}, 50); }, 50);
@ -590,6 +687,119 @@ onHide(() => {
stopIMMonitoring(); stopIMMonitoring();
}); });
//
const sendCommonPhrase = (content) => {
if (chatInputRef.value) {
chatInputRef.value.sendTextMessageFromPhrase(content);
}
};
//
defineExpose({
sendCommonPhrase,
});
//
const handleAcceptConsult = async () => {
try {
uni.showLoading({
title: '处理中...',
});
//
const customMessage = {
data: JSON.stringify({
type: 'system_message',
messageType: 'consult_accepted',
content: '医生已接诊',
timestamp: Date.now(),
}),
description: '系统消息标记',
extension: '',
};
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace('GROUP', ''),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
});
const sendResult = await timChatManager.tim.sendMessage(message);
if (sendResult.code === 0) {
showConsultAccept.value = false;
uni.hideLoading();
uni.showToast({
title: '已接受问诊',
icon: 'success',
});
} else {
throw new Error(sendResult.message || '发送失败');
}
} catch (error) {
console.error('接受问诊失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败',
icon: 'none',
});
}
};
//
const handleRejectConsult = () => {
//
showRejectReasonModal.value = true;
};
//
const handleRejectReasonConfirm = async (reason) => {
try {
showRejectReasonModal.value = false;
uni.showLoading({
title: '处理中...',
});
//
const memberName = account.value?.name || '医生';
// ID
const groupId = chatInfo.value.conversationID.replace('GROUP', '');
//
const result = await sendConsultRejectedMessage({
groupId,
memberName,
reason,
});
uni.hideLoading();
if (result.success) {
showConsultAccept.value = false;
uni.showToast({
title: '已拒绝问诊',
icon: 'success',
});
} else {
throw new Error(result.message || '发送失败');
}
} catch (error) {
console.error('拒绝问诊失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败',
icon: 'none',
});
}
};
//
const handleRejectReasonCancel = () => {
showRejectReasonModal.value = false;
};
// //
onUnmounted(() => { onUnmounted(() => {
clearMessageCache(); clearMessageCache();
@ -599,6 +809,172 @@ onUnmounted(() => {
timChatManager.setCallback("onMessageReceived", null); timChatManager.setCallback("onMessageReceived", null);
timChatManager.setCallback("onMessageListLoaded", null); timChatManager.setCallback("onMessageListLoaded", null);
timChatManager.setCallback("onError", null); timChatManager.setCallback("onError", null);
//
uni.$off("sendArticle");
//
uni.$off("sendSurvey");
});
//
uni.$on("sendArticle", async (data) => {
const { article, corpId, userId } = data;
if (!article || !article._id) {
uni.showToast({
title: "文章信息不完整",
icon: "none",
});
return;
}
try {
//
const env = __VITE_ENV__;
const baseUrl = env.VITE_PATIENT_PAGE_BASE_URL || "";
//
const articleUrl = `${baseUrl}pages/article/index?id=${article._id}&corpId=${corpId}`;
//
const customMessage = {
data: JSON.stringify({
type: "article",
title: article.title || "宣教文章",
desc: "宣教文章",
url: articleUrl,
imgUrl:
"https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/19-%E9%97%AE%E5%8D%B7.png?sign=55a4cd77c418b2c548b65792a2cf6bce&t=1701328694",
}),
description: "ARTICLE",
extension: "",
};
//
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace("GROUP", ""),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
});
const sendResult = await timChatManager.tim.sendMessage(message);
if (sendResult.code === 0) {
uni.showToast({
title: "发送成功",
icon: "success",
});
//
// await addArticleSendRecord({
// corpId,
// userId,
// articleId: article._id,
// customerId: chatInfo.value.userID
// });
} else {
throw new Error(sendResult.message || "发送失败");
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
}
});
//
uni.$on("sendSurvey", async (data) => {
const { survey, corpId, userId, sendSurveyId } = data;
if (!survey || !survey._id) {
uni.showToast({
title: "问卷信息不完整",
icon: "none",
});
return;
}
try {
//
const env = __VITE_ENV__;
const baseUrl = env.VITE_PATIENT_PAGE_BASE_URL || "";
const surveyUrl = env.VITE_SURVEY_URL || "";
//
const customerId = chatInfo.value.userID || "";
const customerName = chatInfo.value.customerName || "";
//
const { createSurveyRecord } = await import("@/utils/api.js");
const recordRes = await createSurveyRecord({
corpId,
userId,
surveryId: survey._id,
memberId: customerId,
customer: customerName,
sendSurveyId,
});
if (!recordRes.success) {
throw new Error(recordRes.message || "创建问卷记录失败");
}
const answerId = recordRes.data?.id || "";
//
let surveyLink = "";
if (survey.createBy === "system") {
//
surveyLink = `${surveyUrl}?corpId=${corpId}&surveyId=${survey.surveyId}&memberId=${customerId}&sendSurveyId=${sendSurveyId}&userId=${userId}`;
} else {
//
surveyLink = `${baseUrl}pages/survery/fill?corpId=${corpId}&surveryId=${
survey._id
}&memberId=${customerId}&answerId=${answerId}&name=${encodeURIComponent(
customerName
)}`;
}
//
const customMessage = {
data: JSON.stringify({
type: "survey",
title: survey.name || "填写问卷",
desc: "请填写问卷",
url: surveyLink,
imgUrl:
"https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/19-%E9%97%AE%E5%8D%B7.png?sign=55a4cd77c418b2c548b65792a2cf6bce&t=1701328694",
}),
description: "SURVEY",
extension: "",
};
//
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace("GROUP", ""),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
});
const sendResult = await timChatManager.tim.sendMessage(message);
if (sendResult.code === 0) {
uni.showToast({
title: "发送成功",
icon: "success",
});
} else {
throw new Error(sendResult.message || "发送失败");
}
} catch (error) {
console.error("发送问卷失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
}
}); });
</script> </script>

View File

@ -0,0 +1,423 @@
<template>
<view class="survey-page">
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input class="search-input" v-model="searchName" placeholder="输入问卷名称搜索" @input="handleSearch" />
</view>
</view>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === (cate._id || 'all') }"
@click="selectCategory(cate)"
>
{{ cate.name }}
</view>
</scroll-view>
</view>
<view class="survey-list">
<scroll-view
scroll-y
class="survey-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
>
<view v-if="loading && surveyList.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="surveyList.length === 0" class="empty-container">
<empty-data :title="emptyText || '暂无问卷'" />
</view>
<view v-else>
<view
v-for="survey in surveyList"
:key="survey._id"
class="survey-item"
>
<view class="survey-content" @click="previewSurvey(survey)">
<text class="survey-title">{{ survey.name }}</text>
<text class="survey-desc">{{ survey.description || '暂无问卷说明' }}</text>
</view>
<view class="survey-action">
<button class="send-btn" size="mini" type="primary" @click="sendSurvey(survey)">
发送
</button>
</view>
</view>
<view v-if="loading && surveyList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!loading && surveyList.length >= total" class="no-more">
没有更多了
</view>
</view>
</scroll-view>
</view>
</view>
<view class="footer">
<button class="cancel-btn" @click="goBack">取消</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getSurveyCateList, getSurveyList, createSurveyRecord } from '@/utils/api.js';
import useAccountStore from '@/store/account.js';
import EmptyData from '@/components/empty-data.vue';
const env = __VITE_ENV__;
const accountStore = useAccountStore();
const corpId = env.MP_CORP_ID;
const userId = ref('');
//
const searchName = ref('');
let searchTimer = null;
//
const categoryList = ref([{ _id: '', name: '全部' }]);
const currentCateId = ref('');
//
const surveyList = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
const emptyText = ref('');
//
const getCategoryList = async () => {
try {
const res = await getSurveyCateList({ corpId: corpId });
if (res.success && res.data) {
const cates = res.data.list || [];
categoryList.value = [
{ _id: '', name: '全部' },
...cates
];
}
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
//
const selectCategory = (cate) => {
currentCateId.value = cate._id || '';
page.value = 1;
surveyList.value = [];
loadSurveyList();
};
//
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
page.value = 1;
surveyList.value = [];
loadSurveyList();
}, 500);
};
//
const loadSurveyList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
corpId: corpId,
page: page.value,
pageSize: pageSize,
name: searchName.value.trim(),
status: 'enable',
showCount: false
};
// ID
if (currentCateId.value) {
params.cateIds = [currentCateId.value];
}
const res = await getSurveyList(params);
if (res.success && res.data) {
const { list = [], total: count = 0 } = res.data;
if (page.value === 1) {
surveyList.value = list;
} else {
surveyList.value = [...surveyList.value, ...list];
}
total.value = count;
emptyText.value = '暂无问卷信息';
} else {
emptyText.value = res.message || '加载失败';
uni.showToast({
title: res.message || '获取问卷列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载问卷列表失败:', error);
emptyText.value = '加载失败';
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
loading.value = false;
}
};
//
const loadMore = () => {
if (loading.value || surveyList.value.length >= total.value) return;
page.value += 1;
loadSurveyList();
};
//
const previewSurvey = (survey) => {
//
uni.showToast({
title: '预览功能开发中',
icon: 'none'
});
};
//
const generateRandomString = (length) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
//
const sendSurvey = async (survey) => {
if (loading.value) return;
try {
loading.value = true;
//
const doctorInfo = accountStore.doctorInfo;
userId.value = doctorInfo?.userid || accountStore.openid;
// ID
const sendSurveyId = generateRandomString(10);
//
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
//
// 使
uni.$emit('sendSurvey', {
survey: survey,
corpId: corpId,
userId: userId.value,
sendSurveyId: sendSurveyId
});
uni.showToast({
title: '已选择问卷',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 500);
} catch (error) {
console.error('发送问卷失败:', error);
uni.showToast({
title: error.message || '发送失败',
icon: 'none'
});
} finally {
loading.value = false;
}
};
//
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
getCategoryList();
loadSurveyList();
});
</script>
<style scoped lang="scss">
.survey-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 16rpx 24rpx;
}
.search-input {
flex: 1;
margin-left: 16rpx;
font-size: 28rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-scroll {
height: 100%;
}
.category-item {
padding: 32rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
}
.category-item.active {
background-color: #fff;
color: #1890ff;
font-weight: bold;
border-left: 4rpx solid #1890ff;
}
.survey-list {
flex: 1;
background-color: #fff;
}
.survey-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.survey-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
}
.survey-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
}
.survey-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
margin-bottom: 12rpx;
}
.survey-desc {
font-size: 24rpx;
color: #999;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.survey-action {
flex-shrink: 0;
}
.send-btn {
padding: 8rpx 32rpx;
font-size: 26rpx;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
.footer {
background-color: #fff;
padding: 20rpx;
border-top: 1px solid #eee;
}
.cancel-btn {
width: 100%;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<template v-if="status === 'unverified'">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark">
认证须知
</view>
<view class="text-base text-dark px-15 leading-normal font-semibold mt-10">
1认证通过后您个人账号病历档案管理数含所有团队上限由10个升级至100个
</view>
<view class="px-15 leading-normal mt-10 ">
<text class="text-base text-dark">2认证前请仔细核对个人信息确保准确无误认证后部分信息不支持修改包括姓名岗位等如需修改以上信息请联系客服人工处理</text>
</view>
<view class="mt-10 px-15 leading-normal font-semibold pb-50 text-lg text-primary" @click="toService()">点击添加客服
</view>
</template>
<template v-else-if="status === 'verified'">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark">
提示
</view>
<view class="text-base text-dark px-15 leading-normal font-semibold mt-10">
您的认证已通过
</view>
<view class="px-15 leading-normal mt-10 text-base text-dark">
若需要修改姓名岗位等信息请联系客服人工处理
</view>
<view class="mt-10 px-15 leading-normal font-semibold pb-50 text-lg text-primary" @click="toService()">
点击添加客服
</view>
</template>
<template v-else-if="status === 'failed'">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark">
认证失败原因
</view>
<view class="px-15 leading-normal mt-10 text-base text-dark pb-50">
{{ reason }}
</view>
</template>
<view v-if="btns" class="footer-buttons">
<button-footer hideden-shadow :cancelText="btns.cancelText" :confirmText="btns.confirmText"
:showCancel="btns.showCancel" :showConfirm="btns.showConfirm" @confirm="confirm()" @cancel="close()" />
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { toast } from '@/utils/widget';
import ButtonFooter from '@/components/button-footer.vue';
const emits = defineEmits(['close', 'confirm'])
const props = defineProps({
status: {
type: String,
default: ''
},
visible: {
type: Boolean,
default: false
}
})
const popup = ref();
const status = ref('');
const reason = ref('');
const { account, doctorInfo } = storeToRefs(useAccountStore());
const btns = computed(() => {
if (status.value === 'unverified') {
return { showCancel: true, showConfirm: true, confirmText: '去认证' }
}
if (status.value === 'verified') {
return { showConfirm: false, cancelText: '我知道了' }
}
if (status.value === 'failed') {
return { showCancel: true, showConfirm: true, confirmText: '重新认证' }
}
})
function close() {
emits('close')
}
function confirm() {
close()
uni.navigateTo({
url: "/pages/work/profile?type=cert",
});
}
function toService() {
close()
}
async function getStatus() {
const res = await api('getMemberVerifyStatus', { corpId: account.value.corpId, weChatOpenId: account.value.openid, id: doctorInfo.value._id })
if (res && res.success) {
status.value = res.data.verifyStatus;
reason.value = res.data.reason || '';
popup.value && popup.value.open()
} else {
toast(res.message);
close()
}
}
watch(() => props.visible, n => {
if (n) {
getStatus()
} else {
popup.value && popup.value.close()
}
})
</script>
<style lang="scss" scoped>
.pb-50 {
padding-bottom: 100rpx;
}
</style>

308
pages/work/profile copy.vue Normal file
View File

@ -0,0 +1,308 @@
<template>
<view class="profile-page">
<!-- 表单区域 -->
<view class="form-section bg-white">
<!-- 姓名 -->
<form-input
name="姓名"
:required="true"
:form="formData"
title="anotherName"
@change="handleFieldChange"
/>
<!-- 头像 -->
<common-cell name="头像">
<view class="form-content__wrapper" @click="chooseAvatar">
<view class="flex-main-content flex items-center">
<image
v-if="formData.avatar"
class="avatar-preview"
:src="formData.avatar"
mode="aspectFill"
/>
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 性别 -->
<form-select
name="性别"
:form="formData"
title="gender"
:range="genderOptions"
@change="handleFieldChange"
/>
<!-- 手机号不可修改 -->
<common-cell name="手机号 (不可修改)">
<view class="form-content__wrapper">
<view class="flex-main-content text-dark">{{ formData.mobile }}</view>
</view>
</common-cell>
<!-- 岗位 -->
<form-select
name="岗位"
:form="formData"
title="position"
:range="positionOptions"
@change="handleFieldChange"
/>
<!-- 职称 -->
<form-select
name="职称"
:form="formData"
title="title"
:range="titleOptions"
@change="handleFieldChange"
/>
<!-- 科室 -->
<common-cell name="科室">
<view class="form-content__wrapper" @click="openDepartmentSelect">
<view
class="flex-main-content text-right"
:class="{ 'text-placeholder': !formData.departmentName }"
>
{{ formData.departmentName || "请选择" }}
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 个人介绍 -->
<form-textarea
name="个人介绍"
:form="formData"
title="intro"
:word-limit="500"
@change="handleFieldChange"
/>
</view>
<!-- 底部按钮 -->
<view class="button-footer">
<view class="btn btn-cancel" @click="handleCancel">取消</view>
<view class="btn btn-save" @click="handleSave">保存</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import useGuard from "@/hooks/useGuard.js";
import { chooseAndUploadImage } from "@/utils/file.js";
import useAccountStore from "@/store/account.js";
import { storeToRefs } from "pinia";
import CommonCell from "@/components/form-template/common-cell.vue";
import FormInput from "@/components/form-template/form-cell/form-input.vue";
import FormSelect from "@/components/form-template/form-cell/form-select.vue";
import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue";
import api from "@/utils/api.js";
const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad } = useGuard();
const { getDoctorInfo } = useAccountStore();
//
const formData = ref({
anotherName: "",
avatar: "",
gender: "",
mobile: "",
position: "",
title: "",
department: "",
departmentName: "",
departmentId: "",
intro: "",
});
//
const genderOptions = [
{ label: "男", value: "0" },
{ label: "女", value: "1" },
];
const positionOptions = ["医生", "护士", "药师", "技师", "其他"];
const titleOptions = ["主任医师", "副主任医师", "主治医师", "医师", "其他"];
//
const handleFieldChange = (e) => {
if (e.title === "gender") {
formData.value[e.title] = e.value.value;
} else {
formData.value[e.title] = e.value;
}
};
//
const chooseAvatar = async () => {
const url = await chooseAndUploadImage();
if (url) {
formData.value.avatar = url;
}
};
const handleCancel = () => {
uni.navigateBack();
};
//
const handleSave = async () => {
createDoctorInfo();
};
useLoad(() => {
if (doctorInfo.value) {
formData.value = { ...doctorInfo.value };
} else {
formData.value.mobile = account.value.mobile;
}
});
//
const createDoctorInfo = async () => {
if (!formData.value.anotherName) {
uni.showToast({
title: "请输入姓名",
icon: "none",
});
return;
}
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
mobile: formData.value.mobile,
weChatOpenId: account.value.openid,
deptIds: [],
loginTypes: ["wxApp"],
corpId: account.value.corpId,
};
const res = await api("addCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "创建成功",
icon: "success",
});
await getDoctorInfo();
uni.navigateBack();
} else {
uni.showToast({
title: "创建失败",
icon: "none",
});
console.error("创建医生信息失败:", res);
}
};
const updateDoctorInfo = async () => {
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
};
const res = await api("updateCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "更新成功",
icon: "success",
});
}
await getDoctorInfo();
uni.navigateBack();
};
//
const openDepartmentSelect = () => {
uni.navigateTo({
url: "/pages/work/department-select",
events: {
deptSelected: ({ name, deptId }) => {
formData.value.department = name || "";
formData.value.departmentName = name || "";
formData.value.departmentId = deptId || "";
},
},
});
};
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.form-section {
margin-top: 20rpx;
}
.form-content__wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
text-align: right;
font-size: 28rpx;
input {
text-align: right;
}
}
.text-placeholder {
color: #999;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
}
.avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
.avatar-icon {
font-size: 48rpx;
color: #999;
}
}
.button-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx 30rpx;
background: #fff;
border-top: 1px solid #eee;
gap: 20rpx;
z-index: 100;
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 8rpx;
font-size: 32rpx;
}
.btn-cancel {
background: #fff;
border: 1px solid #007aff;
color: #007aff;
}
.btn-save {
background: #007aff;
color: #fff;
}
}
</style>

View File

@ -1,215 +1,108 @@
<template> <template>
<view class="profile-page"> <full-page>
<!-- 表单区域 --> <view class="p-15">
<view class="form-section bg-white"> <view class="bg-white px-10 mb-10 rounded">
<!-- 姓名 --> <form-input :form="formData" :required="rule.anotherName.required" wordLimit="10" title="anotherName"
<form-input :name="rule.anotherName.name" @change="onChange($event)" />
name="姓名" <common-cell title="avatar" name="头像">
:required="true" <view class="flex-grow flex items-center justify-end" @click="chooseAvatar()">
:form="formData" <image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" />
title="anotherName" <image v-else class="avatar mr-5 rounded-full" src="/static/default-avatar.png" />
@change="handleFieldChange" <uni-icons color="#999" type="right" size="16" />
/>
<!-- 头像 -->
<common-cell name="头像">
<view class="form-content__wrapper" @click="chooseAvatar">
<view class="flex-main-content flex items-center">
<image
v-if="formData.avatar"
class="avatar-preview"
:src="formData.avatar"
mode="aspectFill"
/>
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view> </view>
</common-cell> </common-cell>
<!-- 性别 --> <form-select :form="formData" name="性别" title="gender" :range="genderOptions" @change="onChange($event)" />
<form-select <form-input :form="formData" disableChange wordLimit="11" title="mobile" name="手机号 (不可修改)" />
name="性别"
:form="formData"
title="gender"
:range="genderOptions"
@change="handleFieldChange"
/>
<!-- 手机号不可修改 -->
<common-cell name="手机号 (不可修改)">
<view class="form-content__wrapper">
<view class="flex-main-content text-dark">{{ formData.mobile }}</view>
</view> </view>
</common-cell>
<!-- 岗位 -->
<form-select
name="岗位"
:form="formData"
title="position"
:range="positionOptions"
@change="handleFieldChange"
/>
<!-- 职称 -->
<form-select
name="职称"
:form="formData"
title="title"
:range="titleOptions"
@change="handleFieldChange"
/>
<!-- 科室 -->
<common-cell name="科室">
<view class="form-content__wrapper" @click="openDepartmentSelect">
<view
class="flex-main-content text-right"
:class="{ 'text-placeholder': !formData.departmentName }"
>
{{ formData.departmentName || "请选择" }}
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 个人介绍 --> <view class="bg-white px-10 mb-10 rounded">
<form-textarea <!-- 填写认证资料的时候岗位必填 -->
name="个人介绍" <common-cell :required="type === 'cert'" title="job" :name="rule.job.name">
:form="formData" <view class="flex-grow flex items-center justify-end" @click="selectJob()">
title="intro" <view v-if="jobStr" class="text-base text-base">{{ jobStr }}</view>
:word-limit="500" <!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
@change="handleFieldChange" <uni-icons color="#999" type="right" size="16" />
/>
</view> </view>
<!-- 底部按钮 --> </common-cell>
<view class="button-footer"> <common-cell title="title" :name="rule.title.name">
<view class="btn btn-cancel" @click="handleCancel">取消</view> <view class="flex-grow flex items-center justify-end">
<view class="btn btn-save" @click="handleSave">保存</view> <!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
<uni-icons color="#999" type="right" size="16" />
</view>
</common-cell>
<common-cell title="dept" :name="rule.dept.name">
<view class="flex-grow flex items-center justify-end">
<!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
<uni-icons color="#999" type="right" size="16" />
</view>
</common-cell>
</view>
<view class="bg-white rounded">
<form-textarea autoHeight :border="false" :form="formData" title="intro" name="个人介绍" :wordLimit="300"
@change="onChange($event)" />
</view> </view>
</view> </view>
<template #footer>
<button-footer :cancelText="cancelText" :confirmText="confirmText" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import useGuard from "@/hooks/useGuard.js";
import { chooseAndUploadImage } from "@/utils/file.js";
import useAccountStore from "@/store/account.js";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import CommonCell from "@/components/form-template/common-cell.vue"; import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { upload } from "@/utils/http.js";
import { toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import commonCell from "@/components/form-template/common-cell.vue";
import FormInput from "@/components/form-template/form-cell/form-input.vue"; import FormInput from "@/components/form-template/form-cell/form-input.vue";
import FormSelect from "@/components/form-template/form-cell/form-select.vue"; import FormSelect from "@/components/form-template/form-cell/form-select.vue";
import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue"; import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue";
import api from "@/utils/api.js"; import fullPage from '@/components/full-page.vue';
const { account, doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad } = useGuard(); const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore(); const { getDoctorInfo } = useAccountStore();
//
const formData = ref({ const job = { assistant: '医生助理', doctor: '医生' };
anotherName: "",
avatar: "", const form = ref({});
gender: "", const type = ref('');
mobile: "", const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile }));
position: "", const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写');
title: "", const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存');
department: "", const jobStr = computed(() => {
departmentName: "", const jobs = formData.value && Array.isArray(formData.value.job) ? formData.value.job.filter(i => i === 'assistant' || i === 'doctor') : [];
departmentId: "", return jobs[0] && job[jobs[0]] ? job[jobs[0]] : '';
intro: "", })
}); const rule = computed(() => {
if (doctorInfo.value && ['verified', 'verifying'].includes(doctorInfo.value.verifyStatus)) {
return {
anotherName: { name: '姓名 (不可修改)', required: false, disabled: true },
job: { name: '岗位 (不可修改)', disabled: true },
title: { name: '职称 (不可修改)', disabled: true },
dept: { name: '科室 (不可修改)', disabled: true },
}
}
return {
anotherName: { name: '姓名', required: true, disabled: false },
job: { name: '岗位', disabled: false },
title: { name: '职称', disabled: false },
dept: { name: '科室', disabled: false },
}
})
// //
const genderOptions = [ const genderOptions = [
{ label: "男", value: "0" }, { label: "男", value: "0" },
{ label: "女", value: "1" }, { label: "女", value: "1" },
]; ];
const positionOptions = ["医生", "护士", "药师", "技师", "其他"];
const titleOptions = ["主任医师", "副主任医师", "主治医师", "医师", "其他"];
//
const handleFieldChange = (e) => {
if (e.title === "gender") {
formData.value[e.title] = e.value.value;
} else {
formData.value[e.title] = e.value;
}
};
//
const chooseAvatar = async () => {
const url = await chooseAndUploadImage();
if (url) {
formData.value.avatar = url;
}
};
const handleCancel = () => {
uni.navigateBack();
};
//
const handleSave = async () => {
createDoctorInfo();
};
useLoad(() => {
if (doctorInfo.value) {
formData.value = { ...doctorInfo.value };
} else {
formData.value.mobile = account.value.mobile;
}
});
//
const createDoctorInfo = async () => {
if (!formData.value.anotherName) {
uni.showToast({
title: "请输入姓名",
icon: "none",
});
return;
}
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
mobile: formData.value.mobile,
weChatOpenId: account.value.openid,
deptIds: [],
loginTypes: ["wxApp"],
corpId: account.value.corpId,
};
const res = await api("addCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "创建成功",
icon: "success",
});
await getDoctorInfo();
uni.switchTab({
url: "/pages/work/work"
});
} else {
uni.showToast({
title: "创建失败",
icon: "none",
});
console.error("创建医生信息失败:", res);
}
};
const updateDoctorInfo = async () => {
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
};
const res = await api("updateCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "更新成功",
icon: "success",
});
}
await getDoctorInfo();
uni.switchTab({
url: "/pages/work/work"
});
};
// //
const openDepartmentSelect = () => { const openDepartmentSelect = () => {
uni.navigateTo({ uni.navigateTo({
@ -223,89 +116,107 @@ const openDepartmentSelect = () => {
}, },
}); });
}; };
function back() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: "/pages/work/work",
});
}
}
function chooseAvatar() {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
const url = await upload(path);
if (url) {
form.value.avatar = url;
} else {
toast('上传失败')
}
}
})
}
function onChange({ title, value }) {
form.value[title] = value
}
function selectJob() {
if (rule.value.job.disabled) return;
uni.showActionSheet({
itemList: ['医生', '医生助理', '无'],
success: ({ tapIndex }) => {
const job = ['doctor', 'assistant',][tapIndex];
form.value.job = job ? [job] : [];
}
})
}
function toCert() {
if (jobStr.value === '医生') {
uni.navigateTo({
url: '/pages/work/verify/doctor'
})
} else if (jobStr.value === '医生助理') {
uni.navigateTo({
url: '/pages/work/verify/assistant'
})
} else {
toast('请选择岗位信息')
}
}
async function save() {
if (typeof formData.value.anotherName !== 'string' || !formData.value.anotherName.trim()) {
return toast('请输入姓名')
}
if (type.value === 'cert' && !jobStr.value) {
return toast('请选择岗位信息')
}
const apiName = doctorInfo.value ? 'updateCorpMemberFromWxapp' : 'addCorpMemberFromWxapp';
const data = {
...form.value,
weChatOpenId: account.value.openid,
mobile: account.value.mobile,
corpId: account.value.corpId,
}
if (doctorInfo.value) {
data.id = doctorInfo.value._id;
}
const res = await api(apiName, data);
if (res && res.success) {
await getDoctorInfo()
form.value = {};
if (type.value === 'cert') {
toCert()
} else {
await toast('保存成功');
back()
}
} else {
await toast(res?.message || '保存失败');
}
}
useLoad(opts => {
type.value = opts?.type;
})
useShow(() => {
getDoctorInfo()
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.profile-page { .avatar {
min-height: 100vh; width: 64rpx;
background: #f5f5f5; height: 64rpx;
padding-bottom: 120rpx;
}
.form-section {
margin-top: 20rpx;
}
.form-content__wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
text-align: right;
font-size: 28rpx;
input {
text-align: right;
}
}
.text-placeholder {
color: #999;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
}
.avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
.avatar-icon {
font-size: 48rpx;
color: #999;
}
}
.button-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx 30rpx;
background: #fff;
border-top: 1px solid #eee;
gap: 20rpx;
z-index: 100;
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 8rpx;
font-size: 32rpx;
}
.btn-cancel {
background: #fff;
border: 1px solid #007aff;
color: #007aff;
}
.btn-save {
background: #007aff;
color: #fff;
}
} }
</style> </style>

View File

@ -0,0 +1,179 @@
<template>
<full-page pageClass="bg-white">
<view class="p-15">
<view class="mt-15 title-bar relative font-semibold">
<text class="mr-5 text-dark text-base">请上传</text>
<text class="text-primary text-base">身份证正反面</text>
</view>
<view class="mt-12 flex items-center justify-between">
<view class="album border rounded overflow-hidden" @click="uploadIdCard('idCardFront')">
<image v-if="form.idCardFront" class="w-full h-full" :src="form.idCardFront" />
<view v-else class="relative w-full h-full ">
<image class="absolute w-full h-full" src="/static/work/fIDCard.png" />
<view class="absolute w-full h-full flex flex-col items-center justify-center">
<image class="carmra-icon" src="/static/work/camera.png" />
<view class="text-lg font-semibold color-428">上传人像面</view>
</view>
</view>
</view>
<view class="album border rounded overflow-hidden" @click="uploadIdCard('idCardBack')">
<image v-if="form.idCardBack" class="w-full h-full" :src="form.idCardBack" />
<view v-else class="relative w-full h-full ">
<image class="absolute w-full h-full" src="/static/work/aIDCard.png" />
<view class="absolute w-full h-full flex flex-col items-center justify-center">
<image class="carmra-icon" src="/static/work/camera.png" />
<view class="text-lg font-semibold color-428">上传国徽面</view>
</view>
</view>
</view>
</view>
<view class="exam px-15 rounded-sm leading-normal text-dark text-base">
身份证示例
</view>
<view class="mt-12 flex items-center justify-between">
<image class="album" src="/static/work/cardFront.png" />
<image class="album" src="/static/work/cardBack.png" />
</view>
<view class="mt-12 text-dark font-semibold text-base">拍摄须知</view>
<view class="mt-10 flex items-center justify-between">
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/fIDCard.png" />
<image class="mb-5 status-icon" src="/static/work/hook.png" />
<view class="text-base text-dark">标准</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/lackCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">缺边</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/vagueCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">模糊</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/flashCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">闪光</view>
</view>
</view>
</view>
<template #footer>
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { upload } from "@/utils/http.js";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
useGuard();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const form = ref({})
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
function back() {
uni.navigateBack()
}
function uploadIdCard(type) {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
showLoading('正在上传')
const url = await upload(path);
hideLoading()
if (url) {
form.value[type] = url;
} else {
toast('上传失败')
}
}
})
}
async function save() {
if (typeof formData.value.idCardFront !== 'string' || formData.value.idCardFront.trim() === '') {
return toast('请上传人像面')
}
if (typeof formData.value.idCardBack !== 'string' || formData.value.idCardBack.trim() === '') {
return toast('请上传国徽面')
}
const data = {
idCardFront: formData.value.idCardFront,
idCardBack: formData.value.idCardBack,
corpId: account.value.corpId,
weChatOpenId: account.value.openid,
id: doctorInfo.value._id,
}
const res = await api('submitCertProfile', data);
if (res && res.success) {
await toast('提交成功');
uni.navigateBack({ delta: 2 })
} else {
toast(res.message || '提交失败')
}
console.log('form.value: ', formData.value)
// uni.showToast({ title: '', icon: 'none' })
}
</script>
<style scoped>
.title-bar {
padding-left: 20rpx;
}
.title-bar::before {
content: "";
position: absolute;
left: 0;
top: 50%;
height: 80%;
width: 8rpx;
transform: translateY(-50%);
border-radius: 4rpx;
background: #0074ff;
}
.album {
width: 330rpx;
height: 200rpx;
}
.carmra-icon {
width: 96rpx;
height: 96rpx;
}
.color-428 {
color: #428bf0;
}
.exam {
margin-top: 80rpx;
background: #dde6f6;
padding: 8rpx 30rpx;
width: fit-content;
}
.exam-icon {
width: 160rpx;
height: 104rpx;
}
.status-icon {
width: 40rpx;
height: 40rpx;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<full-page pageClass="bg-white">
<view class="p-15">
<view class="title-bar relative text-dark text-base font-semibold" @click="log()">请填写执业医院</view>
<!-- <view class="mt-12 p-10 flex items-center justify-between border rounded">
<view class="w-0 flex-grow truncate text-base mr-10" :class="formData.hospitalName ? 'text-dark' : 'text-gray'">
{{ formData.hospitalName || '请填写执业医院' }}
</view>
<uni-icons class="flex-shrink-0" color="#666" type="right" size="16" />
</view> -->
<view class="mt-12 p-10 flex items-center justify-between border rounded">
<input class="w-0 flex-grow text-base mr-10" :value="formData.hospitalName" @change="inputHospitalName($event)"
placeholder="请填写执业医院" />
</view>
<view class="mt-15 title-bar relative font-semibold">
<text class="mr-5 text-dark text-base">请上传</text>
<text class="text-primary text-base">医师执业资格证</text>
</view>
<view class="mt-12 flex items-center justify-between">
<view class="album border rounded overflow-hidden" @click="uploadLicense('medicalLicenseFront')">
<image v-if="formData.medicalLicenseFront" class="w-full h-full" :src="formData.medicalLicenseFront" />
<view v-else class="w-full h-full flex flex-col items-center justify-center">
<uni-icons color="#666" type="camera" size="36" />
<view class="text-dark text-base">上传第一页</view>
</view>
</view>
<view class="album border rounded overflow-hidden" @click="uploadLicense('medicalLicenseBack')">
<image v-if="formData.medicalLicenseBack" class="w-full h-full" :src="formData.medicalLicenseBack" />
<view v-else class="w-full h-full flex flex-col items-center justify-center">
<uni-icons color="#666" type="camera" size="36" />
<view class="text-dark text-base">上传第二页</view>
</view>
</view>
</view>
<view class="exam px-15 rounded-sm leading-normal text-dark text-base">
证书示例
</view>
<view class="mt-10 text-sm text-dark leading-normal">1确保姓名照片编号执业范围签发机关等清晰可见</view>
<view class="mt-10 text-sm text-dark leading-normal">2需上传证书第一第二页图片仅供参考以实际证书为准</view>
<view class="mt-12 flex items-center justify-between">
<image class="album" src="/static/work/licenseFront.png" />
<image class="album" src="/static/work/licenseBack.png" />
</view>
</view>
<template #footer>
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { upload } from "@/utils/http.js";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
useGuard();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const form = ref({});
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
function back() {
uni.navigateBack()
}
function inputHospitalName(e) {
form.value.hospitalName = e.detail.value
}
function uploadLicense(type) {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
showLoading('正在上传')
const url = await upload(path);
hideLoading()
if (url) {
form.value[type] = url;
} else {
toast('上传失败')
}
}
})
}
async function save() {
if (typeof formData.value.hospitalName !== 'string' || formData.value.hospitalName.trim() === '') {
return toast('请填写执业医院')
}
if (typeof formData.value.medicalLicenseFront !== 'string' || formData.value.medicalLicenseFront.trim() === '') {
return toast('请上传医师执业资格证第一页')
}
if (typeof formData.value.medicalLicenseBack !== 'string' || formData.value.medicalLicenseBack.trim() === '') {
return toast('请上传医师执业资格证第二页')
}
const data = {
hospitalName: formData.value.hospitalName,
medicalLicenseFront: formData.value.medicalLicenseFront,
medicalLicenseBack: formData.value.medicalLicenseBack,
corpId: account.value.corpId,
weChatOpenId: account.value.openid,
id: doctorInfo.value._id,
}
const res = await api('submitCertProfile', data);
if (res && res.success) {
await toast('提交成功');
uni.navigateBack({ delta: 2 })
} else {
toast(res.message || '提交失败')
}
// uni.showToast({ title: '', icon: 'none' })
}
function log() {
console.log(doctorInfo.value)
console.log(formData.value)
}
</script>
<style scoped>
.title-bar {
padding-left: 20rpx;
}
.title-bar::before {
content: "";
position: absolute;
left: 0;
top: 50%;
height: 80%;
width: 8rpx;
transform: translateY(-50%);
border-radius: 4rpx;
background: #0074ff;
}
.album {
width: 330rpx;
height: 240rpx;
}
.exam {
margin-top: 80rpx;
background: #dde6f6;
padding: 8rpx 30rpx;
width: fit-content;
}
</style>

View File

@ -1,30 +1,30 @@
<template> <template>
<view class="work-page"> <full-page :showSafeArea="false" :customScroll="true">
<!-- 顶部用户信息区域 --> <template #header>
<view class="user-header bg-white px-15 py-15"> <view class="user-header bg-white px-15 py-15">
<view class="flex items-center justify-between"> <view class="flex items-center justify-between" @click="editProfile()">
<!-- 左侧用户头像和信息 -->
<view class="flex items-center flex-grow"> <view class="flex items-center flex-grow">
<view class="user-avatar mr-10"> <view class="relative user-avatar mr-10">
<image <image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
class="avatar-img" :src="doctorInfo.avatar" mode="aspectFill" />
src="/static/default-avatar.png" <image v-else class="avatar-img rounded-full overflow-hidden" src="/static/default-avatar.png"
mode="aspectFill" mode="aspectFill" />
/> <view v-if="doctorInfo" class="edit-sub flex items-center justify-center rounded-full bg-primary">
<image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" />
</view>
</view> </view>
<view class="flex-col"> <view class="flex-col">
<text class="user-name text-black text-lg font-semibold" <text v-if="doctorInfo && doctorInfo.anotherName" class="user-name text-dark text-lg font-semibold">
>请完善信息</text {{ doctorInfo.anotherName }}
> </text>
<text v-else class="user-name text-black text-lg font-semibold">请完善信息</text>
<view class="flex items-center mt-5"> <view class="flex items-center mt-5">
<view <view v-if="!doctorInfo || !doctorInfo.anotherName" class="status-tag tag-orange mr-10">
class="status-tag tag-orange mr-10"
@click="handleCompleteInfo"
>
<text class="tag-text text-white">信息待完善</text> <text class="tag-text text-white">信息待完善</text>
</view> </view>
<view class="status-tag tag-gray" @click="handleVerify"> <view v-if="certStatus" class="px-10 py-3 text-sm rounded-full" :class="certStatus.classnames"
<text class="tag-text text-dark">未认证</text> @click.stop="handleCert()">
{{ certStatus.text }}
</view> </view>
</view> </view>
</view> </view>
@ -32,49 +32,64 @@
<!-- 右侧操作按钮 --> <!-- 右侧操作按钮 -->
<view class="flex items-center"> <view class="flex items-center">
<view <view class="action-btn flex-col items-center mr-10" @click="handleInvitePatient">
class="action-btn flex-col items-center mr-15" <image class="mb-5 qrcode-icon" src="/static/work/qrcode.svg" />
@click="handleInvitePatient" <text class="action-text text-dark text-sm">邀请</text>
>
<view class="qrcode-icon">
<text class="qrcode-text"></text>
</view> </view>
<text class="action-text text-black text-sm mt-5">邀请患者</text> <view class="action-btn flex-col items-center" @click="handleMore">
</view> <image class="mb-5 qrcode-icon" src="/static/work/more.svg" />
<view class="more-btn" @click="handleMore"> <text class="action-text text-dark text-sm">更多</text>
<text class="more-icon"></text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<view class="mt-15 px-15 py-12 text-dark text-lg font-semibold bg-white border-b">
待办列表11
</view>
</template>
<!-- 待办列表区域 --> <scroll-view v-if="list.length" scroll-y="true" class="h-full bg-white">
<view class="todo-section px-15 mt-15"> <view class="p-15">
<view class="section-title mb-15"> <view v-for="i in 10" class="p-15 bg-primary mb-10"></view>
<text class="text-black text-lg font-semibold">待办列表11</text>
</view> </view>
</scroll-view>
<!-- 空状态 --> <view v-else class="flex flex-col items-center justify-center h-full bg-white">
<view class="empty-state">
<empty-data text="暂无记录" /> <empty-data text="暂无记录" />
</view> </view>
</view> <template #footer>
</view> <view class="border-b"></view>
</template>
</full-page>
<cert-popup :visible="visible" @close="visible = false" />
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue';
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js"; import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import certPopup from "./components/cert-popup.vue";
import EmptyData from "@/components/empty-data.vue"; import EmptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue';
import { toast } from '@/utils/widget';
const { useLoad } = useGuard(); const certConfig = {
failed: { text: '认证失败', classnames: 'bg-danger text-white' },
// verified: { text: '已认证', classnames: 'bg-success text-white' },
const handleCompleteInfo = () => { verifying: { text: '认证中', classnames: 'bg-warning text-white' },
uni.navigateTo({ unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
url: "/pages/work/profile",
});
}; };
const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const list = ref([1]);
const visible = ref(false);
const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null)
// //
const handleVerify = () => { const handleVerify = () => {
uni.showToast({ uni.showToast({
@ -101,15 +116,41 @@ const handleMore = () => {
}); });
}; };
function editProfile() {
uni.navigateTo({
url: "/pages/work/profile",
});
}
function handleCert() {
if (doctorInfo.value.verifyStatus === 'verifying') {
toast('信息认证中,请耐心等待!')
} else {
visible.value = true
}
}
useLoad(() => { useLoad(() => {
console.log("工作台页面加载"); console.log("工作台页面加载");
}); });
useShow(() => {
getDoctorInfo();
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.work-page { .edit-sub {
min-height: 100vh; width: 36rpx;
background: #f5f5f5; height: 36rpx;
position: absolute;
right: 0;
bottom: 0;
}
.edit-icon {
width: 24rpx;
height: 24rpx;
} }
.user-header { .user-header {
@ -119,9 +160,6 @@ useLoad(() => {
.user-avatar { .user-avatar {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%;
overflow: hidden;
background: #e5e5e5;
.avatar-img { .avatar-img {
width: 100%; width: 100%;
@ -133,6 +171,11 @@ useLoad(() => {
line-height: 1.5; line-height: 1.5;
} }
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
}
.status-tag { .status-tag {
padding: 6rpx 16rpx; padding: 6rpx 16rpx;
border-radius: 20rpx; border-radius: 20rpx;
@ -162,17 +205,8 @@ useLoad(() => {
justify-content: center; justify-content: center;
.qrcode-icon { .qrcode-icon {
width: 48rpx; width: 40rpx;
height: 48rpx; height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.qrcode-text {
font-size: 40rpx;
color: #333;
line-height: 1;
}
} }
.action-text { .action-text {
@ -209,5 +243,4 @@ useLoad(() => {
// align-items: center; // align-items: center;
// justify-content: center; // justify-content: center;
// width: 100%; // width: 100%;
// } // }</style>
</style>

View File

@ -9,8 +9,12 @@ export default [
}, },
{ {
path: 'pages/message/message', path: 'pages/message/message',
meta: { title: '消息', login: false }, meta: { title: '消息' }
style: { navigationStyle: 'custom' } },
{
path: 'pages/message/index',
meta: { title: '聊天' },
style: { enablePullDownRefresh: false }
}, },
{ {
path: 'pages/message/index', path: 'pages/message/index',
@ -103,14 +107,30 @@ export default [
}, },
{ {
path: 'pages/work/work', path: 'pages/work/work',
meta: { title: '工作台', login: false } meta: { title: '工作台' }
},
{
path: 'pages/case/case',
meta: { title: '病例' }
}, },
{ {
path: 'pages/work/profile', path: 'pages/work/profile',
meta: { title: '完善个人信息', login: false } meta: { title: '完善个人信息' }
}, },
{ {
path: 'pages/work/department-select', path: 'pages/work/department-select',
meta: { title: '选择科室', login: false } meta: { title: '选择科室' }
} },
{
path: 'pages/work/verify/assistant',
meta: { title: '上传证照', login: true }
},
{
path: 'pages/work/verify/doctor',
meta: { title: '上传证照', login: true }
},
{
path: 'pages/login/login',
meta: { title: '授权登录' }
},
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

BIN
static/work/aIDCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
static/work/camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/work/cardBack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
static/work/cardFront.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
static/work/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

BIN
static/work/fIDCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
static/work/flashCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
static/work/hook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/work/lackCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
static/work/licenseBack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

1
static/work/more.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769069890058" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14332" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M378.112 481.536H159.488c-51.2 0-92.928-41.728-92.928-92.928V169.984c0-51.2 41.728-92.928 92.928-92.928h218.624c51.2 0 92.928 41.728 92.928 92.928v218.624c0 51.2-41.728 92.928-92.928 92.928zM159.488 138.752c-17.408 0-31.488 14.08-31.488 31.488v218.624c0 17.408 14.08 31.488 31.488 31.488h218.624c17.408 0 31.488-14.08 31.488-31.488V169.984c0-17.408-14.08-31.488-31.488-31.488H159.488zM378.112 949.504H159.488c-51.2 0-92.928-41.728-92.928-92.928v-218.624c0-51.2 41.728-92.928 92.928-92.928h218.624c51.2 0 92.928 41.728 92.928 92.928v218.624c0 51.456-41.728 92.928-92.928 92.928zM159.488 606.72c-17.408 0-31.488 14.08-31.488 31.488v218.624c0 17.408 14.08 31.488 31.488 31.488h218.624c17.408 0 31.488-14.08 31.488-31.488v-218.624c0-17.408-14.08-31.488-31.488-31.488H159.488zM680.448 480l-143.616-144.128c-33.536-33.792-33.536-88.576 0.256-122.112l144.128-143.616c33.792-33.536 88.576-33.536 122.112 0.256l143.616 144.128c33.536 33.792 33.536 88.576-0.256 122.112l-144.128 143.616c-33.792 33.536-88.32 33.536-122.112-0.256zM762.88 110.848c-11.264-11.52-29.952-11.52-41.216 0l-144.128 143.616c-11.52 11.264-11.52 29.952 0 41.216l143.616 144.128c11.264 11.52 29.952 11.52 41.216 0l144.128-143.616c11.52-11.264 11.52-29.952 0-41.216L762.88 110.848zM840.96 949.504l-218.624-0.512c-51.2 0-92.672-41.728-92.672-92.928l0.512-218.624c0-51.2 41.728-92.672 92.928-92.672l218.624 0.512c51.2 0 92.672 41.728 92.672 92.928l-0.512 218.624c0 51.2-41.728 92.928-92.928 92.672z m-217.856-343.04c-17.408 0-31.488 14.08-31.488 31.232l-0.512 218.624c0 17.408 14.08 31.488 31.232 31.488l218.624 0.512c17.408 0 31.488-14.08 31.488-31.232l0.512-218.624c0-17.408-14.08-31.488-31.232-31.488l-218.624-0.512z" fill="#2c2c2c" p-id="14333"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

1
static/work/pen.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769067872142" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10445" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M581.50172 186.48l256.04 256.04-555.98 555.98-228.28 25.2C22.72172 1027.08-3.09828 1001.24 0.30172 970.68l25.4-228.44 555.8-555.76z m414.4-38.12l-120.22-120.22c-37.5-37.5-98.32-37.5-135.82 0l-113.1 113.1 256.04 256.04 113.1-113.1c37.5-37.52 37.5-98.32 0-135.82z" fill="#ffffff" p-id="10446"></path></svg>

After

Width:  |  Height:  |  Size: 638 B

1
static/work/qrcode.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769069475491" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11505" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M85.312 85.312V384H384V85.312H85.312zM0 0h469.248v469.248H0V0z m170.624 170.624h128v128h-128v-128zM0 554.624h469.248v469.248H0V554.624z m85.312 85.312v298.624H384V639.936H85.312z m85.312 85.312h128v128h-128v-128zM554.624 0h469.248v469.248H554.624V0z m85.312 85.312V384h298.624V85.312H639.936z m383.936 682.56H1024v85.376h-298.752V639.936H639.936V1023.872H554.624V554.624h255.936v213.248h128V554.624h85.312v213.248z m-298.624-597.248h128v128h-128v-128z m298.624 853.248h-85.312v-85.312h85.312v85.312z m-213.312 0h-85.312v-85.312h85.312v85.312z" fill="#262626" p-id="11506"></path></svg>

After

Width:  |  Height:  |  Size: 919 B

BIN
static/work/vagueCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -8,17 +8,29 @@ const env = __VITE_ENV__;
export default defineStore("accountStore", () => { export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID; const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const account = ref(null); const account = ref(null);
const loading = ref(false) const loading = ref(false);
const loginPromise = ref(null);
// IM 相关 // IM 相关
const openid = ref(""); const openid = ref("");
const isIMInitialized = ref(false); const isIMInitialized = ref(false);
// 医生信息 // 医生信息
const doctorInfo = ref(null); const doctorInfo = ref(null);
async function login(phoneCode = '') { function getLoginPromise(phoneCode = '') {
if (loading.value) return; if (loginPromise.value) return loginPromise.value;
loading.value = true; loginPromise.value = loginByCode(phoneCode);
return loginPromise.value;
}
async function login(phoneCode) {
const res = await getLoginPromise(phoneCode);
loginPromise.value = null;
return res
}
async function loginByCode(phoneCode = '') {
try { try {
const { code } = await uni.login({ const { code } = await uni.login({
appid, appid,
@ -29,8 +41,8 @@ export default defineStore("accountStore", () => {
const res = await api('wxAppLogin', { const res = await api('wxAppLogin', {
phoneCode, phoneCode,
code, code,
corpId,
}); });
loading.value = false;
if (res.success && res.data) { if (res.success && res.data) {
if (!res.data.mobile) { if (!res.data.mobile) {
const target = '/pages/login/login'; const target = '/pages/login/login';
@ -39,27 +51,26 @@ export default defineStore("accountStore", () => {
} }
account.value = res.data; account.value = res.data;
openid.value = res.data.openid; openid.value = res.data.openid;
// 登录成功后初始化腾讯IM // 登录成功后初始化腾讯IM
try { // try {
console.log('开始初始化腾讯IMuserID:', res.data.openid); // console.log('开始初始化腾讯IMuserID:', res.data.openid);
await initGlobalTIM(res.data.openid); // await initGlobalTIM(res.data.openid);
isIMInitialized.value = true; // isIMInitialized.value = true;
console.log('腾讯IM初始化成功'); // console.log('腾讯IM初始化成功');
} catch (imError) { // } catch (imError) {
console.error('腾讯IM初始化失败:', imError); // console.error('腾讯IM初始化失败:', imError);
// IM初始化失败不影响登录流程 // // IM初始化失败不影响登录流程
} // }
await getDoctorInfo(openid.value); await getDoctorInfo(openid.value);
return res.data return res.data
} }
} }
toast('登录失败,请重新登录'); toast('登录失败,请重新登录');
} catch (e) { } catch (e) {
toast('登录失败,请重新登录'); toast('登录失败,请重新登录');
} }
loading.value = false return Promise.reject()
} }
async function getDoctorInfo() { async function getDoctorInfo() {

View File

@ -12,13 +12,41 @@ const urlsConfig = {
getHospitalList: 'getRealHospital', getHospitalList: 'getRealHospital',
addCorpMember: 'addCorpMember', addCorpMember: 'addCorpMember',
getCorpMemberData: 'getCorpMemberData', getCorpMemberData: 'getCorpMemberData',
updateCorpMember: 'updateCorpMember' updateCorpMember: 'updateCorpMember',
addCorpMemberFromWxapp: "addCorpMemberFromWxapp",
updateCorpMemberFromWxapp: "updateCorpMemberFromWxapp",
submitCertProfile: 'submitCertProfile',
getMemberVerifyStatus: "getMemberVerifyStatus"
}, },
knowledgeBase: { knowledgeBase: {
getArticleByIds: 'getArticleByIds', getArticleByIds: 'getArticleByIds',
// 诊断库(对齐 ykt-management-mobile/src/api/knowledgeBase.js // 诊断库(对齐 ykt-management-mobile/src/api/knowledgeBase.js
getDisease: 'getDisease', getDisease: 'getDisease',
getCommonPhrases: 'getCommonPhrases',
saveCommonPhrase: 'saveCommonPhrase',
deleteCommonPhrase: 'deleteCommonPhrase',
getCommonPhraseCategories: 'getCommonPhraseCategories',
saveCommonPhraseCategory: 'saveCommonPhraseCategory',
// 个人常用语接口
getPersonalPhrases: 'getPersonalPhrases',
savePersonalPhrase: 'savePersonalPhrase',
deletePersonalPhrase: 'deletePersonalPhrase',
getPersonalPhraseCategories: 'getPersonalPhraseCategories',
savePersonalPhraseCategory: 'savePersonalPhraseCategory',
deletePersonalPhraseCategory: 'deletePersonalPhraseCategory',
// 宣教文章接口
getArticleCateList: 'getArticleCateList',
getArticleList: 'getArticleList',
getArticle: 'getArticle',
addArticleSendRecord: 'addArticleSendRecord'
},
survery: {
getSurveyCateList: 'getSurveryCateList',
getSurveyList: 'getList',
createSurveyRecord: 'createRecord',
getSurveyDetail: 'getDetail'
}, },
member: { member: {
addCustomer: 'add', addCustomer: 'add',
@ -70,6 +98,8 @@ const urlsConfig = {
addServiceRecord: 'addServiceRecord', addServiceRecord: 'addServiceRecord',
updateServiceRecord: 'updateServiceRecord', updateServiceRecord: 'updateServiceRecord',
removeServiceRecord: 'removeServiceRecord', removeServiceRecord: 'removeServiceRecord',
getChatRecordsByGroupId: "getChatRecordsByGroupId",
sendConsultRejectedMessage: "sendConsultRejectedMessage"
} }
} }
@ -102,3 +132,50 @@ export default async function api(urlId, data) {
} }
}) })
} }
// 宣教文章相关 API
export async function getArticleCateList(data) {
return api('getArticleCateList', data);
}
export async function getArticleList(data) {
return api('getArticleList', data);
}
export async function getArticle(data) {
return api('getArticle', data);
}
export async function addArticleSendRecord(data) {
return api('addArticleSendRecord', data);
}
// 问卷相关 API
export async function getSurveyCateList(data) {
return api('getSurveyCateList', data);
}
export async function getSurveyList(data) {
return api('getSurveyList', data);
}
export async function createSurveyRecord(data) {
return api('createSurveyRecord', data);
}
export async function getSurveyDetail(data) {
return api('getSurveyDetail', data);
}
// IM 系统消息相关 API
export async function sendConsultRejectedMessage(data) {
return request({
url: '/getYoucanData/im',
data: {
type: 'sendConsultRejectedMessage',
...data
}
});
}

View File

@ -25,10 +25,10 @@ export const checkConsultationStatus = (waitingForDoctor, consultationEnded) =>
return true; return true;
}; };
//
// 检查IM连接状态 // 检查IM连接状态
export const checkIMConnection = (timChatManager) => { export const checkIMConnection = (timChatManager) => {
if (!timChatManager.tim || !timChatManager.isLoggedIn) { if (!timChatManager.tim || !timChatManager.isLoggedIn) {
// showMessage("IM连接异常请重新进入");
return false; return false;
} }
return true; return true;
@ -346,133 +346,31 @@ export const chooseMedia = async (options, onSuccess, onFail) => {
}; };
/** /**
* 选择图片针对 TIM SDK 优化 * 选择图片
* @param {function} onSuccess - 成功回调 * @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调 * @param {function} onFail - 失败回调
*/ */
export const chooseImage = async (onSuccess, onFail) => { export const chooseImage = (onSuccess, onFail) => {
// 检查权限
const sourceType = ['album', 'camera'];
if (sourceType.includes('album')) {
const hasPermission = await checkAlbumPermission();
if (!hasPermission) {
console.log('用户未授予相册权限');
if (onFail) {
onFail({ errMsg: '未授权相册权限' });
}
return;
}
}
// 使用 wx.chooseImage 以确保与 TIM SDK 兼容
// #ifdef MP-WEIXIN
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: sourceType,
success: function (res) {
console.log('wx.chooseImage 成功,完整返回数据:', JSON.stringify(res));
console.log('tempFilePaths:', res.tempFilePaths);
console.log('tempFiles:', res.tempFiles);
// TIM SDK 需要完整的 wx.chooseImage 返回对象,而不是单个文件
// 直接传递整个 res 对象
if (onSuccess) onSuccess(res);
},
fail: function (err) {
// 用户取消选择
if (err.errMsg && err.errMsg.includes('cancel')) {
console.log('用户取消选择');
return;
}
// 权限相关错误
if (err.errMsg && (err.errMsg.includes('permission') || err.errMsg.includes('auth') || err.errMsg.includes('拒绝'))) {
console.error('相册权限被拒绝:', err);
uni.showModal({
title: '需要相册权限',
content: '请在设置中开启相册权限后重试',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting();
}
}
});
if (onFail) {
onFail(err);
}
return;
}
// 其他错误
console.error('选择图片失败:', err);
if (onFail) {
onFail(err);
} else {
showMessage('选择图片失败,请重试');
}
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境,使用 uni.chooseMedia
chooseMedia({ chooseMedia({
count: 1, count: 1,
mediaType: ['image'], mediaType: ['image'],
sizeType: ['original', 'compressed'], sizeType: ['original', 'compressed'],
sourceType: sourceType sourceType: ['album', 'camera']
}, onSuccess, onFail); }, onSuccess, onFail);
// #endif
}; };
/** /**
* 拍照针对 TIM SDK 优化 * 拍照
* @param {function} onSuccess - 成功回调 * @param {function} onSuccess - 成功回调
* @param {function} onFail - 失败回调 * @param {function} onFail - 失败回调
*/ */
export const takePhoto = (onSuccess, onFail) => { export const takePhoto = (onSuccess, onFail) => {
// 使用 wx.chooseImage 以确保与 TIM SDK 兼容
// #ifdef MP-WEIXIN
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'],
success: function (res) {
console.log('wx.chooseImage (拍照) 成功,完整返回数据:', JSON.stringify(res));
console.log('tempFilePaths:', res.tempFilePaths);
console.log('tempFiles:', res.tempFiles);
// TIM SDK 需要完整的 wx.chooseImage 返回对象
if (onSuccess) onSuccess(res);
},
fail: function (err) {
// 用户取消
if (err.errMsg && err.errMsg.includes('cancel')) {
console.log('用户取消拍照');
return;
}
console.error('拍照失败:', err);
if (onFail) {
onFail(err);
} else {
showMessage('拍照失败,请重试');
}
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境
chooseMedia({ chooseMedia({
count: 1, count: 1,
mediaType: ['image'], mediaType: ['image'],
sizeType: ['original', 'compressed'], sizeType: ['original', 'compressed'],
sourceType: ['camera'] sourceType: ['camera']
}, onSuccess, onFail); }, onSuccess, onFail);
// #endif
}; };
// ==================== 录音相关工具方法 ==================== // ==================== 录音相关工具方法 ====================
@ -588,8 +486,6 @@ export const sendCustomMessage = async (messageData, timChatManager, validateBef
* @param {function} onSuccess - 成功回调 * @param {function} onSuccess - 成功回调
*/ */
export const sendMessage = async (messageType, data, timChatManager, validateBeforeSend, onSuccess, cloudCustomData) => { export const sendMessage = async (messageType, data, timChatManager, validateBeforeSend, onSuccess, cloudCustomData) => {
console.log('chat-utils sendMessage 被调用:', { messageType, data });
if (!validateBeforeSend()) { if (!validateBeforeSend()) {
return; return;
} }
@ -601,9 +497,7 @@ export const sendMessage = async (messageType, data, timChatManager, validateBef
result = await timChatManager.sendTextMessage(data, cloudCustomData); result = await timChatManager.sendTextMessage(data, cloudCustomData);
break; break;
case 'image': case 'image':
console.log('准备发送图片消息,数据:', data);
result = await timChatManager.sendImageMessage(data, cloudCustomData); result = await timChatManager.sendImageMessage(data, cloudCustomData);
console.log('图片消息发送结果:', result);
break; break;
case 'voice': case 'voice':
result = await timChatManager.sendVoiceMessage(data.file, data.duration,cloudCustomData); result = await timChatManager.sendVoiceMessage(data.file, data.duration,cloudCustomData);
@ -614,7 +508,6 @@ export const sendMessage = async (messageType, data, timChatManager, validateBef
} }
if (result && result.success) { if (result && result.success) {
console.log('消息发送成功');
if (onSuccess) onSuccess(); if (onSuccess) onSuccess();
} else { } else {
console.error('发送消息失败:', result?.error); console.error('发送消息失败:', result?.error);

View File

@ -97,7 +97,7 @@ async function refreshAccessToken() {
const request = async (options = {}, showLoading = true) => { const request = async (options = {}, showLoading = true) => {
// 合并用户传入的配置和默认配置 // 合并用户传入的配置和默认配置
if (!options.data) options.data = {}; if (!options.data) options.data = {};
if(!options.data.corpId) { if (!options.data.corpId) {
options.data.corpId = env.MP_CORP_ID; options.data.corpId = env.MP_CORP_ID;
} }
@ -206,5 +206,28 @@ export default request;
export const uploadUrl = `${baseUrl}/upload`; export const uploadUrl = `${baseUrl}/upload`;
export function getFullPath(path) { export function getFullPath(path) {
return `${baseUrl}${path}`; return `${baseUrl}/${path}`;
} }
export function upload(path) {
return new Promise((resolve) => {
uni.uploadFile({
url: uploadUrl, // 替换为你的上传接口地址
filePath: path,
name: 'file',
fileType: 'image',
success: (res) => {
try {
const url = JSON.parse(res.data).filePath;
resolve(url ? getFullPath(url) : '')
} catch (e) {
resolve()
}
},
fail: res => {
resolve()
}
})
})
}

View File

@ -363,7 +363,6 @@ class TimChatManager {
} }
} }
// 获取用户信息并登录
async getUserInfoAndLogin(userID) { async getUserInfoAndLogin(userID) {
try { try {
if (userID) { if (userID) {
@ -376,7 +375,6 @@ class TimChatManager {
} }
this.currentUserID = userInfo.userID this.currentUserID = userInfo.userID
} }
this.currentUserSig = await this.getUserSig(this.currentUserID) this.currentUserSig = await this.getUserSig(this.currentUserID)
await this.loginTIM() await this.loginTIM()
} catch (error) { } catch (error) {
@ -711,26 +709,37 @@ class TimChatManager {
// 获取消息所属的会话ID // 获取消息所属的会话ID
const messageConversationID = convertedMessage.conversationID const messageConversationID = convertedMessage.conversationID
// 检查是否为系统消息
const isSystemMsg = this.isSystemMessage(convertedMessage)
console.log('收到新消息:', { console.log('收到新消息:', {
messageID: convertedMessage.ID, messageID: convertedMessage.ID,
messageConversationID: messageConversationID, messageConversationID: messageConversationID,
currentConversationID: this.currentConversationID, currentConversationID: this.currentConversationID,
messageType: convertedMessage.type, messageType: convertedMessage.type,
from: convertedMessage.from from: convertedMessage.from,
isSystemMessage: isSystemMsg
}) })
// 判断是否为当前会话的消息必须有currentConversationID且匹配才显示
const isCurrentConversation = this.currentConversationID && // 判断是否为当前会话的消息
messageConversationID === this.currentConversationID // 系统消息只要会话ID匹配就显示不要求必须有currentConversationID
// 普通消息必须有currentConversationID且匹配才显示
const isCurrentConversation = isSystemMsg
? messageConversationID === this.currentConversationID
: (this.currentConversationID && messageConversationID === this.currentConversationID)
console.log('消息会话匹配检查:', { console.log('消息会话匹配检查:', {
isCurrentConversation, isCurrentConversation,
isSystemMessage: isSystemMsg,
hasCurrentConversationID: !!this.currentConversationID, hasCurrentConversationID: !!this.currentConversationID,
conversationIDMatch: messageConversationID === this.currentConversationID conversationIDMatch: messageConversationID === this.currentConversationID
}) })
if (isCurrentConversation) { if (isCurrentConversation) {
// 当前会话的消息,触发回调 // 当前会话的消息,触发回调
console.log('✓ 消息属于当前会话,触发显示') console.log('✓ 消息属于当前会话,触发显示')
this.triggerCallback('onMessageReceived', convertedMessage) this.triggerCallback('onMessageReceived', convertedMessage)
// 处理已读状态 // 处理已读状态(系统消息也标记为已读)
if (this.currentConversationID) { if (this.currentConversationID) {
this.markConversationAsRead(this.currentConversationID) this.markConversationAsRead(this.currentConversationID)
} }
@ -1093,7 +1102,52 @@ class TimChatManager {
} else if (lastMessage.type === 'TIMSoundElem') { } else if (lastMessage.type === 'TIMSoundElem') {
lastMessageText = '[语音]' lastMessageText = '[语音]'
} else if (lastMessage.type === 'TIMCustomElem') { } else if (lastMessage.type === 'TIMCustomElem') {
lastMessageText = lastMessage.payload.data || '[自定义消息]' // 解析自定义消息
try {
const customData = JSON.parse(lastMessage.payload.data)
const messageType = customData.messageType
// 根据消息类型返回不同的预览文本
switch (messageType) {
case 'system_message':
lastMessageText = '[系统消息]'
break
case 'symptom':
lastMessageText = '[病情描述]'
break
case 'prescription':
lastMessageText = '[处方单]'
break
case 'refill':
lastMessageText = '[续方申请]'
break
case 'survey':
lastMessageText = '[问卷调查]'
break
case 'article':
lastMessageText = '[文章]'
break
case "consult_pending":
lastMessageText = '患者向团队发起咨询请在1小时内接诊超时将自动关闭会话'
break
case "consult_rejected":
lastMessageText = '患者向团队发起咨询,由于有紧急事务要处理暂时无法接受咨询.本次会话丿关闭'
break
case "consult_timeout":
lastMessageText = '患者向团队发起咨询,团队成员均未接受咨询,本次会话已自动关闭'
break
case "consult_accepted":
lastMessageText = '已接诊,会话已开始'
break
case "consult_ended":
lastMessageText = '已结束当前会话'
break
default:
lastMessageText = '[自定义消息]'
}
} catch (error) {
console.error('解析自定义消息失败:', error)
lastMessageText = '[自定义消息]'
}
} else { } else {
lastMessageText = '[未知消息类型]' lastMessageText = '[未知消息类型]'
} }
@ -2119,72 +2173,33 @@ class TimChatManager {
// 发送图片消息 // 发送图片消息
async sendImageMessage(imageFile) { async sendImageMessage(imageFile) {
console.log('sendImageMessage 被调用,参数:', imageFile);
if (!this.tim) { if (!this.tim) {
this.triggerCallback('onError', 'IM未初始化') this.triggerCallback('onError', 'IM未初始化')
return { success: false, error: 'IM未初始化' } return
} }
// 检查登录状态 if (!this.conversation) {
if (!this.isLoggedIn) { this.triggerCallback('onError', '群聊会话不存在')
console.error('IM未登录无法发送消息'); return { success: false, error: '群聊会话不存在' }
this.triggerCallback('onError', 'IM未登录请稍后重试')
return { success: false, error: 'IM未登录' }
} }
// 优先使用 currentConversationID如果没有则尝试从 conversation 获取 let groupID = null
let conversationID = this.currentConversationID; if (this.conversation.groupProfile && this.conversation.groupProfile.groupID) {
if (!conversationID && this.conversation) { groupID = this.conversation.groupProfile.groupID
conversationID = this.conversation.conversationID; } else if (this.conversation.conversationID) {
} groupID = this.conversation.conversationID.replace('GROUP', '')
if (!conversationID) {
console.error('会话ID不存在');
this.triggerCallback('onError', '会话不存在,请重新进入聊天')
return { success: false, error: '会话ID不存在' }
}
// 从 conversationID 提取 groupID
let groupID = null;
if (conversationID.startsWith('GROUP')) {
groupID = conversationID.replace('GROUP', '');
} else if (this.conversation?.groupProfile?.groupID) {
groupID = this.conversation.groupProfile.groupID;
} }
if (!groupID) { if (!groupID) {
console.error('无法获取群聊IDconversationID:', conversationID);
this.triggerCallback('onError', '无法获取群聊ID') this.triggerCallback('onError', '无法获取群聊ID')
return { success: false, error: '无法获取群聊ID' } return { success: false, error: '无法获取群聊ID' }
} }
console.log('发送图片消息conversationID:', conversationID, 'groupID:', groupID); // 确保使用当前会话的conversationID
const conversationID = this.conversation.conversationID || this.currentConversationID
// imageFile 现在是完整的 wx.chooseImage 返回对象 // 获取图片尺寸信息
console.log('接收到的图片选择结果:', imageFile); const imageInfo = await this.getImageInfo(imageFile);
console.log('类型:', typeof imageFile);
console.log('keys:', imageFile ? Object.keys(imageFile) : 'null');
// 验证对象
if (!imageFile) {
console.error('图片选择结果为空');
this.triggerCallback('onError', '图片文件无效');
return { success: false, error: '图片选择结果为空' };
}
// 获取文件路径用于显示预览
let previewPath = '';
if (imageFile.tempFilePaths && imageFile.tempFilePaths.length > 0) {
previewPath = imageFile.tempFilePaths[0];
} else if (imageFile.tempFiles && imageFile.tempFiles.length > 0) {
previewPath = imageFile.tempFiles[0].tempFilePath || imageFile.tempFiles[0].path;
}
console.log('预览路径:', previewPath);
// 获取图片尺寸信息(用于本地预览)
const imageInfo = await this.getImageInfo(previewPath);
const localMessage = { const localMessage = {
ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ID: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@ -2192,14 +2207,14 @@ class TimChatManager {
type: 'TIMImageElem', type: 'TIMImageElem',
payload: { payload: {
imageInfoArray: [{ imageInfoArray: [{
url: previewPath, url: this.getImageUrl(imageFile),
width: imageInfo.width, width: imageInfo.width,
height: imageInfo.height height: imageInfo.height
}] }]
}, },
lastTime: Date.now(), lastTime: Date.now(),
status: 'sending', status: 'sending',
avatar: '/static/center/user-avatar.png', avatar: '',
conversationID: conversationID, conversationID: conversationID,
from: this.currentUserID from: this.currentUserID
} }
@ -2211,45 +2226,22 @@ class TimChatManager {
// 触发消息接收回调让UI立即显示 // 触发消息接收回调让UI立即显示
this.triggerCallback('onMessageReceived', localMessage) this.triggerCallback('onMessageReceived', localMessage)
console.log('准备创建 TIM 图片消息groupID:', groupID, 'imageFile:', imageFile);
try {
// 创建图片消息 - 直接传递 wx.chooseImage 的完整返回对象
const message = this.tim.createImageMessage({ const message = this.tim.createImageMessage({
to: groupID, to: groupID,
conversationType: TIM.TYPES.CONV_GROUP, conversationType: TIM.TYPES.CONV_GROUP,
payload: { payload: { file: imageFile }
file: imageFile // 传递完整的 wx.chooseImage 返回对象
}
}) })
console.log('TIM 图片消息已创建:', message); try {
await this.tim.sendMessage(message)
console.log('开始发送图片消息...');
const sendResult = await this.tim.sendMessage(message);
console.log('图片消息发送成功:', sendResult);
localMessage.status = 'success' localMessage.status = 'success'
return { success: true, message: localMessage } return { success: true, message: localMessage }
} catch (error) { } catch (error) {
console.error('图片消息发送失败:', error) console.error('图片消息发送失败:', error)
console.error('错误详情:', {
message: error.message,
stack: error.stack,
imageFile: imageFile
});
localMessage.status = 'failed' localMessage.status = 'failed'
// 如果是因为未登录导致的失败,尝试重连
if (error.message && (error.message.includes('not login') || error.message.includes('sdk not ready'))) {
console.log('检测到未登录错误,尝试重连...');
this.isLoggedIn = false;
this.ensureIMConnection();
}
return { success: false, error } return { success: false, error }
} }
} }
// 发送语音消息 // 发送语音消息
async sendVoiceMessage(voiceFile, duration) { async sendVoiceMessage(voiceFile, duration) {
if (!this.tim) { if (!this.tim) {
@ -2424,6 +2416,41 @@ class TimChatManager {
} }
// 工具方法 // 工具方法
// 判断是否为系统消息
isSystemMessage(message) {
if (message.type !== 'TIMCustomElem') {
return false
}
// 检查 payload.data 是否包含系统消息标记
try {
if (message.payload && message.payload.data) {
const data = typeof message.payload.data === 'string'
? JSON.parse(message.payload.data)
: message.payload.data
// 检查是否为系统消息类型
if (data.type === 'system_message') {
return true
}
}
// 检查 description 是否为系统消息标记
if (message.payload && message.payload.description === '系统消息标记') {
return true
}
// 兼容旧的系统消息格式
if (message.payload && message.payload.description === 'SYSTEM_NOTIFICATION') {
return true
}
} catch (error) {
console.error('判断系统消息失败:', error)
}
return false
}
filterMessage(message) { filterMessage(message) {
if (message.type === 'TIMCustomElem' && message.payload && message.payload.data) { if (message.type === 'TIMCustomElem' && message.payload && message.payload.data) {
if (message.payload.data === 'group_create' || message.payload.data === 'purchased') { if (message.payload.data === 'group_create' || message.payload.data === 'purchased') {
@ -2497,40 +2524,42 @@ class TimChatManager {
} }
getImageUrl(imageFile) { getImageUrl(imageFile) {
// 支持 tempFilePath 或 path // 处理 tempFiles 数组格式
if (imageFile?.tempFilePath) { if (imageFile?.tempFiles?.length > 0) {
return imageFile.tempFilePath; return imageFile.tempFiles[0].tempFilePath
} }
if (imageFile?.path) { // 处理单个文件对象
return imageFile.path; if (imageFile?.tempFilePath) {
return imageFile.tempFilePath
} }
// 处理字符串路径 // 处理字符串路径
if (typeof imageFile === 'string') { if (typeof imageFile === 'string') {
return imageFile; return imageFile
} }
console.warn('无法获取图片URL使用默认图片:', imageFile); console.warn('无法获取图片URL使用默认图片:', imageFile);
return '/static/home/photo.png'; return '/static/home/photo.png'
} }
// 获取图片尺寸信息 // 获取图片尺寸信息
getImageInfo(imagePath) { getImageInfo(imageFile) {
return new Promise((resolve) => { return new Promise((resolve) => {
// 如果传入的是对象,尝试提取路径 let imagePath = '';
if (typeof imagePath === 'object') {
if (imagePath?.tempFilePath) {
imagePath = imagePath.tempFilePath;
} else if (imagePath?.path) {
imagePath = imagePath.path;
} else {
console.warn('无法从对象中获取图片路径,使用默认尺寸:', imagePath);
resolve({ width: 400, height: 300 });
return;
}
}
// 如果不是字符串,使用默认尺寸 // 获取图片路径 - 处理多种格式
if (typeof imagePath !== 'string' || !imagePath) { if (imageFile?.tempFilePaths?.length > 0) {
console.warn('图片路径无效,使用默认尺寸:', imagePath); // wx.chooseImage 返回的对象
imagePath = imageFile.tempFilePaths[0];
} else if (imageFile?.tempFiles?.length > 0) {
// 从 tempFiles 中提取路径
const tempFile = imageFile.tempFiles[0];
imagePath = tempFile.path || tempFile.tempFilePath;
} else if (imageFile?.tempFilePath) {
imagePath = imageFile.tempFilePath;
} else if (typeof imageFile === 'string') {
imagePath = imageFile;
} else {
console.warn('无法获取图片路径,使用默认尺寸:', imageFile);
// 默认尺寸
resolve({ width: 400, height: 300 }); resolve({ width: 400, height: 300 });
return; return;
} }