Compare commits

...

37 Commits

Author SHA1 Message Date
huxuejian
c56580d6c0 fix: 问题修复 2026-02-10 17:13:02 +08:00
huxuejian
93898e03a9 fix: 问题修复 2026-02-10 16:44:01 +08:00
huxuejian
4d7de97445 fix: 问题修复 2026-02-10 16:15:44 +08:00
huxuejian
946d29ad97 fix: 问题修复 2026-02-10 11:08:00 +08:00
huxuejian
9af63de623 Merge remote-tracking branch 'origin/dev-wdb' 2026-02-09 20:52:56 +08:00
huxuejian
624b70fc86 fix: 问题修复 2026-02-09 20:52:49 +08:00
1e5881043b no message 2026-02-09 17:41:20 +08:00
56eeede97e no message 2026-02-09 17:32:49 +08:00
huxuejian
ab64e7c9c9 fix:问题修复 2026-02-09 17:31:16 +08:00
c9483c48c1 no message 2026-02-09 17:09:00 +08:00
edec00cb37 no message 2026-02-09 16:54:59 +08:00
737cb029e4 Merge commit '638a6f3c54a030b7ec191c54efbdc604bdcd6bf7' into dev-wdb 2026-02-09 16:54:50 +08:00
huxuejian
638a6f3c54 fix: 问题修复 2026-02-09 16:34:55 +08:00
f99c412cf2 Merge commit 'a2e6ddd5dc2857db2904aabdb60adff592de58b5' into dev-wdb 2026-02-09 16:08:56 +08:00
dcb3dda5c0 no message 2026-02-09 16:08:45 +08:00
huxuejian
a2e6ddd5dc Update user-agreement.js 2026-02-09 15:28:45 +08:00
huxuejian
4c8768d490 Merge remote-tracking branch 'origin/dev-hjf' 2026-02-09 15:18:06 +08:00
huxuejian
6878ad5834 Merge remote-tracking branch 'origin/dev-wdb' 2026-02-09 15:18:01 +08:00
huxuejian
267bc814cb fix:问题修复 2026-02-09 15:17:45 +08:00
62b03f8343 no message 2026-02-09 14:07:55 +08:00
91db27cca7 Merge commit 'cecb26e762fe652537571972fecca17a576e7cf1' into dev-wdb 2026-02-09 09:19:28 +08:00
1058af4f84 no message 2026-02-09 09:18:33 +08:00
huxuejian
cecb26e762 Merge remote-tracking branch 'origin/dev-hjf' 2026-02-09 09:13:00 +08:00
huxuejian
fb497292ce Merge remote-tracking branch 'origin/dev-wdb' 2026-02-09 09:12:52 +08:00
huxuejian
d7260018ab Update edit-archive.vue 2026-02-09 09:12:43 +08:00
huxuejian
4a8982a91b Update useDebounce.js 2026-02-08 16:16:55 +08:00
huxuejian
275f658b9d fix: 问题修复 2026-02-08 16:15:24 +08:00
c53149c3d5 提交 2026-02-08 13:47:13 +08:00
huxuejian
913e6420cc fix:问题修复 2026-02-08 13:36:10 +08:00
540286e288 Merge commit '4079f52ed6cf8f7928cddfceff520b9ecb8973be' into dev-wdb 2026-02-08 10:43:09 +08:00
huxuejian
aa9bc1ca3d Merge remote-tracking branch 'origin/dev-wdb' 2026-02-08 10:42:06 +08:00
huxuejian
b2e9102e78 fix: 问题修复 2026-02-08 10:41:41 +08:00
6c0b60db7b no message 2026-02-08 10:38:58 +08:00
b73d84f2e4 no message 2026-02-06 17:36:25 +08:00
huxuejian
4079f52ed6 Merge remote-tracking branch 'origin/dev-hjf' 2026-02-06 17:20:49 +08:00
huxuejian
cecfff6124 Merge remote-tracking branch 'origin/dev-wdb' 2026-02-06 17:20:43 +08:00
huxuejian
f4e2f87b6e fix: 问题修复 2026-02-06 17:20:12 +08:00
53 changed files with 1639 additions and 626 deletions

View File

@ -1,5 +1,4 @@
MP_API_BASE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx6ee11733526b4f04
MP_TIM_SDK_APP_ID=1600123876
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876

View File

@ -2,4 +2,3 @@ MP_API_BASE_URL=http://localhost:8080
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx6ee11733526b4f04
MP_TIM_SDK_APP_ID=1600123876
MP_CORP_ID=wwe3fb2faa52cf9dfb

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
MP_API_BASE_URL=https://ykt.youcan365.com
MP_CACHE_PREFIX=production
MP_WX_APP_ID=wx6ee11733526b4f04
MP_TIM_SDK_APP_ID=1600123876
MP_CORP_ID=wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg

View File

@ -0,0 +1,140 @@
<template>
<common-cell :name="name" :required="required">
<view class="form-content__wrapper" @click="showPopup()">
<view class="w-0 flex-grow leading-normal text-base line-clamp-2">
{{ valueStr || '' }}
</view>
<!-- <view class="flex-main-content truncate" :class="value ? '' : 'form__placeholder'">
{{ valueStr || '' }}
</view> -->
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<view v-if="show" class="teleport-container">
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<view class="flex items-center justify-between px-15 py-12 border-b">
<view class="text-lg font-semibold text-dark">请选择</view>
<uni-icons type="closeempty" :size="24" color="#999" @click="close"></uni-icons>
</view>
<scroll-view scroll-y="true" class="popup-content-scroll">
<view class="px-15 py-12">
<view class="flex flex-wrap">
<view v-for="(i, idx) in range" :key="idx" class="mt-10 mr-5 px-10 py-5 text-base rounded-sm"
:class="selectMap[idx] ? 'bg-primary border-primary text-white' : 'border'" @click="toggle(i)">
{{ i }}
</view>
</view>
<view v-if="hasOther" class="mt-10 px-10 py-5 border rounded-sm">
<input v-model="otherText" class="text-base" placeholder-class="text-base" @input="changeOther($event)" />
</view>
<view class="w-full pt-15"></view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import useDebounce from '@/utils/useDebounce';
import commonCell from '../common-cell.vue';
const emits = defineEmits(['change']);
const props = defineProps({
form: {
type: Object,
default: () => ({})
},
name: {
default: ''
},
range: {
type: Array,
default: () => []
},
required: {
type: Boolean,
default: false
},
disableChange: {
type: Boolean,
default: false
},
title: {
default: ''
}
})
const popup = ref(null);
const show = ref(false);
const otherText = ref('');
const value = computed(() => props.form && Array.isArray(props.form[props.title]) ? props.form[props.title] : [])
const valueStr = computed(() => value.value.filter(i => i !== '其他').join(','));
const hasOther = computed(() => value.value.includes('其他'));
const hasNo = computed(() => value.value.includes('无'));
const selectMap = computed(() => (props.range || []).map(i => value.value.includes(i)))
const changeOther = useDebounce(() => {
let val = value.value.filter(i => (props.range || []).includes(i));
if (otherText.value) {
val.push(otherText.value);
}
emits('change', {
title: props.title,
value: val
})
})
function close() {
popup.value?.close();
show.value = false;
}
function showPopup() {
if (props.disableChange) return;
show.value = true;
const others = value.value.filter(i => !props.range.includes(i));
otherText.value = others.length ? others.join(',') : '';
setTimeout(() => {
popup.value?.open();
}, 750)
}
function toggle(i) {
let val = [...value.value];
if (i === '无') {
val = hasNo.value ? [] : ['无'];
} else if (i === '其他') {
if (hasOther.value) {
val = val.filter(v => v !== '其他');
otherText.value = '';
} else {
val = val.filter(v => v !== '无');
val.push('其他');
otherText.value = '';
}
} else if (val.includes(i)) {
val = val.filter(v => v !== i);
} else {
val = val.filter(v => v !== '无');
val.push(i);
}
emits('change', {
title: props.title,
value: val
})
console.log(value.value);
}
</script>
<style>
@import '../cell-style.css';
.popup-content-scroll {
max-height: 65vh;
}
</style>

View File

@ -9,12 +9,12 @@
</common-cell>
</template>
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { set } from '@/utils/cache';
import commonCell from '../common-cell.vue';
const emits = defineEmits(['change']);
const emits = defineEmits(['change', 'addRule']);
const props = defineProps({
form: {
type: Object,
@ -52,6 +52,18 @@ function select() {
})
}
onMounted(() => {
if (props.required && props.title) {
emits('addRule', {
title: props.title,
fn: () => {
if (value.value.length > 0) return true;
return `请选择${props.name || ''}`
}
})
}
})
</script>
<style>
@import '../cell-style.css';

View File

@ -1,4 +1,5 @@
<template>
<!-- <view class="px-10">{{ attrs.name }} {{ attrs.title }} {{ attrs.type }}</view> -->
<form-datepicker v-if="attrs.type === 'date'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" />
<form-input v-else-if="attrs.type === 'input'" v-bind="attrs" :form="form" :disableChange="disableChange"
@ -11,12 +12,12 @@
@change="change" />
<form-textarea v-else-if="attrs.type === 'textarea'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" />
<form-mult-disease v-else-if="attrs.type === 'selfMultipleDiseases'" v-bind="attrs" :form="form"
@change="change"></form-mult-disease>
<form-mult-disease v-else-if="attrs.type === 'diagnosis'" v-bind="attrs" :form="form"
@change="change"></form-mult-disease>
<form-mult-disease v-else-if="attrs.type === 'selfMultipleDiseases'" v-bind="attrs" :form="form" @change="change"
@addRule="addRule"></form-mult-disease>
<form-mult-disease v-else-if="attrs.type === 'diagnosis'" v-bind="attrs" :form="form" @change="change"
@addRule="addRule"></form-mult-disease>
<form-upload v-else-if="attrs.type === 'files'" v-bind="attrs" :form="form" @change="change" />
<form-mult-other v-else-if="attrs.type === 'multiSelectAndOther'" v-bind="attrs" :form="form" @change="change" />
<!--
<form-operation v-else-if="attrs.title === 'surgicalHistory'" v-bind="attrs" :form="form" @change="change"
@addRule="addRule" />
@ -37,6 +38,7 @@ import formRegion from './form-region.vue';
import formTextarea from './form-textarea.vue';
import formMultDisease from './form-multiple-diseases.vue';
import formUpload from './form-upload.vue';
import formMultOther from './form-mult-select-and-other.vue';
defineProps({
form: {
@ -50,11 +52,14 @@ defineProps({
})
const attrs = useAttrs();
const emits = defineEmits(['change']);
const emits = defineEmits(['change', 'addRule']);
function change(data) {
emits('change', data)
}
function addRule(data) {
emits('addRule', data)
}
</script>
<!-- <script>

View File

@ -42,7 +42,6 @@ const disabledMap = computed(() => props.disableTitles.reduce((m, i) => {
return m
}, {}))
provide('addRule', addRule);
const customRule = ref({});

View File

@ -25,7 +25,7 @@ const props = defineProps({
}
})
const list = computed(() => props.avatarList.map(i => i || '/static/default-avatar.svg'))
const list = computed(() => props.avatarList.map(i => i || '/static/default-avatar.png'))
const size = computed(() => {
const val = Number.isInteger(props.size) && props.size > 0 ? props.size : 144;

View File

@ -1,20 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

View File

@ -1,34 +1,40 @@
{
"name": "医客通患者端",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"prebuild": "node scripts/pre-build.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dayjs": "^1.11.10",
"tim-upload-plugin": "^1.4.2",
"tim-wx-sdk": "^2.27.6"
},
"uni-app": {
"scripts": {
"dev": {
"title": "测试",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
},
"localhost": {
"title": "本地",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
}
}
},
"devDependencies": {}
{
"name": "医客通患者端",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"prebuild": "node scripts/pre-build.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dayjs": "^1.11.10",
"tim-upload-plugin": "^1.4.2",
"tim-wx-sdk": "^2.27.6"
},
"uni-app": {
"scripts": {
"dev": {
"title": "测试",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
},
"localhost": {
"title": "本地",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
},
"pro": {
"title": "线上",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
}
}
},
"devDependencies": {}
}

View File

@ -50,6 +50,12 @@
"navigationBarTitleText": "健康柚"
}
},
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": "健康柚"
}
},
{
"path": "pages/archive/archive-manage",
"style": {
@ -162,7 +168,7 @@
"pagePath": "pages/home/home",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "消息"
"text": "服务"
},
{
"pagePath": "pages/message/message",

View File

@ -43,10 +43,10 @@
</view>
</view>
<view class="mt-12 border-primary qrcode p-15 mx-auto rounded" @click="previewImage()">
<image v-if="qrcode" class="h-full w-full" :src="qrcode"></image>
<image v-if="qrcode" class="h-full w-full" :show-menu-by-longpress="true" :src="qrcode"></image>
</view>
<view class="mt-12 text-base text-center text-dark">
点击识别下方二维码加我为好友
长按识别二维码加我为好友
</view>
</view>
</template>
@ -68,6 +68,7 @@ const { useLoad } = useGuard();
const { account } = storeToRefs(useAccount());
const { memberJob, memberList: list } = useJob();
const qrcode = ref('');
const corpId = ref('')
const friends = computed(() => {
const memberList = Array.isArray(team.value?.memberList) ? team.value.memberList : [];
@ -84,11 +85,11 @@ function previewImage() {
}
function toFriend(userid) {
uni.navigateTo({ url: `/pages/team/friend?corpId=${account.value?.corpId}&userid=${userid}` })
uni.navigateTo({ url: `/pages/team/friend?corpId=${corpId.value}&userid=${userid}` })
}
async function getQrcode(userid) {
const res = await api('addContactWay', { corpUserId: userid, corpId: account.value?.corpId, unionid: account.value?.openid });
const res = await api('addContactWay', { corpUserId: userid, corpId: corpId.value, unionid: account.value?.openid });
if (res && res.data) {
qrcode.value = res.data;
}
@ -104,6 +105,7 @@ async function getTeam(corpId, teamId) {
}
useLoad(options => {
corpId.value = options.corpId;
if (options.teamId && options.corpId) {
getTeam(options.corpId, options.teamId);
}

View File

@ -9,9 +9,8 @@
@change="change($event)" />
</view>
</view>
<template #footer>
<button-footer :showCancel="customerId" cancelText="删除" confirmText="保存" @cancel="unBindArchive()"
<button-footer :showCancel="customerId ? true : false" cancelText="删除" confirmText="保存" @cancel="unBindArchive()"
@confirm="confirm()" />
</template>
</full-page>
@ -27,6 +26,7 @@ import dayjs from 'dayjs';
import useGuard from '@/hooks/useGuard';
import useAccount from '@/store/account';
import api from '@/utils/api';
import { set } from "@/utils/cache";
import { toast, confirm as uniConfirm } from '@/utils/widget';
import validate from '@/utils/validate';
@ -57,9 +57,6 @@ const verifyVisible = ref(false);
const visible = ref(false);
const formData = computed(() => {
if (customerId.value) {
return { ...customer.value, ...form.value }
}
return { ...customer.value, ...form.value, mobile: account.value?.mobile }
});
@ -86,6 +83,46 @@ function confirm() {
}
}
/**
* 产品要求, 建档页面与联系人关系默认选中本人证件类型默认选中身份证
*/
function preProcessFrom() {
const relationItem = formItems.value.find(item => item.title === 'relationship');
const range = relationItem && Array.isArray(relationItem.range) ? relationItem.range : [];
if (range.includes('本人')) {
form.value.relationship = '本人';
}
const cardTypeItem = formItems.value.find(item => item.title === 'cardType');
const cardTypeRange = cardTypeItem && Array.isArray(cardTypeItem.range) ? relationItem.range : [];
if (cardTypeRange.includes('身份证')) {
form.value.cardType = '身份证';
}
}
/**
* 产品要求, 编辑的情况下 姓名身份证号性别年龄出生年月
* 建档成功或者绑定档案成功后姓名身份证号性别年龄出生年月如果有内容的都不允许修改没有内容的则允许编辑
*/
function setDisabledTitles(data) {
const list = ['mobile'];
if (data.name) {
list.push('name');
}
if (data.idCard) {
list.push('idCard');
}
if (data.sex) {
list.push('sex');
}
if (data.age) {
list.push('age');
}
if (data.birthday) {
list.push('birthday');
}
disableTitles.value = list;
}
async function addArchive() {
if (loading.value) return;
loading.value = true;
@ -101,6 +138,7 @@ async function addArchive() {
loading.value = false;
const res = await api('addCustomer', { params });
if (res && res.success) {
set('home-invite-teamId', teamId.value);
uni.$emit('reloadTeamCustomers')
uni.redirectTo({
url: `/pages/archive/archive-result?corpId=${corpId.value}&teamId=${teamId.value}`
@ -114,7 +152,11 @@ async function bindArchive(customerId) {
const res = await api('bindMiniAppArchive', { id: customerId, corpId: corpId.value, teamId: teamId.value, miniAppId: account.value.openid });
if (res && res.success) {
await toast('绑定成功');
uni.reLaunch({ url: `/pages/home/home?corpId=${corpId.value}&teamId=${teamId.value}` })
set('home-invite-teamId', teamId.value);
uni.switchTab({
url:'/pages/home/home'
})
// uni.reLaunch({ url: `/pages/home/home?corpId=${corpId.value}&teamId=${teamId.value}` })
} else {
toast(res?.message || '绑定失败');
}
@ -130,11 +172,13 @@ async function init() {
if (res.length > 0) {
visible.value = true;
}
if (!externalUserId.value) {
getExternalUserId();
}
getExternalUserId(corpId.value);
}
await getBaseForm();
if (!customerId.value) {
preProcessFrom()
}
}
async function getArchives() {
@ -153,6 +197,11 @@ async function getBaseForm() {
const res = await api('getTeamBaseInfo', { corpId: corpId.value, teamId: teamId.value });
if (res && res.success) {
formItems.value = Array.isArray(res.data) ? res.data : [];
const mobileIndex = formItems.value.findIndex(item => item.title === 'mobile');
if (mobileIndex > -1) {
formItems.value[mobileIndex].appendText = `(授权手机号)`;
}
} else {
toast(res?.message || '查询失败');
return Promise.reject()
@ -163,6 +212,7 @@ async function getCustomer() {
const res = await api('getCustomerByCustomerId', { customerId: customerId.value });
if (res && res.success && res.data) {
customer.value = res.data;
setDisabledTitles(res.data)
} else {
await toast(res?.message || '查询档案信息失败');
uni.navigateBack();

View File

@ -56,9 +56,6 @@ import dayjs from "dayjs";
import api from "@/utils/api.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const defaultCorpId = env.MP_CORP_ID;
const corpId = ref("");
const enabledIds = ref([]);
const tabs = ref([{ name: "全部", value: "" }]);
@ -261,7 +258,7 @@ function goToDetail(item) {
}
onLoad(async (options) => {
corpId.value = options?.corpId || defaultCorpId || "";
corpId.value = options?.corpId || "";
enabledIds.value = parseIdsParam(options?.ids);
activeCateId.value = "";
await loadEnabledArticles();

View File

@ -30,8 +30,6 @@ import api from "@/utils/api.js";
import { ref } from "vue";
import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js";
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const { account } = storeToRefs(useAccountStore());
const loading = ref(true);
const error = ref("");
@ -42,12 +40,17 @@ const articleData = ref({
});
let articleId = "";
const corpId = ref('')
const markArticleRead = async () => {
const unionid = account.value?.unionid;
if (!unionid || !articleId) return;
try {
await api("addArticleReadRecord", { corpId, articleId, unionid }, false);
await api(
"addArticleReadRecord",
{ corpId: corpId.value, articleId, unionid },
false
);
} catch (err) {
console.warn("markArticleRead failed:", err?.message || err);
}
@ -88,7 +91,7 @@ const loadArticle = async () => {
loading.value = true;
error.value = "";
try {
const res = await api("getArticle", { id: articleId, corpId });
const res = await api("getArticle", { id: articleId, corpId: corpId.value });
if (res.success && res.data) {
//
@ -118,6 +121,7 @@ const loadArticle = async () => {
};
onLoad((options) => {
corpId.value = options.corpId;
if (options.id) {
articleId = options.id;
markArticleRead();

View File

@ -1,18 +1,14 @@
<template>
<view class="bg-gray-100 min-h-screen">
<!-- Filter Tabs -->
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full" :show-scrollbar="false">
<view
v-for="(tab, index) in tabs"
:key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors"
:class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]"
@click="selectTab(tab.value)"
>
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
@ -28,16 +24,12 @@
</view>
<view v-else class="p-15">
<view
v-for="item in articles"
:key="item._id"
class="bg-white rounded-lg p-15 mb-15 shadow-sm"
@click="goToDetail(item)"
>
<view v-for="item in articles" :key="item._id" class="bg-white rounded-lg p-15 mb-15 shadow-sm"
@click="goToDetail(item)">
<!-- Header -->
<view class="flex items-start justify-between mb-10">
<view class="flex items-start flex-1 mr-10 relative">
<!-- Tag -->
<!-- Tag -->
<view class="text-xs text-green-600 border border-green-600 px-5 rounded mr-5 flex-shrink-0 mt-1 tag-box">
宣教文章
</view>
@ -46,16 +38,13 @@
{{ item.title }}
</view>
</view>
<!-- Status -->
<view class="flex items-center flex-shrink-0 ml-2">
<text
class="text-sm mr-2"
:class="item.status === 'UNREAD' ? 'text-red-500' : 'text-gray-400'"
>
{{ item.status === 'UNREAD' ? '未阅读' : '查看' }}
</text>
<uni-icons type="right" size="14" :color="item.status === 'UNREAD' ? '#ef4444' : '#9ca3af'"></uni-icons>
<text class="text-sm mr-2" :class="item.status === 'UNREAD' ? 'text-red-500' : 'text-gray-400'">
{{ item.status === 'UNREAD' ? '未阅读' : '查看' }}
</text>
<uni-icons type="right" size="14" :color="item.status === 'UNREAD' ? '#ef4444' : '#9ca3af'"></uni-icons>
</view>
</view>
@ -69,7 +58,7 @@
<text class="text-gray-400 mr-2 field-label">团队:</text>
<text>{{ item.team || '-' }}</text>
</view>
<!-- Footer -->
<view class="text-sm text-gray-400">
发送时间: {{ item.time || '-' }}
@ -90,15 +79,13 @@
<script setup>
import { ref } from "vue";
import { onShow, onReachBottom } from "@dcloudio/uni-app";
import { onLoad, onShow, onReachBottom } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const { account, openid } = storeToRefs(useAccountStore());
const tabs = ref([{ name: "全部", value: "" }]);
@ -110,6 +97,7 @@ const page = ref(1);
const pageSize = 20;
const loading = ref(false);
const inited = ref(false);
const corpId = ref('');
const selectTab = async (customerId) => {
if (activeTab.value === customerId) return;
@ -121,7 +109,7 @@ const loadCustomers = async () => {
const miniAppId = openid.value || uni.getStorageSync("openid");
if (!miniAppId) return;
try {
const res = await api("getMiniAppCustomers", { miniAppId, corpId });
const res = await api("getMiniAppCustomers", { miniAppId, corpId: corpId.value });
if (res && res.success) {
const list = Array.isArray(res.data) ? res.data : [];
tabs.value = [
@ -168,7 +156,7 @@ const loadArticleList = async (reset = false) => {
loading.value = true;
try {
const params = {
corpId,
corpId: corpId.value,
unionid,
miniAppId,
page: page.value,
@ -196,8 +184,11 @@ const loadArticleList = async (reset = false) => {
function goToDetail(item) {
if (!item?.articleId) return;
uni.navigateTo({ url: `/pages/article/article-detail?id=${item.articleId}` });
uni.navigateTo({ url: `/pages/article/article-detail?id=${item.articleId}&corpId=${corpId.value}` });
}
onLoad(opts => {
corpId.value = opts.corpId;
})
onShow(async () => {
if (!inited.value) {
@ -219,70 +210,224 @@ onReachBottom(() => {
<style scoped>
/* Utility helpers similar to Windi/Tailwind */
.min-h-screen { min-height: 100vh; }
.bg-gray-100 { background-color: #f7f8fa; }
.bg-white { background-color: #ffffff; }
.min-h-screen {
min-height: 100vh;
}
.p-15 { padding: 30rpx; }
.px-15 { padding-left: 30rpx; padding-right: 30rpx; }
.py-10 { padding-top: 20rpx; padding-bottom: 20rpx; }
.py-5 { padding-top: 10rpx; padding-bottom: 10rpx; }
.px-5 { padding-left: 10rpx; padding-right: 10rpx; }
.bg-gray-100 {
background-color: #f7f8fa;
}
.mr-10 { margin-right: 20rpx; }
.mr-5 { margin-right: 10rpx; }
.ml-2 { margin-left: 10rpx; }
.mr-2 { margin-right: 10rpx; }
.mb-15 { margin-bottom: 30rpx; }
.mb-10 { margin-bottom: 20rpx; }
.mb-5 { margin-bottom: 10rpx; }
.mt-1 { margin-top: 6rpx; }
.pb-10 { padding-bottom: 20rpx; }
.bg-white {
background-color: #ffffff;
}
.flex { display: flex; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.flex-1 { flex: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.relative { position: relative; }
.p-15 {
padding: 30rpx;
}
.border { border-width: 1px; border-style: solid; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
.rounded-full { border-radius: 9999px; }
.rounded { border-radius: 8rpx; }
.rounded-lg { border-radius: 12rpx; }
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.shadow-sm { box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.text-xs { font-size: 22rpx; }
.text-sm { font-size: 28rpx; }
.text-base { font-size: 32rpx; }
.font-bold { font-weight: 600; }
.leading-normal { line-height: 1.4; }
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.ml-2 {
margin-left: 10rpx;
}
.mr-2 {
margin-right: 10rpx;
}
.mb-15 {
margin-bottom: 30rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mb-5 {
margin-bottom: 10rpx;
}
.mt-1 {
margin-top: 6rpx;
}
.pb-10 {
padding-bottom: 20rpx;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.relative {
position: relative;
}
.border {
border-width: 1px;
border-style: solid;
}
.border-b {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.rounded-full {
border-radius: 9999px;
}
.rounded {
border-radius: 8rpx;
}
.rounded-lg {
border-radius: 12rpx;
}
.shadow-sm {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 28rpx;
}
.text-base {
font-size: 32rpx;
}
.font-bold {
font-weight: 600;
}
.leading-normal {
line-height: 1.4;
}
/* Colors - Adjusting to match image roughly */
.text-orange-500 { color: #f29e38; }
.bg-orange-100 { background-color: #fff8eb; }
.border-orange-500 { border-color: #f29e38; }
.text-orange-500 {
color: #f29e38;
}
.text-gray-600 { color: #333333; }
.text-gray-500 { color: #999999; }
.text-gray-400 { color: #999999; }
.text-gray-800 { color: #1a1a1a; }
.border-gray-200 { border-color: #e5e5e5; }
.border-gray-100 { border-color: #f5f5f5; }
.bg-orange-100 {
background-color: #fff8eb;
}
.text-green-600 { color: #4b8d5f; }
.border-green-600 { border-color: #4b8d5f; }
.text-red-500 { color: #e04a4a; }
.border-orange-500 {
border-color: #f29e38;
}
.sticky { position: sticky; }
.top-0 { top: 0; }
.z-10 { z-index: 10; }
.w-full { width: 100%; }
.whitespace-nowrap { white-space: nowrap; }
.inline-block { display: inline-block; }
.text-gray-600 {
color: #333333;
}
.text-gray-500 {
color: #999999;
}
.text-gray-400 {
color: #999999;
}
.text-gray-800 {
color: #1a1a1a;
}
.border-gray-200 {
border-color: #e5e5e5;
}
.border-gray-100 {
border-color: #f5f5f5;
}
.text-green-600 {
color: #4b8d5f;
}
.border-green-600 {
border-color: #4b8d5f;
}
.text-red-500 {
color: #e04a4a;
}
.sticky {
position: sticky;
}
.top-0 {
top: 0;
}
.z-10 {
z-index: 10;
}
.w-full {
width: 100%;
}
.whitespace-nowrap {
white-space: nowrap;
}
.inline-block {
display: inline-block;
}
.tag-box {
border-radius: 4rpx;

View File

@ -314,8 +314,12 @@ const closePreview = () => {
const sendArticle = async (article) => {
try {
const { globalTimChatManager } = await import("@/utils/tim-chat.js");
if (!globalTimChatManager || !globalTimChatManager.tim || !globalTimChatManager.isLoggedIn) {
if (
!globalTimChatManager ||
!globalTimChatManager.tim ||
!globalTimChatManager.isLoggedIn
) {
uni.showToast({
title: "IM系统未就绪请重试",
icon: "none",
@ -326,9 +330,9 @@ const sendArticle = async (article) => {
// ID
const conversationID = `GROUP${pageParams.value.groupId}`;
globalTimChatManager.currentConversationID = conversationID;
console.log("发送文章会话ID:", conversationID);
//
const customMessageData = {
type: "article",
@ -337,16 +341,18 @@ const sendArticle = async (article) => {
imgUrl: article.cover || "",
desc: "点击查看详情",
};
//
const sendResult = await globalTimChatManager.sendCustomMessage(customMessageData);
const sendResult = await globalTimChatManager.sendCustomMessage(
customMessageData
);
if (sendResult && sendResult.success) {
console.log("✓ 文章消息已发送");
uni.showToast({
title: "发送成功",
icon: "success",
});
//
setTimeout(() => {
uni.navigateBack();

View File

@ -137,7 +137,11 @@ function addArchive() {
function getTempRows(type, data) {
if (config[type] && tempShowField.value && tempShowField.value[type]) {
const list = [];
config[type].forEach(i => {
let titles = config[type];
if (data.corp === '其他') {
titles = ['corpName', 'diagnosisName', 'files']
}
titles.forEach(i => {
if (tempShowField.value[type][i]) {
list.push({ title: i, label: tempShowField.value[type][i], value: data[i], key: `${i}_${data._id}` })
}

View File

@ -3,12 +3,7 @@
<view class="consult-title">咨询互动</view>
<view class="consult-grid">
<view
class="consult-item"
v-for="item in consultItems"
:key="item.id"
@click="handleItemClick(item)"
>
<view class="consult-item" v-for="item in consultItems" :key="item.id" @click="handleItemClick(item)">
<view class="item-icon">
<image :src="item.icon" class="icon-img" mode="aspectFill" />
</view>
@ -17,14 +12,8 @@
</view>
<!-- 选择咨询人弹窗 -->
<select-consultant-popup
ref="consultantPopup"
:customers="customers"
:corpId="corpId"
:teamId="teamId"
@confirm="handleConsultantConfirm"
@addNew="handleAddNewArchive"
/>
<select-consultant-popup ref="consultantPopup" :customers="customers" :corpId="corpId" :teamId="teamId"
@confirm="handleConsultantConfirm" @addNew="handleAddNewArchive" />
</view>
</template>
@ -41,6 +30,10 @@ const props = defineProps({
type: String,
default: "",
},
team: {
type: Object,
default: () => ({})
},
teamId: {
type: String,
default: "",
@ -84,6 +77,10 @@ const consultItems = ref([
function handleItemClick(item) {
//
if (item.needSelectConsultant) {
if (!props.team || !props.team.creator) {
return toast('该团队暂未开放咨询服务')
}
if (!props.customers || props.customers.length === 0) {
toast("请先添加档案");
//
@ -105,7 +102,7 @@ function handleItemClick(item) {
}
uni.navigateTo({
url: item.path,
url: `${item.path}?teamId=${props.teamId}&corpId=${props.corpId}`,
fail: () => {
toast("页面跳转失败");
},
@ -114,14 +111,26 @@ function handleItemClick(item) {
//
async function handleConsultantConfirm(customer) {
console.log("选择的咨询人:", customer);
const teamIds = customer && Array.isArray(customer.teamId) ? customer.teamId : [];
if (!teamIds.includes(props.teamId)) {
const res = await api("authCustomerToTeam", {
corpId: props.team.corpId,
teamId: props.team.teamId,
id: customer._id,
});
if (res && res.success) {
uni.$emit("reloadTeamCustomers");
} else {
toast(res?.message || "授权团队失败");
}
}
//
uni.showLoading({ title: "创建咨询中..." });
try {
const res = await api("createConsultGroup", {
teamId: props.teamId,
corpId: props.corpId,
customerId: customer._id,
customerId: customer._id,
customerImUserId: account.value.openid,
});
uni.hideLoading();

View File

@ -1,12 +1,10 @@
<template>
<view class="archive-container">
<view class="mb-10 flex items-center justify-between">
<view class="module-title flex-shrink-0 truncate">
成员档案
</view>
<view class="module-title flex-shrink-0 truncate"> 成员档案 </view>
<view class="flex items-center leading-normal rounded-sm" style="padding-right: 0" @click="toManagePage()">
<image class="manage-icon mr-5" src="/static/home/archive-manage.png" mode="aspectFit"></image>
<view style="font-size: 28rpx; color: #065BD6;">档案管理</view>
<view style="font-size: 28rpx; color: #065bd6">档案管理</view>
</view>
</view>
<view v-if="customers.length === 0" class="add-archive-card" @click="toManagePage()">
@ -18,19 +16,11 @@
</view>
<scroll-view scroll-x="true">
<view class="flex flex-nowrap pb-5">
<view
v-for="i in customers"
:key="i._id"
class="customer-card flex-shrink-0 mr-15 relative"
:class="current && i._id === current._id ? 'current-customer' : ''"
@click="toggle(i)"
>
<view v-for="i in customers" :key="i._id" class="customer-card flex-shrink-0 mr-15 relative"
:class="current && i._id === current._id ? 'current-customer' : ''" @click="toggle(i)">
<!-- 关系标签 -->
<view
v-if="i.relationship"
class="relationship-tag"
:class="i.relationship === '本人' ? 'tag-blue' : 'tag-green'"
>
<view v-if="i.relationship" class="relationship-tag"
:class="i.relationship === '本人' ? 'tag-blue' : 'tag-green'">
{{ i.relationship }}
</view>
<view class="flex flex-col items-center">
@ -40,27 +30,18 @@
</view>
</view>
<!-- 选中状态底部条和三角 -->
<view
v-if="current && i._id === current._id"
class="active-indicator"
>
<view v-if="current && i._id === current._id" class="active-indicator">
<view class="active-bar"></view>
<view class="active-triangle"></view>
</view>
</view>
</view>
</scroll-view>
<view
v-if="canAuth"
class="px-10 py-5 mt-5 flex items-center bg-danger rounded-sm"
>
<view v-if="canAuth" class="px-10 py-5 mt-5 flex items-center bg-danger rounded-sm">
<view class="mr-5 w-0 flex-grow text-base text-white">
点击右侧授权按钮, 我们将更精准的为您服务
</view>
<view
class="px-12 py-5 text-base rounded-sm text-dark bg-white"
@click="auth()"
>
<view class="px-12 py-5 text-base rounded-sm text-dark bg-white" @click="auth()">
授权
</view>
</view>
@ -70,32 +51,18 @@
<view class="info-content">
<view class="flex items-center justify-between mb-8">
<view class="info-title">个人基本信息</view>
<image
class="arrow-icon-small"
src="/static/home/arrow-right-blue.png"
mode="aspectFit"
></image>
<image class="arrow-icon-small" src="/static/home/arrow-right-blue.png" mode="aspectFit"></image>
</view>
<view v-if="baseInfoError" class="text-sm text-danger"
>请完善您的个人信息</view
>
<view v-if="baseInfoError" class="text-sm text-danger">请完善您的个人信息</view>
<view v-else class="info-subtitle">完善个人信息</view>
</view>
</view>
<view
v-if="hasHealthTemp"
class="ml-10 info-card-new flex-grow"
@click="toHealthList()"
>
<view v-if="hasHealthTemp" class="ml-10 info-card-new flex-grow" @click="toHealthList()">
<view class="info-bg info-bg-health"></view>
<view class="info-content">
<view class="flex items-center justify-between mb-8">
<view class="info-title">健康信息</view>
<image
class="arrow-icon-small"
src="/static/home/arrow-right-blue.png"
mode="aspectFit"
></image>
<image class="arrow-icon-small" src="/static/home/arrow-right-blue.png" mode="aspectFit"></image>
</view>
<view class="info-subtitle">上传健康档案</view>
</view>
@ -154,17 +121,12 @@ const hasHealthTemp = computed(
);
const baseInfo = computed(() =>
qrcode.value &&
qrcode.value.teamFileds &&
Array.isArray(qrcode.value.teamFileds.baseInfo)
qrcode.value.teamFileds &&
Array.isArray(qrcode.value.teamFileds.baseInfo)
? qrcode.value.teamFileds.baseInfo
: []
);
const baseInfoError = computed(() => {
const requiredTitles = baseInfo.value
.filter((i) => i.required)
.map((i) => i.title);
return current.value && requiredTitles.some((i) => !current.value[i]);
});
const baseInfoError = computed(() => current.value && baseInfo.value.some((i) => i.title && !current.value[i.title]));
function fillBaseInfo() {
if (canAuth.value) {
@ -180,13 +142,11 @@ function toHealthList() {
if (canAuth.value) {
toast("请先授权本服务团队");
} else {
const name = `${current.value.name} ${
current.value.relationship ? `(${current.value.relationship})` : ""
}`;
const name = `${current.value.name} ${current.value.relationship ? `(${current.value.relationship})` : ""
}`;
uni.navigateTo({
url: `/pages/health/list?teamId=${props.team.teamId}&corpId=${
props.corpId
}&id=${current.value._id}&name=${encodeURIComponent(name)}`,
url: `/pages/health/list?teamId=${props.team.teamId}&corpId=${props.corpId
}&id=${current.value._id}&name=${encodeURIComponent(name)}`,
});
}
}
@ -450,7 +410,7 @@ watch(
.add-archive-card {
height: 140rpx;
border-radius: 16rpx;
background: linear-gradient(95deg, #ECFBFF -7.38%, #A5CBFF 72.72%);
background: linear-gradient(95deg, #ecfbff -7.38%, #a5cbff 72.72%);
display: flex;
align-items: center;
justify-content: space-between;
@ -464,7 +424,7 @@ watch(
width: 188rpx;
height: 56rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, #33A0F9 0%, #065BD6 100%);
background: linear-gradient(90deg, #33a0f9 0%, #065bd6 100%);
box-shadow: 0 8rpx 8rpx 0 rgba(6, 91, 214, 0.3);
display: flex;
align-items: center;
@ -480,7 +440,7 @@ watch(
.add-archive-btn-text {
font-size: 28rpx;
color: #FFFFFF;
color: #ffffff;
font-weight: 500;
}

View File

@ -8,7 +8,7 @@
<customer-archive :corpId="corpId" :team="team" @update:customers="handleCustomersUpdate" />
</view>
<view class="home-section">
<consult :corpId="corpId" :teamId="team.teamId" :customers="customers" />
<consult :corpId="corpId" :teamId="team.teamId" :team="team" :customers="customers" />
</view>
<!-- <view class="home-section">
<team-mate :team="team" />
@ -20,12 +20,14 @@
<yc-home v-else />
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard";
import { onLoad, onShow } from "@dcloudio/uni-app";
// import useGuard from "@/hooks/useGuard";
import useAccount from "@/store/account";
import api from "@/utils/api";
import { toast } from "@/utils/widget";
import { get, remove } from "@/utils/cache";
import FullPage from "@/components/full-page.vue";
import articleList from "./article-list.vue";
@ -36,12 +38,13 @@ import teamMate from "./team-mate.vue";
import ycHome from "./yc-home.vue";
import pageLoading from "./loading.vue";
const { useLoad, useShow } = useGuard();
// const { useLoad, useShow } = useGuard();
const { account } = storeToRefs(useAccount());
const { login } = useAccount();
const team = ref(null);
const teams = ref([]);
const loading = ref(true);
const loading = ref(false);
const customers = ref([]);
const corpId = computed(() => team.value?.corpId);
@ -70,27 +73,35 @@ async function getTeams() {
loading.value = true;
const res = await api('getWxappRelateTeams', { openid: account.value.openid });
teams.value = res && Array.isArray(res.data) ? res.data : [];
const validTeam =
teams.value.find(
(item) => team.value && item.teamId === team.value.teamId
) || teams.value[0];
if (validTeam) {
changeTeam(validTeam);
return;
const matchTeamId = get('home-invite-teamId') || (team.value ? team.value.teamId : '');
const validTeam = teams.value.find(i => i.teamId && i.teamId === matchTeamId);
const firstTeam = teams.value[0]
if (validTeam || firstTeam) {
remove('home-invite-teamId');
changeTeam(validTeam || firstTeam);
} else {
team.value = null;
}
loading.value = false;
}
useLoad((opts) => {
if (opts.teamId) {
team.value = { teamId: opts.teamId, corpId: opts.corpId };
}
});
// useLoad((opts) => {
// if (opts.teamId) {
// team.value = { teamId: opts.teamId, corpId: opts.corpId };
// }
// });
useShow(() => {
getTeams();
onLoad(() => {
if (!account.value) login();
})
onShow(async () => {
if (!account.value) await login();
if (account.value && account.value.openid) {
getTeams();
} else {
teams.value = [];
}
});
</script>
<style scoped>

View File

@ -13,7 +13,7 @@
<view v-for="i in teamates" :key="i.userid"
class="member-card flex flex-shrink-0 min-w-120 mr-15 p-15"
@click="toHomePage(i)">
<image class="flex-shrink-0 avatar mr-10" :src="i.avatar || '/static/default-avatar.svg'" />
<image class="flex-shrink-0 avatar mr-10" :src="i.avatar || '/static/default-avatar.png'" />
<view class="flex-grow flex flex-col justify-between">
<view>
<view class="member-name leading-normal h-24 text-base font-semibold text-dark whitespace-nowrap">

View File

@ -7,8 +7,8 @@
<image class="logo" src="/static/logo-plain.png"></image>
</view>
<view class="w-0 flex-grow">
<view class="text-lg font-semibold text-white">健康 </view>
<view class="leading-normal text-base text-white truncate">生命全周期健康管理伙伴</view>
<view class="text-lg font-semibold text-white">健康</view>
<view class="leading-normal text-base text-white truncate">全周期健康管理伙伴</view>
</view>
<view v-if="menuButtonInfo && menuButtonInfo.width > 0" class="flex-shrink-0"
:style="{ width: menuButtonInfo.width + 'px', height: menuButtonInfo.height + 'px' }">
@ -17,8 +17,8 @@
</view>
<view class="flex-grow flex flex-col items-center justify-center bg-white">
<empty-data :showText="false" />
<view class="mb-10 text-lg text-dark font-semibold">暂无团队</view>
<view class="text-lg text-dark font-semibold">需要扫团队二维码绑定服务团队哦</view>
<!-- <view class="mb-10 text-lg text-dark font-semibold">暂无团队</view> -->
<view class="text-lg text-dark font-semibold">微信扫一扫医生团队二维码</view>
</view>
</view>
</template>

30
pages/login/agreement.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<scroll-view class="h-full bg-white" scroll-y="true">
<view class="p-15 text-base text-dark leading-normal" style="white-space: pre-wrap;">
{{ content }}
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import privacy from './privacy-policy.js';
import userAgreement from './user-agreement.js';
const content = ref('');
onLoad((options) => {
if (options.type === 'privacyPolicy') {
content.value = privacy;
uni.setNavigationBarTitle({
title: '隐私政策'
})
} else if (options.type === 'userAgreement') {
content.value = userAgreement;
uni.setNavigationBarTitle({
title: '用户协议'
})
}
})
</script>

View File

@ -13,8 +13,8 @@
<view v-else class="doctor-card doctor-card-empty">
<view class="doctor-info">
<image class="logo" src="/static/logo-plain.png" mode="aspectFill" />
<view class="doctor-name">健康</view>
<view class="login-tip">生命全周期健康管理伙伴</view>
<view class="doctor-name">健康</view>
<view class="login-tip">全周期健康管理伙伴</view>
</view>
</view>
<view v-if="team" class="doctor-avatar">
@ -38,9 +38,9 @@
<view class="agreement">
<checkbox :checked="checked" @click="checked = !checked" />
<text>我已阅读并同意</text>
<text class="link" @click="openUserAgreement">用户协议</text>
<text class="link" @click="toAggreement('userAgreement')">用户协议</text>
<text></text>
<text class="link" @click="openPrivacyPolicy">隐私政策</text>
<text class="link" @click="toAggreement('privacyPolicy')">隐私政策</text>
</view>
</view>
</template>
@ -52,7 +52,7 @@ import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account";
import api from '@/utils/api';
import { get } from "@/utils/cache";
import { get, set } from "@/utils/cache";
import { toast } from "@/utils/widget";
import groupAvatar from "@/components/group-avatar.vue";
@ -89,19 +89,28 @@ function remind() {
toast("请先阅读并同意用户协议和隐私政策");
}
function toAggreement(type) {
uni.navigateTo({
url: `/pages/login/agreement?type=${type}`
})
}
function toHome() {
uni.switchTab({
url: "/pages/home/home",
});
}
async function bindTeam() {
const res = await api('bindWxappWithTeam', { appid, corpId: team.value.corpId, teamId: team.value.teamId, openid: account.value.openid });
if (!res || !res.success) {
return toast("关联团队失败");
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: account.value.corpId, teamId: team.value.teamId });
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
set('home-invite-teamId', team.value.teamId);
toHome();
} else {
attempToPage(redirectUrl.value)
@ -216,12 +225,12 @@ onLoad((opts) => {
}
.doctor-card {
height: 398rpx;
/* min-height: 300rpx; */
background: #fff;
border-radius: 24rpx;
background: linear-gradient(180deg, #dbe8ff 0%, #fff 50.25%);
box-shadow: 0 8rpx 32rpx rgba(59, 124, 255, 0.08);
padding: 100rpx 24rpx 0rpx 24rpx;
padding: 100rpx 24rpx 60rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;

View File

@ -0,0 +1,166 @@
export default `
隐私政策
欢迎您访问健康柚平台
下称患者用户在健康柚平台使用我们的服务或产品时我们可能会收集您的相关个人信息健康柚深知个人信息对您的重要性并会尽全力保护您的个人信息安全可靠我们致力于维持您对我们的信任恪守以下原则保护您的个人信息权责一致原则目的明确原则选择同意原则最少够用原则确保安全原则主体参与原则公开透明原则等同时健康柚承诺我们将按业界成熟的安全标准采取相应的安全保护措施来保护您的个人信息
为此我们制定本隐私政策适用于我们为患者提供的产品和服务包括但不限于健康柚微信小程序柚助手微信小程序
在使用健康柚平台提供的产品或服务前请您务必认真仔细阅读并确认充分理解本隐私政策在确认充分理解并同意后再开始使用一旦您主动选择确认本隐私政策并继续使用的即视为同意本隐私政策的全部内容如您不同意相关协议或其中的任何条款的您应停止访问健康柚平台或使用健康柚产品和服务
如您是未成年人请您和您的监护人仔细阅读本政策并在征得您的监护人授权同意的前提下使用我们的服务或向我们提供个人信息
本隐私政策将帮助您了解以下内容
1我们如何收集和使用您的个人信息
2我们如何使用Cookie和同类技术
3我们如何共享转移公开披露您的个人信息
4我们如何保存和保护您的个人信息
5您的权利
6我们如何处理未成年人的个人信息
7您的个人信息如何在全球范围转移
8本隐私政策更新及通知
9如何联系我们
10争议解决
我们如何收集和使用您的个人信息
个人信息是指以电子或其他方式记录的与已识别或者可识别的自然人有关的各种信息不包括匿名化处理后的信息
我们仅会出于本政策所述的以下目的收集和使用您的个人信息当我们要将信息用于本政策未载明的其他用途时会事先征求您的同意
() 注册成为用户
健康柚平台提供的服务或产品是基于注册用户使用的如您希望使用健康柚平台提供的服务或产品则需要通过以下步骤完成账号注册
创建健康柚账户我们将提供手机号码授权登陆方式您需要提供您的手机号码如不提供上述注册信息您无法使用需注册成为健康柚平台用户方可使用的服务
() 成员档案管理服务
用户在使用成员档案管理服务时需先添加成员信息包括您的姓名性别年龄与成员关系本人/子女/父母/其他等目的是协助医生对患者进行管理
() 我的服务团队服务
用户在成员档案管理中添加成员信息后使用团队服务时我们可能会收集在健康柚其他平台已与患者建立关系的服务团队信息目的是与服务团队建立联系以确保成员聊天咨询的连续性和准确性我们还可能收集您的登记信息姓名身份证号码性别年龄检查检验报告用药记录过敏史等个人健康生理信息以及与个人身体健康状况相关的身高体重信息用于了解您的健康状况和咨询需求如不收集这类信息我们将无法为您提供健康咨询相关的服务但不影响您使用其他服务
您与团队建立服务关系后您理解并同意将添加的个人信息病历信息就诊记录将向该团队展示
() 回访服务
在您的服务团队人员对您进行回访的过程中我们可能收集您与团队人员的沟通聊天记录为您制定的回访计划向您发送的文章以及您填写的问卷信息以支持您在健康柚平台上获得持续可追溯的回访服务
() 客户服务
当您向我们提出问题投诉或建议时我们需要收集您的通信/通话记录您提供的联系方式信息您为了证明相关事实提供的信息以及您参与问卷调查时向我们发送的问卷答复信息我们收集上述信息的法律依据是基于向您提供健康柚平台服务所必需为您解决您在使用平台及享受服务过程中所遇到的问题以及向您提供相关问题的处理方案和结果如不收集这类信息您的投诉建议和反馈将无法得到及时有效处理但不影响您使用其它服务
() 保障功能运行和风控服务
为保障您正常使用我们及我们关联公司合作伙伴提供的服务维护我们系统基础功能的正常运行拦截钓鱼网站欺诈防止网络漏洞计算机病毒网络攻击网络侵入改进及优化我们的服务体验以及保障您的账号安全我们需要整合我们已根据本隐私政策合法收集的您的个人基本信息姓名身份证号码手机号码性别年龄个人生理健康信息既往病史用药记录体重并收集使用或整合您的网络身份标识信息BSSIDDNS地址IP地址SSID代理信息网络类型网络名称掩码信息个人常用设备信息IMEIIMSI设备IDMAC地址IDFAIDFVAndroidIdMCCMNCUUID标准国家码操作系统信息Cookie启用状态重力传感陀螺仪传感加速度传感已安装应用列表位置信息经纬度人脸识别信息以及我们关联公司合作伙伴取得您授权或依据法律共享的信息我们收集上述信息的法律依据是基于法定义务及向您提供健康柚平台服务所必需以综合判断您账户及交易风险进行身份验证检测及防范账户安全事件并依法采取必要的记录审计分析处置措施如不收集这类信息您将无法使用健康柚平台及健康柚平台提供的相应服务
() 我们如何使用您的信息
1我们会对我们提供的服务使用情况进行统计并可能会与公众或第三方共享这些统计信息以用于产品开发服务优化安全保障数据分析等目的但这些统计信息不包含您的任何身份识别信息
2根据相关法律法规规定以下情形中收集使用您的个人信息无需征得您的授权同意
1为订立履行您作为一方当事人的合同所必需
2为履行法定职责或者法定义务所必需
3为应对突发公共卫生事件或者紧急情况下为保护自然人的生命健康和财产安全所必需
4为公共利益实施新闻报道舆论监督等行为在合理的范围内处理您的个人信息
5依照法律规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息
6法律行政法规规定的其他情形
我们如何使用Cookies和同类技术
() Cookies
为确保网站正常运转为您获得更轻松的访问体验我们会在您的计算机或移动设备上存储名为Cookies的小数据文件Cookies通常包含标识符站点名称以及一些号码和字符借助于Cookies网站能够记住您的选择存储您的偏好等数据
我们不会将Cookies用于本政策所述目的之外的任何用途您可根据自己的偏好管理或删除Cookies您可以清除计算机或手机上保存的所有Cookies大部分网络浏览器都设有阻止Cookies的功能如果您这么做则需要在每一次访问我们的网站时亲自更改用户设置
第三方合作伙伴通过Cookies收集和使用您的信息不受本政策约束而是受到其自身的信息保护声明约束我们不对第三方的Cookies或同类技术承担责任
我们如何共享转移公开披露您的个人信息
() 对外提供
如您主动自愿要求我们向第三方提供您的个人信息的我们将基于您同意的目的在相应页面中以适当方式告知您个人信息接收方的名称和联系方式例如您主动要求使用健康柚平台账户登录第三方产品或服务的我们或第三方将在关联登录页面告知您为此目的健康柚平台需向第三方提供的个人信息以及第三方的名称和联系方式
我们基于以下情况可能会对外共享您的个人信息
1在法定情形下的共享我们可能会根据法律法规规定或按政府主管部门的强制性要求对外共享您的个人信息我们为履行法定义务而向第三方提供您的个人信息的我们将在相应页面中以适当方式告知您个人信息接收方的名称和联系信息
2与关联公司间共享我们只会共享必要的个人信息如为便于您通过统一账号使用我们关联公司产品或服务我们会向关联公司共享您必要的账户信息如果我们共享您的个人敏感信息或关联公司改变个人信息的使用及处理目的将在此就分享目的范围形式等必要内容征求您的授权统一
3基于向您提供健康柚平台服务所必需部分服务可能是我们的关联公司和合作机构授权合作伙伴或我们与第三方共同向您提供因此为向您提供健康柚平台服务我们必需将您的个人信息提供给我们的关联公司及业务合作伙伴例如在某些情况下我们必须与物流服务提供商共享您的收货信息才能安排配送我们仅会出于合法正当必要特定明确的目的共享您的个人信息并且只会共享提供服务所必要的个人信息我们的合作伙伴无权将共享的个人信息用于任何其他用途
目前我们的授权合作伙伴包含以下类型
1)技术服务供应商我们可能会将您的个人信息共享给支持我们功能的第三方这些支持包括为我们提供基础设施技术服务安全保障服务代表我们发出短信的通讯服务供应商物流配送服务数据处理等我们共享这些信息的目的是可以实现我们产品或服务的功能比如我们必须与物流服务提供商共享您的收货信息才能安排送货
2)分析服务类的授权合作伙伴在征得您的许可后我们可能将不能识别您的个人身份信息的统计或匿名信息共享给提供分析服务的合作伙伴对于分析数据的伙伴我们仅会向这些合作伙伴提供不能识别个人身份的统计或匿名信息
3)委托我们进行推广的合作伙伴有时我们会代表其他企业向使用我们产品或服务的用户群提供促销推广的服务我们可能会使用您的个人信息以及您的非个人信息集合形成的间接用户画像与委托我们进行推广的合作伙伴委托方共享但我们仅会向这些委托方提供推广的覆盖面和有效性的信息而不会提供您的个人身份信息或者我们将这些信息进行汇总以便它不会识别您个人比如我们可以告知该委托方有多少人看了他们的推广信息或者向他们提供不能识别个人身份的统计信息帮助他们了解其受众或顾客对我们与之共享个人信息的公司组织和个人我们会与其签署严格的保密协定要求他们按照我们的说明本隐私政策以及其他任何相关的保密和安全措施来处理个人信息
4医疗技术与药物研发合作伙伴在对您的个人信息进行去标识化处理统计后我们可能会向开展医疗技术与药物研发的合作伙伴提供相关去标识化之后的信息我们将与我们的合作伙伴签署严格的保密协议要求他们采取严格的保密和安全措施仅为医疗技术与药物研发目的处理该等去标识化后的信息并禁止其采取任何技术手段尝试利用该等信息重新识别您的身份
4设备权限调用及SDK
我们将审慎评估关联方第三方数据使用共享信息的目的对这些合作方的安全保障能力进行综合评估并要求其遵循合作法律协议我们会对合作方获取信息的软件工具开发包SDK应用程序接口API进行严格的安全监测以保护数据安全
() 转移
1您如果需要将您的个人信息转移至您指定的第三方的您可以通过本政策载明的方式联系我们在符合法律法规规定的条件下我们将逐一处理和响应
2在涉及合并分立清算资产或业务的收购或出售等交易原因需要转移您的个人信息我们将向您告知接收方的名称或者姓名和联系方式并促使接收方继续履行个人信息保护义务接收方变更原先的处理目的处理方式的应当依法规定重新取得您的同意或具备其他合法事由
() 公开披露
我们原则上不会公开披露您的个人信息以下情况除外
1获得您的单独同意后
2基于法律的披露在法律法律程序诉讼或政府主管部门强制性要求的情况下我们可能会公开披露您的个人信息
3在符合法律法规的前提下当我们收到上述披露信息的请求时我们会要求必须出具与之相应的法律文件如传票或调查函我们坚信对于要求我们提供的信息应该在法律允许的范围内尽可能保持透明
() 共享转移公开披露个人信息时事先征得授权同意的例外
在以下情形中共享转移公开披露您的个人信息无需事先征得您的授权同意
1为履行法定职责或者法定义务所必需
2为应对突发公共卫生事件或者紧急情况下为保护自然人的生命健康和财产安全所必需
3为公共利益实施新闻报道舆论监督等行为在合理的范围内处理个人信息
4依照本法规定在合理的范围内处理个人自行公开或者其他已经合法公开的个人信息
5法律行政法规规定的其他情形
6已经匿名化处理的您的个人信息指经过处理无法识别特定自然人且不能复原
我们如何保存和保护您的个人信息
() 个人信息的保存
1保存期限如您删除或通过系统设置拒绝我们对您的个人信息进行收集或者在您申请注销账号经核实身份注销后我们将停止使用并删除或匿名化处理您的个人信息我们的个人信息保存期限为实现目的所需及法律法规要求的最短时间但法律法规另有规定或者您另行授权同意的除外
2保存地域上述信息将存储于中华人民共和国境内如需跨境传输我们将会在符合国家对于信息出境的相关法律规定情况下另行单独征得您的授权同意
() 个人信息的保护
1安全措施
1)我们已使用符合业界标准的安全防护措施保护您提供的个人信息防止数据遭到未经授权访问公开披露使用修改损坏或丢失我们会采取一切合理可行的措施保护您的个人信息
2)我们会使用加密技术确保数据的安全我们会使用受信赖的保护机制防止数据遭到恶意攻击
3)我们已部署访问控制机制确保只有授权人员才可访问个人信息我们会与接触您个人信息的员工合作伙伴签署保密协议明确岗位职责及行为准则确保只有授权人员才可访问个人信息并对此进行审查若有违反保密协议的行为会被追究相关责任
4)我们会举办安全和隐私保护培训课程加强员工对于保护个人信息重要性的认识
2安全提醒
1)互联网并非绝对安全的环境我们强烈建议您不要通过电子邮件即使通讯及与其他用户交流等未加密的方式发送个人信息请登陆时使用手机验证码协助我们保证您的账号安全
2)请使用复杂密码协助我们保证您的账号安全我们将尽力保障您发送给我们的任何信息的安全性如果我们的物理技术或管理防护设施遭到破坏导致信息被非授权访问公开披露篡改或毁坏导致您的合法权益受损我们将承担相应的法律责任
3)您在使用健康柚平台及服务时请谨慎发表上传可能会涉及您或他人隐私的信息也勿将该等信息通过健康柚平台的服务传播给他人若因您该等行为引起您或他人的隐私泄露由您自行承担责任
4)请勿在使用健康柚平台服务时公开透露自己的各类财产账户银行卡信用卡第三方支付账户及对应密码等重要资料否则由此带来的损失由您自行承担责任
5)健康柚平台一旦发现假冒仿冒盗用他人名义进行平台认证的健康柚有权立即删除用户信息并有权在用户提供充分证据前禁止其使用平台服务
3安全事件通知
1)我们会制定相应的网络安全事件应急预案及时处置系统漏洞计算机病毒网络攻击网络侵入等安全风险在发生危害网络安全的事件时我们会立即启动应急预案采取相应的补救措施
2)在不幸发生个人信息安全事件后我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响我们已采取或将要采取的处置措施您可自主防范和降低风险的建议对您的补救措施等我们将及时将事件相关情况以邮件信函电话推送通知等方式告知您难以逐一告知个人信息主体时我们会采取合理有效的方式发布公告
同时我们还将按照监督部门要求主动上报个人信息安全事件的处置情况
请您理解根据法律法规的规定如果我们采取的措施能够有效避免信息泄露篡改丢失造成危害的除非监管部门要求向您通知我们可以选择不向您通知该个人信息安全事件
3)如您发现自己的个人信息泄密尤其是您的账户及密码发生泄露请您立即通过健康柚平台或本隐私政策提供的联系方式联络我们以便我们采取相应措施
您的权利
按照中国相关的法律法规标准以及其他国家地区的通行做法我们保障您对自己的个人信息行使以下权利
() 访问您的个人信息
您有权访问您的个人信息法律法规规定的例外情况除外如果您想行使数据访问权可以通过以下方式自行访问
档案信息小程序中您可以通过档案管理中新增查阅删除您的档案信息
咨询记录小程序中您可以通过咨询列表查阅历史咨询记录
问卷信息小程序中您可以通过我的问卷查阅历史填写的问卷记录
() 更正您的个人信息
当您发现我们处理的关于您的个人信息有错误时您有权通过客服提出更正申请
() 删除您的个人信息
如果您决定不再使用我们平台需要注销账户请联系客服进入个人中心扫描客户二维码
() 改变您授权同意的范围
您可以通过解除绑定删除信息关闭设备功能修改个人设置联系客服等方式改变您授权我们继续收集个人信息的范围或随时撤回您的授权包括对第三方共享信息授权
() 注销帐号
您随时可注销此前注册的账户您可以通过客服向我们申请注销和删除您的信息
在注销账户之后我们将停止为您提供产品或服务并依据您的要求删除您的个人信息或进行匿名化处理法律法规另有规定的除外
() 个人信息主体获取个人信息副本
您有权复制我们收集的您的个人信息在法律法规规定的条件下如果技术可行您也可以要求我们将您的个人信息转移至您指定的其他主体您可以通过以下方式自行操作通过客服与我们联系我们将在15个工作日内对您的请求进行处理
() 约束信息系统自动决策
在某些业务功能中我们可能仅依据信息系统算法等在内的非人工自动决策机制做出决定如果这些决定显著影响您的合法权益您有权拒绝并要求我们做出解释我们将提供适当的救济方式
() 响应您的上述请求
为保障安全我们可能会先验证您的身份然后再处理您的请求您可能需要提供书面请求或以其他方式证明您的身份验证通过后对于您的请求我们原则上将于15个工作日内做出答复
对于您合理的请求我们原则上不收取费用但对多次重复超出合理限度的请求我们将视情收取一定成本费用对于那些无端重复需要过多技术手段例如需要开发新系统或从根本上改变现行惯例给他人合法权益带来风险或者非常不切实际例如涉及备份磁带上存放的信息的请求我们可能会予以拒绝
在以下情形中按照法律法规要求我们将无法响应您的请求
1与我们履行法律法规规定的义务相关的
2与国家安全国防安全直接相关的
3与公共安全公共卫生重大公共利益直接相关的
4与犯罪侦查起诉审判和判决执行等直接相关的
5有充分证据表明您存在主观恶意或滥用权利的
6响应您的请求将导致您或其他个人组织的合法权益受到严重损害的
7涉及商业秘密的
() 获得解释的权利
您有权要求我们就个人信息处理规则作出解释说明您可以通过第九部分中的联系方式与我们取得联系
我们如何处理未成年人的个人信息
6.1如果没有父母或其他监护人的统一儿童不得创建自己的用户账户如您为儿童的我们要求您请您的父母或其他监护人仔细阅读本政策并在征得您的父母或其他监护人同意的前提下使用我们的服务或产品或向我们提供信息
6.2对于经父母或其他监护人同意使用我们的服务或产品而收集儿童个人信息的情况我们只会在法律法规允许父母或其他监护人明确同意或者保护儿童所必要的情况下使用共享转让或披露此信息
您的个人信息如何在全球范围转移
我们在中华人民共和国境内运营中收集和产生的个人信息储存在中国境内一下情形除外
1. 法律法规有明确规定
2. 获得您的明确授权且经过国家安全相关审查的
针对以上情形我们会确保依据本政策对您的个人信息提供足够的保护
本隐私政策更新及通知
我们的隐私政策可能变更
未经您明确同意我们不会削减您按照本隐私政策所应享有的权利我们会在本页面上发布对本政策所做的任何变更并取得您的同意
对于重大变更我们可能还会提供更为显著的通知(包括对于某些服务我们会通过电子邮件站内信短信小程序服务通知公众号通知弹窗等方式发送通知说明隐私政策的具体变更内容)
本政策重大变更包括但不限于
1我们的服务模式发生重大变化如处理个人信息的目的处理的个人信息类型个人信息的使用方式等
2我们在所有权结构组织架构等方面发生重大变化如业务调整破产并购等引起的所有者变更等
3个人信息共享转移或公开披露的主要对象发生变化
4您参与个人信息处理方面的权利及其行使方式发生重大变化
5我们负责处理个人信息安全的责任部门联络方式及投诉渠道发生变化
6个人信息安全影响评估报告表明存在高风险时
我们还会将本政策的旧版本存档供您查阅
如何联系我们
如您对本政策内容有任何疑问意见或建议或发现个人信息可能被泄露的您可以通过以下方式与我们联系一般情况下我们将在15个工作日内回复您的请求可以通过我的-联系客服联系我们或邮寄至下列地址
公司名称杭州柚康科技有限公司
法定代表人王荣波
联系地址中国浙江省杭州西湖区塘苗路1号2号楼4楼
争议解决
因本政策以及我们处理您个人信息事宜引起的任何争议您可随时联系我公司个人信息保护相关负责人要求给出回复如果您对我们的回复不满意的认为我们的个人信息处理行为严重损害了您的合法权益的您还可以通过向本隐私政策服务提供商健康柚所在地杭州市有管辖权的人民法院提起诉讼来寻求解决方案
感谢您对健康柚平台以及健康柚产品和服务的信任和使用
`

View File

@ -54,8 +54,9 @@ async function bindTeam() {
if (!res || !res.success) {
return toast("关联团队失败");
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: account.value.corpId, teamId: team.value.teamId });
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
set('home-invite-teamId', team.value.teamId);
uni.switchTab({
url: "/pages/home/home",
});

View File

@ -0,0 +1,79 @@
export default `
用户注册服务协议
尊敬的用户
为了保障您的权益请您在使用健康柚平台各项服务下称本服务详细阅读此用户注册服务协议下称本协议如您不同意本协议中的任何条款或对本协议存在质疑请您停止使用本服务如您已经开始或正在使用本服务即表示您已阅读并同意本协议全部内容本协议对您与健康柚平台服务提供者下称我们具有同等法律效力
提示条款
请您务必审慎阅读充分理解各条款内容特别是免除或限制责任的相应条款以及开通或使用某项服务的单独协议限制或免除责任条款将以加粗形式提示您注意当您点击已阅读并同意用户注册服务协议即表示您已充分阅读理解并接受本协议全部内容
您确认在您开始使用本服务前您应具备中华人民共和国法律规定的与您行为相适应的民事行为能力如您未满18周岁或是其他限制行为能力人或无民事行为能力人请您在监护人的监护下阅读并遵守本协议并在监护人的指导下使用本服务如您不具备前述与您行为相适应的民事行为能力则您及您的监护人应依照法律规定承担因此而导致的一切责任
注册和登录
1.1您可通过手机号码注册成为健康柚平台的正式用户在注册过程中您的用户名注册与使用应符合网络道德遵守中华人民共和国法律法规您的用户名和昵称中不能含有威胁淫秽谩骂非法侵害他人正当权益等有争议性的文字如发现您的账号中含有不雅文字或不恰当等名称我们有权要求您更改不予注册或收回账号的权利
1.2您应对自己的账户和密码的安全负责您利用本账户和密码所进行的一切活动引起的任何损失均由您自行承担全部责任如您发现账号遭到未授权的使用或发生任何其他安全问题应立即修改账号密码并妥善保管如有必要请立即联系我们除非有法律规定或司法裁定否则您的账户不得以任何方式转让赠与继承符合个人信息保护法等相关法律规定的除外借用否则您应自行承担由此产生的全部责任
1.3您应保证提供详尽真实准确和完整的个人资料以符合实名认证的要求如果资料发生变动您应及时更改若您提供任何错误不实过时或不完整的资料并为我们所确知或者我们有合理理由怀疑前述资料为错误不实过时或不完整的资料我们有权暂停或终止对您的账号提供服务并拒绝现在或将来申请使用本服务的全部或一部分的请求在此情况下您可通过我们的申诉途径与我们取得联系并修正个人资料经我们核实后恢复账号使用
用户责任
2.1我们运用自己的操作系统通过互联网为您提供互联网电子服务或商品并承担本协议和其它服务协议中对您的责任和义务为使用本服务您必须能够自行通过有法律资格的第三方对您提供互联网接入服务并自行承担以下内容
1自行配备上网所需的设备包括个人电脑调制解调器及其他必要的设备装置
2自行承担上网所需的相关必要费用电话费用网络费用等
3本协议中规定的您的其他责任和义务
2.2您在使用本服务过程中必须遵循以下原则如因您违反相关法律法规或本协议的规定给我们医师或第三方造成任何损失的您统一承担由此产生的损害赔偿责任其中包括但不限于我们为此而支付的律师费用公证费用公告费用检测费用鉴定费用诉讼费用
1遵守中华人民共和国有关的法律法规社会道德规范及公序良俗
2遵守所有与本服务有关的网络协议规定程序和惯例
3不得因任何非法目的而使用本服务不得利用本服务进行任何可能对互联网的正常运转造成不利影响的行为
4不得填写发布传输任何非法的违反公序良俗的虚假的骚扰性的中伤他人的信息资料不得发布介绍个人科室等广告性质的内容不得利用本服务进行任何损害我们或第三方合法权益的行为
5不得侵犯任何第三方的合法知识产权
6不得擅自更改医师处方隐瞒过敏史
7不得利用健康柚平台从事洗钱窃取商业秘密窃取个人信息等违法犯罪活动
8不得干扰健康柚平台的正常运转如恶意投诉医师或平台不得侵入健康柚平台及国家计算机信息系统
9不得教唆他人从事本条所禁止的行为
您单独承担在健康柚平台上发布内容的一切相关责任如您违反以上原则我们有权随时中断或终止向您提供本协议项下的服务而无需对您或患者承担任何责任
2.3我们不接受用户线上咨询包括但不限于以下问题
1非健康类问题如社会意识形态问题等
2医疗司法举证或询证问题
3胎儿性别鉴定问题
4未按提问要求提问如提问时未指定医师却要求具体医师回复
5有危害他人自己的问题
6涉及医师个人信息问题
7故意挑逗侮辱医师的提问
8其他可能危害国家公共安全违反社会公共秩序违背公序良俗侵犯他人合法权益或者损害公共利益的问题
2.4您从中国境内向外传输技术性资料时必须符合中国有关法律法规的规定
2.5您的授权行为对我们而言您的帐号和密码是唯一验证您真实性的依据只要使用了正确的您的账号和密码无论是谁登录均视为已经得到您本人的授权
2.6您同意您勾选知情同意书选项或采纳医师建议即视为风险提示已告知并获得您的知情同意
2.7您的授权行为用户同意授权我们获取患者数据并为患者服务的目的按照最小影响原则使用就诊数据包括用户在其他实体医疗机构的数据请您慎重考虑
用户管理
3.1我们保留在中华人民共和国大陆地区施行之法律允许的范围内独自决定拒绝服务关闭用户账户清除或编辑内容或取消订单的权利
3.2本服务不会提供给被暂时中止或永久终止资格的健康柚平台用户
3.3鉴于移动互联网服务的特殊性我们有权随时变更中止或终止部分或全部的服务如变更中止或终止的服务属于免费服务我们无需通知您也无需对您或任何第三方承担任何责任
3.4您理解我们需要定期或不定期地对提供本服务的平台或相关的设备进行检修或者维护如因此类情况而造成本服务在合理时间内的中断我们无需为此承担任何责任但我们将通过平台提前发布通知
3.5我们不对您所发布信息的删除或储存失败负责我们积极采用数据备份加密等措施保障您数据的安全但不对由于因意外因素导致的数据损失和泄漏负责我们有权审查和监督您的行为是否符合本协议的要求如果您违背了本协议的约定则我们有权中断您的服务
4.6若您的行为不符合本协议的规定我们有权做出独立判断并立即停止向您的帐号提供服务您需对自己在网上的行为承担法律责任您若在健康柚平台上散布和传播反动色情或其他违反国家法律法规的信息我们的系统记录有可能作为您违反法律的证据
责任限制
4.1您同意因下列情形之一的我们不承担任何责任
1用户或其近亲属不配合进行符合诊疗规范的诊疗或提供信息不完整不真实不准确对医师诊断产生误导影响或未按要求披露过敏史等
2医务人员在紧急情况下已经尽到合理诊疗义务或限于当时的医疗水平难以诊疗
3因不可抗力病毒木马黑客攻击系统不稳定第三方服务瑕疵政府行为等原因可能导致的服务中断数据丢失以及其他的损失和风险
4因用户不正当使用网络服务私自在网上进行交易非法使用网络服务或传送的信息有所变动而受到的损害
5其他法律法规规定应当免责的情形
4.2健康柚所有健康资讯仅供参考健康柚致力于提供正确完整的健康资讯但不保证信息的绝对正确性和完整性且不对因信息的不正确或遗漏导致的任何损失或损害承担责任健康柚所提供的任何健康资讯不能替代医师和其他医务人员的建议如自行使用健康柚资料发生偏差健康柚不承担任何法律责任
4.3用户知晓并同意自开始使用健康柚服务时起其就相同或类似服务将不与健康柚签约医师在健康柚外达成任何形式的约定协议如果用户与健康柚签约的医师在健康柚外进行咨询或者相关交易产生的纠纷健康柚将不予受理不承担任何法律责任
用户特别授权
您授权我们使用您注册使用本服务过程中形成的信息并允许我们通过邮件微信短信电话等形式向您传送我们的服务您同意接受我们通过短信邮件电话或其他形式向您发送活动服务或其他相关商业信息如果您不需要我们提供的部分或全部服务的活动服务或其他相关商业信息的服务在您向客服提出申请后将予以中止终止对您提供的该部分或全部服务
知识产权条款
6.1您一旦接受本协议即表明您主动将您在任何时间段在健康柚发表的任何形式的信息内容的财产性权利及任何可转让的权利如著作权财产权全部独家且不可撤销地转让给健康柚所有
6.2杭州柚康科技有限公司拥有健康柚平台内容及资源的著作权等合法权利受国家法律保护有权不时地对本协议及健康柚平台的内容进行修改并在健康柚平台公告无须另行通知您在法律允许的最大限度范围内杭州柚康科技有限公司对本协议及健康柚平台的内容拥有解释权
6.3除法律另有强制性规定外未经健康柚明确的特别书面许可任何单位或个人不得以任何方式非法地全部或部分复制转载引用链接抓取或以其他方式使用健康柚平台的信息内容否则健康柚有权追究其法律责任
个人信息保护
保护您隐私是我们的基本政策您的信任对我们非常重要我们深知个人信息安全的重要性并将按照法律法规要求采取安全保护措施保护您的个人信息安全具体详见隐私政策
协议内容及修改
8.1我们在此特别提醒您本协议内容包括协议正文隐私政策及所有我们已经发布或将来可能发布的各类规则规范通知公告等您确认本协议是处理双方权利义务的契约始终有效法律另有强制性规定或双方另有特别约定的依其规定
8.2根据国家法律法规变化及网络运营需要我们有权不定时修订协议如本协议有任何变更您再次登录的时候系统会提醒您条款变更请您重新确定是否接受确认接受后条款即生效如您不接受的您有权终止使用本服务如您继续使用本服务即视为同意更新后的协议
法律管辖和适用
9.1本协议的订立执行和解释及争议的解决均应适用中华人民共和国大陆地区之有效法律但不包括其冲突法规则
9.2如缔约方就本协议内容或其执行发生任何争议应首先协商解决协商不成时任何一方均可向被告所在地有管辖权的人民法院提起诉讼
如何联系我们
如您对本政策内容有任何疑问意见或建议或发现个人信息可能被泄露的您可以通过以下方式与我们联系一般情况下我们将在15个工作日内回复您的请求可以通过我的-联系客服联系我们或邮寄至下列地址
公司名称杭州柚康科技有限公司
法定代表人王荣波
联系地址中国浙江省杭州西湖区塘苗路1号2号楼4楼
`

View File

@ -1,24 +1,22 @@
// SCSS 变量定义
$font-size-text: 28rpx;
$font-size-tip: 24rpx;
$font-size-text: 30rpx;
$font-size-tip: 28rpx;
$font-size-title: 32rpx;
$text-color-sub: #999;
$primary-color: #0877F1;
.chat-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
}
/* 患者信息栏样式 */
.patient-info-bar {
position: relative;
position: sticky;
top: 0;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 32rpx;
@ -115,6 +113,8 @@ $primary-color: #0877F1;
flex: 1;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
min-height: 0;
}
.chat-content-compressed {
@ -358,7 +358,7 @@ $primary-color: #0877F1;
}
.message-text {
font-size: $font-size-text;
font-size: 30rpx;
line-height: 1.4;
word-wrap: break-word;
word-break: break-all;
@ -384,6 +384,7 @@ $primary-color: #0877F1;
position: relative;
z-index: 200;
padding-bottom: env(safe-area-inset-bottom);
flex-shrink: 0;
}
.input-toolbar {
@ -403,6 +404,14 @@ $primary-color: #0877F1;
padding: 0;
}
.voice-toggle-icon {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
}
.plus-btn {
width: 56rpx;
height: 56rpx;

View File

@ -2,13 +2,13 @@
<view class="input-section">
<view class="input-toolbar">
<view @click="toggleVoiceInput" class="voice-toggle-btn">
<uni-icons v-if="showVoiceInput" fontFamily="keyboard" :size="28">{{ '&#xe61a;' }}</uni-icons>
<image v-if="showVoiceInput" src="/static/jianpan.png" class="voice-toggle-icon" mode="aspectFit"></image>
<uni-icons v-else type="mic" size="28" color="#666" />
</view>
<view class="input-area">
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="40" />
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="80" />
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
</input>

View File

@ -13,7 +13,6 @@
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['accept', 'reject']);

View File

@ -22,7 +22,6 @@
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['apply']);

View File

@ -12,7 +12,6 @@
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['cancel']);

View File

@ -5,57 +5,31 @@
</text>
<!-- 图片消息 -->
<image
v-else-if="message.type === 'TIMImageElem'"
class="message-image"
:src="
message.payload.imageInfoArray[0].LocalURL ||
message.payload.imageInfoArray[0].url
"
mode="aspectFill"
:style="getImageStyle(message.payload.imageInfoArray[0])"
@click="
<image v-else-if="message.type === 'TIMImageElem'" class="message-image" :src="message.payload.imageInfoArray[0].LocalURL ||
message.payload.imageInfoArray[0].url
" mode="aspectFill" :style="getImageStyle(message.payload.imageInfoArray[0])" @click="
$emit(
'previewImage',
message.payload.imageInfoArray[0].LocalURL ||
message.payload.imageInfoArray[0].url
message.payload.imageInfoArray[0].url
)
"
/>
" />
<!-- 语音消息 -->
<view
v-else-if="message.type === 'TIMSoundElem'"
class="voice-message"
:class="{ 'voice-playing': isPlaying }"
:style="getVoiceStyle(message.payload.second)"
@click="$emit('playVoice', message)"
>
<view v-else-if="message.type === 'TIMSoundElem'" class="voice-message" :class="{ 'voice-playing': isPlaying }"
:style="getVoiceStyle(message.payload.second)" @click="$emit('playVoice', message)">
<view class="voice-content">
<view class="voice-icon-wrapper">
<uni-icons
type="sound"
size="20"
:color="message.flow === 'out' ? '#fff' : '#333'"
:class="{ 'icon-animate': isPlaying }"
/>
<uni-icons type="sound" size="20" :color="message.flow === 'out' ? '#fff' : '#333'"
:class="{ 'icon-animate': isPlaying }" />
<!-- 播放中的声波动画 -->
<view v-if="isPlaying" class="sound-wave">
<view
class="wave-bar"
:style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0s"
></view>
<view
class="wave-bar"
:style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0.2s"
></view>
<view
class="wave-bar"
:style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0.4s"
></view>
<view class="wave-bar" :style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0s"></view>
<view class="wave-bar" :style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0.2s"></view>
<view class="wave-bar" :style="{ background: message.flow === 'out' ? '#fff' : '#0877f1' }"
style="animation-delay: 0.4s"></view>
</view>
</view>
<text class="voice-duration">{{ message.payload.second }}"</text>
@ -65,39 +39,24 @@
<!-- 自定义消息卡片 -->
<template v-else-if="message.type === 'TIMCustomElem'">
<!-- 文章消息 -->
<view
v-if="getCustomMessageType(message) === 'article'"
class="article-card"
@click="handleArticleClick(message)"
>
<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"
/>
<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 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"
/>
<image v-if="getSurveyData(message).imgUrl" class="survey-image" :src="getSurveyData(message).imgUrl"
mode="aspectFill" />
</view>
<!-- 其他自定义消息 -->
@ -122,6 +81,7 @@ import { getParsedCustomMessage } from "@/utils/chat-utils.js";
// import MessageCard from "./message-card/message-card.vue";
const props = defineProps({
corpId: String,
message: Object,
patientInfo: Object,
formatTime: Function,
@ -237,7 +197,7 @@ const getArticleData = (message) => {
const handleArticleClick = (message) => {
const { articleId } = getArticleData(message);
uni.navigateTo({
url: `/pages/article/article-detail?id=${articleId}`,
url: `/pages/article/article-detail?id=${articleId}&corpId=${props.corpId}`,
});
};

View File

@ -10,20 +10,20 @@ export default function useGroupAvatars() {
const groupAvatarMap = ref({}) // { groupID: [avatarUrl1, avatarUrl2, ...] }
const teamStore = useTeamStore()
const patientDefaultAvatar = '/static/default-patient-avatar.png'
const teamMemberDefaultAvatar = '/static/default-avatar.svg'
const teamMemberDefaultAvatar = '/static/default-avatar.png'
/**
* 获取单个群聊的头像列表
* @param {string} groupID 群组ID
* @param {string} teamId 团队ID
* @param {string} patientId 患者ID
* @returns {Promise<Array>} 头像URL数组
* @returns {Promise<Array>} 头像URL数组仅包含团队成员头像不包含患者头像
*/
async function getGroupAvatarList(groupID, teamId, patientId) {
try {
if (!teamId) {
console.warn(`群聊 ${groupID} 没有 teamId无法获取头像`)
return [patientDefaultAvatar]
return []
}
// 获取团队成员的头像和名称
@ -31,7 +31,7 @@ export default function useGroupAvatars() {
if (!memberMap || Object.keys(memberMap).length === 0) {
console.warn(`群聊 ${groupID} 的团队成员为空`)
return [patientDefaultAvatar]
return []
}
// 提取头像列表(过滤掉空头像,使用默认头像替代)
@ -43,14 +43,11 @@ export default function useGroupAvatars() {
: teamMemberDefaultAvatar
})
// 添加患者默认头像
avatarList.push(patientDefaultAvatar)
console.log(`群聊 ${groupID} 的头像列表已加载,共 ${avatarList.length} 个头像`)
console.log(`群聊 ${groupID} 的头像列表已加载,共 ${avatarList.length} 个团队成员头像`)
return avatarList
} catch (error) {
console.error(`获取群聊 ${groupID} 的头像列表失败:`, error)
return [patientDefaultAvatar]
return []
}
}
@ -85,10 +82,10 @@ export default function useGroupAvatars() {
/**
* 获取指定群聊的头像列表
* @param {string} groupID 群组ID
* @returns {Array} 头像URL数组
* @returns {Array} 头像URL数组仅包含团队成员头像
*/
function getAvatarList(groupID) {
return groupAvatarMap.value[groupID] || [patientDefaultAvatar]
return groupAvatarMap.value[groupID] || []
}
/**

View File

@ -49,8 +49,8 @@ export default function useGroupChat(groupID) {
if (!member) {
// 如果找不到成员信息,根据是否为团队成员返回默认头像
// 患者userId为当前账户openid使用 default-patient-avatar.png
// 其他情况使用 default-avatar.svg
return userId === openid.value ? '/static/default-patient-avatar.png' : '/static/default-avatar.svg'
// 其他情况使用 default-avatar.png
return userId === openid.value ? '/static/default-patient-avatar.png' : '/static/default-avatar.png'
}
// 如果有头像且不为空字符串,返回头像
@ -59,8 +59,8 @@ export default function useGroupChat(groupID) {
}
// 否则根据是否为团队成员返回默认头像
// 患者使用 default-patient-avatar.png团队成员使用 default-avatar.svg
return member.isTeamMember ? '/static/default-avatar.svg' : '/static/default-patient-avatar.png'
// 患者使用 default-patient-avatar.png团队成员使用 default-avatar.png
return member.isTeamMember ? '/static/default-avatar.png' : '/static/default-patient-avatar.png'
}
// 获取群聊信息和成员头像

View File

@ -99,6 +99,7 @@
<view class="message-bubble" :class="getBubbleClass(message)">
<!-- 消息内容 -->
<MessageTypes
:corpId="corpId"
:message="message"
:formatTime="formatTime"
:playingVoiceId="playingVoiceId"
@ -239,7 +240,7 @@ const showConsultApply = computed(
() =>
orderStatus.value === "finished" ||
orderStatus.value === "cancelled" ||
orderStatus.value === "rejected"
orderStatus.value === "consult_ended"
);
//
@ -524,6 +525,7 @@ const initTIMCallbacks = async () => {
})
.then(() => {
console.log("✓ 收到新消息后已标记为已读");
// tabBar
})
.catch((error) => {
console.error("✗ 标记已读失败:", error);
@ -677,6 +679,7 @@ const loadMessageList = async () => {
})
.then(() => {
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
// tabBar
})
.catch((error) => {
console.error("✗ 标记会话已读失败:", error);
@ -825,6 +828,12 @@ onShow(() => {
checkLoginAndInitTIM();
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
timChatManager.ensureIMConnection();
} else if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
messageList.value = [];
isCompleted.value = false;
lastFirstMessageId.value = "";
loadMessageList();
}
startIMMonitoring(30000);

View File

@ -1,5 +1,10 @@
<template>
<view class="message-page">
<!-- 标题栏 -->
<!-- <view class="message-header">
<text class="header-title">咨询</text>
</view> -->
<!-- 消息列表 -->
<scroll-view
class="message-list"
@ -49,7 +54,7 @@
</view>
<view class="message-preview">
<text class="preview-text">{{
conversation.lastMessage || "暂无消息"
cleanMessageText(conversation.lastMessage) || "暂无消息"
}}</text>
</view>
</view>
@ -75,7 +80,7 @@
</template>
<script setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js";
@ -98,6 +103,57 @@ const refreshing = ref(false);
//
const { loadGroupAvatars, getAvatarList } = useGroupAvatars();
//
const totalUnreadCount = computed(() => {
return conversationList.value.reduce(
(sum, conv) => sum + (conv.unreadCount || 0),
0
);
});
//
const updateUnreadBadgeImmediately = async () => {
try {
if (!globalTimChatManager || !globalTimChatManager.tim) {
console.warn("TIM实例不存在无法更新徽章");
return;
}
const response = await globalTimChatManager.tim.getConversationList();
if (!response || !response.data || !response.data.conversationList) {
console.warn("获取会话列表返回数据异常");
return;
}
//
const totalUnreadCount = response.data.conversationList
.filter(
(conv) => conv.conversationID && conv.conversationID.startsWith("GROUP")
)
.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0);
try {
if (totalUnreadCount > 0) {
uni.setTabBarBadge({
index: 1,
text: totalUnreadCount > 99 ? "99+" : String(totalUnreadCount),
});
console.log("已更新 tabBar 徽章:", totalUnreadCount);
} else {
uni.setTabBarBadge({
index: 1,
text: "",
});
console.log("已清除 tabBar 徽章");
}
} catch (badgeError) {
console.error("更新TabBar徽章失败:", badgeError);
}
} catch (error) {
console.error("更新未读徽章失败:", error);
}
};
// IM
const initIM = async () => {
console.log("=== message.vue initIM 开始 ===");
@ -110,22 +166,12 @@ const initIM = async () => {
);
if (!isIMInitialized.value) {
uni.showLoading({
title: "连接中...",
});
console.log("开始调用 initIMAfterLogin");
const success = await initIMAfterLogin();
console.log("initIMAfterLogin 返回:", success);
uni.hideLoading();
if (!success) {
console.error("initIMAfterLogin 失败");
uni.showToast({
title: "IM连接失败请重试",
icon: "none",
});
console.log("initIMAfterLogin 失败,跳过 IM 初始化");
return false;
}
@ -141,14 +187,10 @@ const initIM = async () => {
);
} else if (globalTimChatManager && !globalTimChatManager.isLoggedIn) {
console.log("IM 已初始化但未登录,尝试重连");
uni.showLoading({
title: "重连中...",
});
const reconnected = await globalTimChatManager.ensureIMConnection();
uni.hideLoading();
if (!reconnected) {
console.error("重连失败");
console.log("重连失败");
return false;
}
}
@ -167,19 +209,22 @@ const loadConversationList = async () => {
// IM TIM
if (!globalTimChatManager) {
throw new Error("IM管理器未初始化");
console.log("IM管理器未初始化跳过加载");
return;
}
if (!globalTimChatManager.tim) {
console.error("TIM实例不存在尝试重新初始化");
console.log("TIM实例不存在尝试重新初始化");
const imReady = await initIM();
if (!imReady || !globalTimChatManager.tim) {
throw new Error("TIM实例初始化失败");
console.log("TIM实例初始化失败跳过加载");
return;
}
}
if (!globalTimChatManager.getGroupList) {
throw new Error("getGroupList 方法不存在");
console.log("getGroupList 方法不存在,跳过加载");
return;
}
// getGroupListSDK
@ -198,18 +243,10 @@ const loadConversationList = async () => {
//
await loadGroupAvatars(conversationList.value);
} else {
console.error("加载群聊列表失败:", result);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
console.log("加载群聊列表失败或返回数据为空");
}
} catch (error) {
console.error("加载会话列表失败:", error);
uni.showToast({
title: error.message || "加载失败,请重试",
icon: "none",
});
console.log("加载会话列表异常:", error.message);
} finally {
loading.value = false;
}
@ -221,11 +258,9 @@ let updateTimer = null;
//
const setupConversationListener = () => {
if (!globalTimChatManager) return;
//
globalTimChatManager.setCallback("onConversationListUpdated", (eventData) => {
console.log("会话列表更新事件:", eventData);
//
if (eventData && !Array.isArray(eventData) && eventData.conversationID) {
const conversationID = eventData.conversationID;
@ -414,16 +449,13 @@ const formatMessageTime = (timestamp) => {
};
//
const handleClickConversation = (conversation) => {
const handleClickConversation = async (conversation) => {
console.log("点击会话:", conversation);
//
const conversationIndex = conversationList.value.findIndex(
(conv) => conv.conversationID === conversation.conversationID
);
if (conversationIndex !== -1) {
conversationList.value[conversationIndex].unreadCount = 0;
console.log("已清空本地未读数:", conversation.name);
}
//
@ -459,24 +491,26 @@ onLoad(() => {
console.log("消息列表页面加载");
});
//
const cleanMessageText = (text) => {
if (!text) return "";
return text.replace(/[\r\n]+/g, " ").trim();
};
//
onShow(async () => {
try {
console.log("消息列表页面显示,开始初始化");
// IM
const imReady = await initIM();
if (!imReady) initIMAfterLogin();
if (!imReady) {
console.log("IM初始化失败继续加载列表");
}
//
await loadConversationList();
//
setupConversationListener();
} catch (error) {
console.error("页面初始化失败:", error);
uni.showToast({
title: error.message || "初始化失败,请重试",
icon: "none",
});
console.log("页面初始化异常:", error.message);
}
});
@ -487,8 +521,6 @@ onHide(() => {
clearTimeout(updateTimer);
updateTimer = null;
}
//
if (globalTimChatManager) {
globalTimChatManager.setCallback("onConversationListUpdated", null);
globalTimChatManager.setCallback("onMessageReceived", null);
@ -501,11 +533,46 @@ onHide(() => {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.message-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #000;
}
.total-unread-badge {
min-width: 40rpx;
height: 40rpx;
padding: 0 12rpx;
background-color: #ff4d4f;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.badge-text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
.message-list {
flex: 1;
width: 100%;
height: 100%;
}
.loading-container,
@ -612,14 +679,17 @@ onHide(() => {
.message-preview {
display: flex;
align-items: center;
min-width: 0;
}
.preview-text {
font-size: 26rpx;
// color: #999;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.load-more {

View File

@ -1,6 +1,7 @@
<template>
<view class="flex flex-col justify-center h-full bg-white">
<view>
<view class="flex flex-col justify-center items-center h-full bg-white">
<image class="service-qrcode" :show-menu-by-longpress="true" src="/static/service-qrcode.jpg" />
<!-- <view>
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
柚康企微客服
</view>
@ -13,7 +14,7 @@
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
我们将为您提供软件使用咨询服务并支持补充病历宣教问卷回访等多种工作模板
</view>
</view>
</view> -->
</view>
</template>
@ -22,5 +23,8 @@ const options = { margin: 10 }
</script>
<style>
/* Using global styles from App.vue */
.service-qrcode {
width: 750rpx;
height: 977rpx;
}
</style>

View File

@ -2,7 +2,7 @@
<view class="page-container">
<!-- Blue background area for header -->
<view class="header-bg"></view>
<!-- User Card -->
<view class="user-card">
<view class="avatar-container">
@ -16,37 +16,75 @@
<!-- Menu List -->
<view class="menu-container">
<view class="px-15 py-12 flex items-center border-b" @click="toPage('/pages/mine/contact')">
<view class="flex-shrink-0 item-icon">
<uni-icons type="headphones" size="22" color="#000"></uni-icons>
</view>
<view class="mr-10 flex-grow w-0 truncate text-lg text-dark">联系客服</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="18" color="#999"></uni-icons>
</view>
</view>
<view class="px-15 py-12 flex items-center border-b" @click="toPage('/pages/login/agreement?type=privacyPolicy')">
<view class="flex-shrink-0 item-icon">
<uni-icons type="locked" size="22" color="#000"></uni-icons>
</view>
<view class="mr-10 flex-grow w-0 truncate text-lg text-dark">隐私保护政策</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="18" color="#999"></uni-icons>
</view>
</view>
<view class="px-15 py-12 flex items-center border-b" @click="toPage('/pages/login/agreement?type=userAgreement')">
<view class="flex-shrink-0 item-icon">
<uni-icons type="locked" size="22" color="#000"></uni-icons>
</view>
<view class="mr-10 flex-grow w-0 truncate text-lg text-dark">用户注册协议</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="18" color="#999"></uni-icons>
</view>
</view>
<view class="px-15 py-12 flex items-center" @click="handleLogout()">
<view class="flex-shrink-0 item-icon">
<uni-icons type="undo" size="22" color="#000"></uni-icons>
</view>
<view class="mr-10 flex-grow w-0 truncate text-lg text-dark">退出登录</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="18" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- <view class="menu-container">
<uni-list>
<uni-list-item title="联系客服" link to="/pages/mine/contact" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="headphones" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="隐私保护政策" link to="/pages/common/privacy" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="locked" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="用户注册协议" link to="/pages/common/agreement" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="paperclip" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="退出登录" link @click="handleLogout">
<template v-slot:header>
<view class="item-icon">
<uni-icons type="undo" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
</uni-list>
</view>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="headphones" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="隐私保护政策" link to="/pages/common/privacy" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="locked" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="用户注册协议" link to="/pages/common/agreement" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="paperclip" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="退出登录" link @click="handleLogout">
<template v-slot:header>
<view class="item-icon">
<uni-icons type="undo" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
</uni-list>
</view> -->
</view>
</template>
@ -74,16 +112,22 @@ const handleLogout = () => {
uni.removeStorageSync('account');
uni.removeStorageSync('openid');
store.account = null;
// Redirect to login or home
uni.reLaunch({
url: '/pages/login/login'
url: '/pages/login/login'
});
}
}
});
};
function toPage(url) {
uni.navigateTo({
url: url
});
}
</script>
<style lang="scss">
@ -108,7 +152,7 @@ page {
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
.avatar-container {
width: 60px;
@ -125,14 +169,14 @@ page {
.user-info {
display: flex;
flex-direction: column;
.nickname {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.phone {
font-size: 14px;
color: #666;
@ -146,8 +190,8 @@ page {
}
.item-icon {
margin-right: 10px;
display: flex;
align-items: center;
margin-right: 10px;
display: flex;
align-items: center;
}
</style>

View File

@ -1,18 +1,14 @@
<template>
<view class="bg-gray-100 min-h-screen">
<!-- Filter Tabs -->
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full" :show-scrollbar="false">
<view
v-for="(tab, index) in tabs"
:key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors"
:class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]"
@click="selectTab(tab.value)"
>
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
@ -28,16 +24,12 @@
</view>
<view v-else class="p-15">
<view
v-for="item in surveys"
:key="item._id"
class="bg-white rounded-lg p-15 mb-15 shadow-sm"
@click="goToDetail(item)"
>
<view v-for="item in surveys" :key="item._id" class="bg-white rounded-lg p-15 mb-15 shadow-sm"
@click="goToDetail(item)">
<!-- Header -->
<view class="flex items-start justify-between mb-10">
<view class="flex items-start flex-1 mr-10 relative">
<!-- Tag -->
<!-- Tag -->
<view class="text-xs text-green-600 border border-green-600 px-5 rounded mr-5 flex-shrink-0 mt-1 tag-box">
问卷调查
</view>
@ -46,7 +38,7 @@
{{ item.title }}
</view>
</view>
<!-- Status (暂不展示未填写/已填写) -->
<view class="flex items-center flex-shrink-0 ml-2">
<text class="text-sm mr-2 text-gray-400">查看</text>
@ -64,7 +56,7 @@
<text class="text-gray-400 mr-2 field-label">团队:</text>
<text>{{ item.team || '-' }}</text>
</view>
<!-- Footer -->
<view class="text-sm text-gray-400">
发送时间: {{ item.time || '-' }}
@ -85,19 +77,19 @@
<script setup>
import { ref } from "vue";
import { onShow, onReachBottom } from "@dcloudio/uni-app";
import { onLoad, onShow, onReachBottom } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const { openid } = storeToRefs(useAccountStore());
const tabs = ref([{ name: "全部", value: "" }]);
const activeTab = ref("");
const corpId = ref('')
const surveys = ref([]);
const total = ref(0);
@ -116,7 +108,7 @@ const loadCustomers = async () => {
const miniAppId = openid.value || uni.getStorageSync("openid");
if (!miniAppId) return;
try {
const res = await api("getMiniAppCustomers", { miniAppId, corpId });
const res = await api("getMiniAppCustomers", { miniAppId, corpId: corpId.value });
if (res && res.success) {
const list = Array.isArray(res.data) ? res.data : [];
tabs.value = [
@ -162,7 +154,7 @@ const loadSurveyList = async (reset = false) => {
loading.value = true;
try {
const params = {
corpId,
corpId: corpId.value,
miniAppId,
page: page.value,
pageSize,
@ -191,6 +183,11 @@ function goToDetail() {
uni.showToast({ title: "详情暂未接入", icon: "none" });
}
onLoad(opts => {
corpId.value = opts.corpId
})
onShow(async () => {
if (!inited.value) {
await loadCustomers();
@ -211,70 +208,224 @@ onReachBottom(() => {
<style scoped>
/* Utility helpers similar to Windi/Tailwind */
.min-h-screen { min-height: 100vh; }
.bg-gray-100 { background-color: #f7f8fa; }
.bg-white { background-color: #ffffff; }
.min-h-screen {
min-height: 100vh;
}
.p-15 { padding: 30rpx; }
.px-15 { padding-left: 30rpx; padding-right: 30rpx; }
.py-10 { padding-top: 20rpx; padding-bottom: 20rpx; }
.py-5 { padding-top: 10rpx; padding-bottom: 10rpx; }
.px-5 { padding-left: 10rpx; padding-right: 10rpx; }
.bg-gray-100 {
background-color: #f7f8fa;
}
.mr-10 { margin-right: 20rpx; }
.mr-5 { margin-right: 10rpx; }
.ml-2 { margin-left: 10rpx; }
.mr-2 { margin-right: 10rpx; }
.mb-15 { margin-bottom: 30rpx; }
.mb-10 { margin-bottom: 20rpx; }
.mb-5 { margin-bottom: 10rpx; }
.mt-1 { margin-top: 6rpx; }
.pb-10 { padding-bottom: 20rpx; }
.bg-white {
background-color: #ffffff;
}
.flex { display: flex; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.flex-1 { flex: 1; }
.flex-shrink-0 { flex-shrink: 0; }
.relative { position: relative; }
.p-15 {
padding: 30rpx;
}
.border { border-width: 1px; border-style: solid; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
.rounded-full { border-radius: 9999px; }
.rounded { border-radius: 8rpx; }
.rounded-lg { border-radius: 12rpx; }
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.shadow-sm { box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.text-xs { font-size: 22rpx; }
.text-sm { font-size: 28rpx; }
.text-base { font-size: 32rpx; }
.font-bold { font-weight: 600; }
.leading-normal { line-height: 1.4; }
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.ml-2 {
margin-left: 10rpx;
}
.mr-2 {
margin-right: 10rpx;
}
.mb-15 {
margin-bottom: 30rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mb-5 {
margin-bottom: 10rpx;
}
.mt-1 {
margin-top: 6rpx;
}
.pb-10 {
padding-bottom: 20rpx;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.relative {
position: relative;
}
.border {
border-width: 1px;
border-style: solid;
}
.border-b {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.rounded-full {
border-radius: 9999px;
}
.rounded {
border-radius: 8rpx;
}
.rounded-lg {
border-radius: 12rpx;
}
.shadow-sm {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 28rpx;
}
.text-base {
font-size: 32rpx;
}
.font-bold {
font-weight: 600;
}
.leading-normal {
line-height: 1.4;
}
/* Colors - Adjusting to match image roughly */
.text-orange-500 { color: #f29e38; }
.bg-orange-100 { background-color: #fff8eb; }
.border-orange-500 { border-color: #f29e38; }
.text-orange-500 {
color: #f29e38;
}
.text-gray-600 { color: #333333; }
.text-gray-500 { color: #999999; }
.text-gray-400 { color: #999999; }
.text-gray-800 { color: #1a1a1a; }
.border-gray-200 { border-color: #e5e5e5; }
.border-gray-100 { border-color: #f5f5f5; }
.bg-orange-100 {
background-color: #fff8eb;
}
.text-green-600 { color: #4b8d5f; }
.border-green-600 { border-color: #4b8d5f; }
.text-red-500 { color: #e04a4a; }
.border-orange-500 {
border-color: #f29e38;
}
.sticky { position: sticky; }
.top-0 { top: 0; }
.z-10 { z-index: 10; }
.w-full { width: 100%; }
.whitespace-nowrap { white-space: nowrap; }
.inline-block { display: inline-block; }
.text-gray-600 {
color: #333333;
}
.text-gray-500 {
color: #999999;
}
.text-gray-400 {
color: #999999;
}
.text-gray-800 {
color: #1a1a1a;
}
.border-gray-200 {
border-color: #e5e5e5;
}
.border-gray-100 {
border-color: #f5f5f5;
}
.text-green-600 {
color: #4b8d5f;
}
.border-green-600 {
border-color: #4b8d5f;
}
.text-red-500 {
color: #e04a4a;
}
.sticky {
position: sticky;
}
.top-0 {
top: 0;
}
.z-10 {
z-index: 10;
}
.w-full {
width: 100%;
}
.whitespace-nowrap {
white-space: nowrap;
}
.inline-block {
display: inline-block;
}
.tag-box {
border-radius: 4rpx;

View File

@ -2,7 +2,7 @@
<view v-if="member" class="flex flex-col h-full items-center justify-center">
<view class="business-card">
<view class="flex">
<image class="mr-10 avatar" :src="member.avatar || '/static/default-avatar.svg'"></image>
<image class="mr-10 avatar" :src="member.avatar || '/static/default-avatar.png'"></image>
<view class="w-0 flex-grow leading-normal">
<view class="flex items-center">
<view class="mr-5 text-lg font-semibold text-dark">{{ member.anotherName }}</view>
@ -12,10 +12,10 @@
</view>
</view>
<view class="mt-12 border-primary qrcode p-15 mx-auto rounded" @click="previewImage()">
<image v-if="qrcode" class="h-full w-full" :src="qrcode"></image>
<image v-if="qrcode" class="h-full w-full" :show-menu-by-longpress="true" :src="qrcode"></image>
</view>
<view class="mt-12 text-base text-center text-dark">
点击识别下方二维码加我为好友
长按识别二维码加我为好友
</view>
</view>
</view>

View File

@ -5,13 +5,14 @@
<view class="mr-5 flex-shrink-0 text-xl text-dark font-semibold">{{ member.anotherName }}</view>
<view class="w-0 flex-grow truncate text-base text-gray">{{ memberJob[member.userid] }}</view>
</view>
<view class="flex">
<!-- <view class="flex">
<view class="flex-shrink-0 text-base text-gray">机构部门</view>
<view class="flex-shrink-0 text-base text-dark">{{ deptNames }}</view>
</view>
</view> -->
<view class="flex">
<view class="flex-shrink-0 text-base text-gray">执业机构</view>
<view class="flex-shrink-0 text-base text-dark">{{ corpNames }}</view>
<view v-if="member.hospitalName" class="flex-shrink-0 text-base text-dark">{{ member.hospitalName }}</view>
<view v-else class="flex-shrink-0 text-base text-dark">{{ corpNames }}</view>
</view>
</view>
<image class="avatar" :src="member.avatar"></image>
@ -36,8 +37,8 @@
<image class="flex-shrink-0 mr-10 section-icon" src="/static/homepage/out-phone.svg"></image>
<view class="w-0 flex-grow text-lg font-semibold">对外联系电话</view>
</view>
<view class="mt-10 text-dark" :class="member.callNumber ? 'text-primary' : 'text-gray'" @click="callNumber()">
{{ member.callNumber || '暂无联系电话' }}
<view class="mt-10 text-dark" :class="member.externalContact ? 'text-primary' : 'text-gray'" @click="callNumber()">
{{ member.externalContact || '暂无联系电话' }}
</view>
<view class="mt-20 flex items-center">
<image class="flex-shrink-0 mr-10 section-icon" src="/static/homepage/sunshine-home.svg"></image>
@ -53,10 +54,10 @@
</view>
<view v-if="qrcode" class="p-15 mt-12 leading-normal bg-white shadow-lg">
<view class="text-lg font-semibold text-center text-dark">
点击识别下方二维码加我为好友
长按识别下方二维码加我为好友
</view>
<view class="mt-12 border-primary qrcode p-15 mx-auto rounded" @click="previewImage()">
<image :src="qrcode" class="h-full w-full"></image>
<image :src="qrcode" :show-menu-by-longpress="true" class="h-full w-full"></image>
</view>
</view>
<view class="safe-bottom-padding"></view>
@ -99,9 +100,9 @@ const services = computed(() => {
})
function callNumber() {
if (member.value && member.value.callNumber) {
if (member.value && member.value.externalContact) {
uni.makePhoneCall({
phoneNumber: member.value.callNumber
phoneNumber: member.value.externalContact
})
}
}

View File

@ -11,7 +11,12 @@
</view>
</view>
</view>
<view v-if="team.teamTroduce" class="mt-12 text-base text-dark leading-normal break-all pre-wrap">
<view class="flex items-center justify-between mt-12" @click="showAllIntroduce = !showAllIntroduce">
<view class="text-dark font-semibold">团队介绍</view>
<uni-icons :type="showAllIntroduce ? 'up' : 'down'" size="20" color="#666"></uni-icons>
</view>
<view v-if="team.teamTroduce" class="mt-10 text-base text-dark leading-normal break-all pre-wrap"
:class="showAllIntroduce ? '' : 'line-clamp-2'" @click="showAllIntroduce = !showAllIntroduce">
{{ team.teamTroduce }}
</view>
<template v-if="teammate.leaders.length">
@ -20,7 +25,7 @@
</view>
<view v-for="i in teammate.leaders" :key="i._id" class="mt-12 flex p-10 border-primary rounded-sm"
@click="toHomePage(i.userid)">
<image class="flex-shrink-0 mr-10 avatar" :src="i.avatar || '/static/default-avatar.svg'"></image>
<image class="flex-shrink-0 mr-10 avatar" :src="i.avatar || '/static/default-avatar.png'"></image>
<view class="w-0 flex-grow leading-normal">
<view class="flex items-center justify-between">
<view class="flex-shrink-0 mr-5 view-lg text-dark font-semibold">{{ i.anotherName }}</view>
@ -45,7 +50,7 @@
</view>
<view v-for="i in teammate.members" :key="i._id" class="mt-12 flex p-10 border-primary rounded-sm"
@click="toHomePage(i.userid)">
<image class="flex-shrink-0 mr-10 avatar" :src="i.avatar || '/static/default-avatar.svg'"></image>
<image class="flex-shrink-0 mr-10 avatar" :src="i.avatar || '/static/default-avatar.png'"></image>
<view class="w-0 flex-grow leading-normal">
<view class="flex items-center justify-between">
<view class="flex-shrink-0 mr-5 view-lg text-dark font-semibold">{{ i.anotherName }}</view>
@ -79,11 +84,12 @@ const corpId = ref('');
const teamId = ref('');
const team = ref(null);
const corpName = ref('');
const showAllIntroduce = ref(false);
const { memberJob, memberList: list } = useJob();
const memberList = computed(() => team.value && Array.isArray(team.value.memberList) ? team.value.memberList : [])
const avatarList = computed(() => memberList.value.map(i => i.avatar || '/static/default-avatar.svg').filter(Boolean))
const avatarList = computed(() => memberList.value.map(i => i.avatar || '/static/default-avatar.png').filter(Boolean))
const teammate = computed(() => {
const memberLeaderList = team.value && Array.isArray(team.value.memberLeaderList) ? team.value.memberLeaderList : [];

BIN
static/jianpan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/service-qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -7,7 +7,6 @@ const env = __VITE_ENV__;
export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const account = ref(null);
const loading = ref(false)
const isIMInitialized = ref(false);
@ -27,11 +26,10 @@ export default defineStore("accountStore", () => {
const res = await api('wxAppLogin', {
appId: appid,
phoneCode,
code,
corpId
code
});
loading.value = false
if (res.success && res.data && res.data.mobile) {
if (res.success && res.data) {
account.value = res.data;
openid.value = res.data.openid;
@ -58,7 +56,7 @@ export default defineStore("accountStore", () => {
// 使用 openid 作为 userID 初始化 IM
const userID = openid.value || uni.getStorageSync('openid');
if (!userID) {
console.error('无法获取 openidIM 初始化失败');
console.log('未获取到有效的 userId跳过 IM 初始化');
return false;
}
@ -66,13 +64,13 @@ export default defineStore("accountStore", () => {
const success = await initGlobalTIM(userID);
if (!success) {
console.error('initGlobalTIM 返回失败');
console.log('initGlobalTIM 返回失败,跳过 IM 初始化');
return false;
}
// 验证 TIM 实例是否真正创建成功
if (!globalTimChatManager || !globalTimChatManager.tim) {
console.error('IM 初始化后 TIM 实例不存在');
console.log('IM 初始化后 TIM 实例不存在,跳过 IM 初始化');
return false;
}
@ -80,7 +78,7 @@ export default defineStore("accountStore", () => {
console.log('IM 初始化成功');
return true;
} catch (error) {
console.error('IM初始化失败:', error);
console.log('IM初始化异常跳过 IM 初始化:', error.message);
return false;
}
}
@ -107,8 +105,7 @@ export default defineStore("accountStore", () => {
uni.removeStorageSync('openid');
}
async function getExternalUserId() {
const corpId = account.value?.corpId;
async function getExternalUserId(corpId) {
const unionid = account.value?.unionid;
const openid = account.value?.openid;
if (!(corpId && unionid && openid) || externalUserId.value) return;
@ -118,9 +115,5 @@ export default defineStore("accountStore", () => {
}
}
watch(account, n => {
getExternalUserId()
}, { immediate: true })
return { account, login, initIMAfterLogin, logout, openid, isIMInitialized, externalUserId, getExternalUserId }
})

View File

@ -35,7 +35,8 @@ const urlsConfig = {
getArticle: 'getArticle',
addArticleSendRecord: 'addArticleSendRecord',
addArticleReadRecord: 'addArticleReadRecord',
getMiniAppReceivedArticleList: 'getMiniAppReceivedArticleList'
getMiniAppReceivedArticleList: 'getMiniAppReceivedArticleList',
getPageDisease:'getPageDisease'
},
member: {
addCustomer: 'add',

View File

@ -144,7 +144,7 @@ function mergeConversationData(conversation, groupDetailsMap) {
name: formatConversationName(groupDetail),
// 更新头像(优先使用已有头像,避免闪动)
avatar: conversation.avatar || groupDetail.patient?.avatar || '/static/default-avatar.svg'
avatar: conversation.avatar || groupDetail.patient?.avatar || '/static/default-avatar.png'
}
}

View File

@ -0,0 +1,135 @@
import { globalTimChatManager } from './tim-chat.js';
/**
* 全局未读消息监听管理器
* 负责监听会话列表更新和消息接收事件实时更新 tabBar 徽章
*/
class GlobalUnreadListenerManager {
constructor() {
this.isInitialized = false;
this.originalConversationListCallback = null;
this.originalMessageReceivedCallback = null;
}
/**
* 初始化全局未读消息监听
* 监听会话列表更新消息接收和消息已读事件
*/
setup() {
if (this.isInitialized) {
console.warn('全局未读消息监听已初始化,跳过重复初始化');
return;
}
if (!globalTimChatManager) {
console.warn('globalTimChatManager 未初始化');
return;
}
// 保存原始回调(在覆盖前保存)
this.originalConversationListCallback = globalTimChatManager.callbacks.onConversationListUpdated;
this.originalMessageReceivedCallback = globalTimChatManager.callbacks.onMessageReceived;
console.log('保存原始回调:', {
hasConversationListCallback: !!this.originalConversationListCallback,
hasMessageReceivedCallback: !!this.originalMessageReceivedCallback
});
// 监听会话列表更新事件(包括消息接收和已读状态变化)
globalTimChatManager.setCallback('onConversationListUpdated', (eventData) => {
console.log('onConversationListUpdated 触发,调用原始回调');
// 调用原始回调(如果存在)
if (this.originalConversationListCallback && typeof this.originalConversationListCallback === 'function') {
this.originalConversationListCallback(eventData);
}
// 更新 tabBar 徽章
this.updateTabBarBadge();
});
// 监听消息接收事件
globalTimChatManager.setCallback('onMessageReceived', (message) => {
console.log('onMessageReceived 触发,调用原始回调');
// 调用原始回调(如果存在)
if (this.originalMessageReceivedCallback && typeof this.originalMessageReceivedCallback === 'function') {
this.originalMessageReceivedCallback(message);
}
// 更新 tabBar 徽章
this.updateTabBarBadge();
});
this.isInitialized = true;
console.log('全局未读消息监听已设置');
}
/**
* 更新 tabBar 徽章
* 获取所有群聊会话的未读数并更新对应的 tabBar 徽章
*/
async updateTabBarBadge() {
try {
if (!globalTimChatManager || !globalTimChatManager.tim) {
console.warn('globalTimChatManager 或 tim 未初始化');
return;
}
const response = await globalTimChatManager.tim.getConversationList();
if (!response || !response.data || !response.data.conversationList) {
console.warn('获取会话列表返回数据异常');
return;
}
const totalUnreadCount = this.calculateGroupUnreadCount(response.data.conversationList);
this.setTabBarBadge(totalUnreadCount);
} catch (error) {
console.error('更新 tabBar 徽章失败:', error);
}
}
/**
* 计算群聊会话的总未读数
* @param {Array} conversationList - 会话列表
* @returns {number} 总未读数
*/
calculateGroupUnreadCount(conversationList) {
return conversationList
.filter((conv) => conv.conversationID && conv.conversationID.startsWith('GROUP'))
.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0);
}
/**
* 设置 tabBar 徽章
* @param {number} count - 未读数
* @param {number} tabIndex - tabBar 索引默认为 1消息页
*/
setTabBarBadge(count, tabIndex = 1) {
if (count > 0) {
uni.setTabBarBadge({
index: tabIndex,
text: count > 99 ? '99+' : String(count)
});
console.log(`已更新 tabBar 徽章(索引 ${tabIndex}:`, count);
} else {
// uni.removeTabBarBadge({
// index: tabIndex
// });
console.log(`已移除 tabBar 徽章(索引 ${tabIndex}`);
}
}
async refreshBadge() {
console.log('手动刷新 tabBar 徽章');
await this.updateTabBarBadge();
}
/**
* 清除监听可选
*/
destroy() {
this.isInitialized = false;
console.log('全局未读消息监听已清除');
}
}
// 导出单例
export const globalUnreadListenerManager = new GlobalUnreadListenerManager();

View File

@ -193,7 +193,12 @@ class TimChatManager {
// 获取用户信息并登录
console.log('开始获取用户信息并登录...')
await this.getUserInfoAndLogin(userID)
const loginSuccess = await this.getUserInfoAndLogin(userID)
if (!loginSuccess) {
console.log('获取用户信息失败,跳过 IM 初始化')
await this.cleanupOldInstance()
return false
}
console.log('用户信息获取并登录成功')
// 等待SDK Ready
@ -206,11 +211,7 @@ class TimChatManager {
return true
} catch (error) {
console.error('=== IM初始化失败 ===', error)
console.error('错误详情:', error.message || error)
console.error('错误堆栈:', error.stack)
this.triggerCallback('onError', `初始化失败: ${error.message || error}`)
console.log('=== IM初始化异常 ===', error.message)
// 初始化失败时清理资源
console.log('初始化失败,开始清理资源...')
await this.cleanupOldInstance()
@ -390,7 +391,8 @@ class TimChatManager {
console.log('本地存储的 userInfo:', userInfo)
if (!userInfo?.userID) {
throw new Error('未找到用户信息,请先登录')
console.log('未找到用户信息,跳过 IM 初始化')
return false
}
this.currentUserID = userInfo.userID
console.log('从本地存储获取到 userID:', this.currentUserID)
@ -403,11 +405,10 @@ class TimChatManager {
console.log('开始登录 TIM...')
await this.loginTIM()
console.log('TIM 登录成功')
return true
} catch (error) {
console.error('获取用户信息失败:', error)
console.error('错误详情:', error.message || error)
this.triggerCallback('onError', `登录失败: ${error.message || error}`)
throw error // 重新抛出错误,让调用者知道登录失败
console.log('获取用户信息异常:', error.message)
return false
}
}
@ -2625,7 +2626,7 @@ class TimChatManager {
conversationID,
groupID,
name: patientName ? `${patientName}的问诊` : groupName || '问诊群聊',
avatar: '/static/default-avatar.svg',
avatar: '/static/default-avatar.png',
lastMessage,
lastMessageTime,
unreadCount: conversation.unreadCount || 0,
@ -2638,7 +2639,7 @@ class TimChatManager {
conversationID: conversation.conversationID,
groupID: conversation.conversationID?.replace('GROUP', '') || '',
name: '问诊群聊',
avatar: '/static/default-avatar.svg',
avatar: '/static/default-avatar.png',
lastMessage: '暂无消息',
lastMessageTime: Date.now(),
unreadCount: 0,
@ -2729,32 +2730,24 @@ class TimChatManager {
console.log('⚠️ TIM未初始化或未登录无法标记会话已读');
return;
}
try {
let formattedConversationID = conversationID
if (!conversationID.startsWith('GROUP')) {
formattedConversationID = `GROUP${conversationID}`
}
console.log('📖 标记会话为已读:', formattedConversationID);
this.tim.setMessageRead({
conversationID: formattedConversationID
}).then(() => {
console.log('✓ 会话已标记为已读:', formattedConversationID);
// 触发会话列表更新回调,通知消息列表页面清空未读数
this.triggerCallback('onConversationListUpdated', {
conversationID: formattedConversationID,
unreadCount: 0
})
}).catch(error => {
console.error('✗ 标记会话已读失败:', error)
this.triggerCallback('onConversationListUpdated', {
conversationID: formattedConversationID,
unreadCount: 0
})
} catch (error) {
console.error('✗ 标记会话已读异常:', error)
}
}
// 更新会话列表
updateConversationListOnNewMessage(message) {
try {

View File

@ -1,12 +1,11 @@
export default function useDebounce(callback, delay = 1000) {
let cd = false;
export default function useDebounce(callback, delay = 500) {
let timer = null
return (...args) => {
if (cd) return;
cd = true;
callback(...args);
setTimeout(() => {
cd = false;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
timer = null;
}, delay);
}
}