Compare commits
37 Commits
86ab607965
...
c56580d6c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c56580d6c0 | ||
|
|
93898e03a9 | ||
|
|
4d7de97445 | ||
|
|
946d29ad97 | ||
|
|
9af63de623 | ||
|
|
624b70fc86 | ||
| 1e5881043b | |||
| 56eeede97e | |||
|
|
ab64e7c9c9 | ||
| c9483c48c1 | |||
| edec00cb37 | |||
| 737cb029e4 | |||
|
|
638a6f3c54 | ||
| f99c412cf2 | |||
| dcb3dda5c0 | |||
|
|
a2e6ddd5dc | ||
|
|
4c8768d490 | ||
|
|
6878ad5834 | ||
|
|
267bc814cb | ||
| 62b03f8343 | |||
| 91db27cca7 | |||
| 1058af4f84 | |||
|
|
cecb26e762 | ||
|
|
fb497292ce | ||
|
|
d7260018ab | ||
|
|
4a8982a91b | ||
|
|
275f658b9d | ||
| c53149c3d5 | |||
|
|
913e6420cc | ||
| 540286e288 | |||
|
|
aa9bc1ca3d | ||
|
|
b2e9102e78 | ||
| 6c0b60db7b | |||
| b73d84f2e4 | |||
|
|
4079f52ed6 | ||
|
|
cecfff6124 | ||
|
|
f4e2f87b6e |
@ -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
|
||||
@ -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
5
.env.production
Normal 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
|
||||
@ -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>
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -42,7 +42,6 @@ const disabledMap = computed(() => props.disableTitles.reduce((m, i) => {
|
||||
return m
|
||||
}, {}))
|
||||
|
||||
provide('addRule', addRule);
|
||||
|
||||
const customRule = ref({});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
40
index.html
40
index.html
@ -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>
|
||||
72
package.json
72
package.json
@ -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": {}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}` })
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
30
pages/login/agreement.vue
Normal 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>
|
||||
@ -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;
|
||||
|
||||
166
pages/login/privacy-policy.js
Normal file
166
pages/login/privacy-policy.js
Normal file
@ -0,0 +1,166 @@
|
||||
export default `
|
||||
隐私政策
|
||||
|
||||
欢迎您访问健康柚平台!
|
||||
您(下称“患者”、“用户”)在健康柚平台使用我们的服务或产品时,我们可能会收集您的相关个人信息。健康柚深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。同时,健康柚承诺,我们将按业界成熟的安全标准,采取相应的安全保护措施来保护您的个人信息。
|
||||
为此,我们制定本隐私政策,适用于我们为患者提供的产品和服务,包括但不限于健康柚微信小程序、柚助手微信小程序。
|
||||
在使用健康柚平台提供的产品或服务前,请您务必认真仔细阅读并确认充分理解本隐私政策,在确认充分理解并同意后再开始使用。一旦您主动选择确认本隐私政策并继续使用的,即视为同意本隐私政策的全部内容;如您不同意相关协议或其中的任何条款的,您应停止访问健康柚平台或使用健康柚产品和服务。
|
||||
如您是未成年人,请您和您的监护人仔细阅读本政策,并在征得您的监护人授权同意的前提下使用我们的服务或向我们提供个人信息。
|
||||
|
||||
本隐私政策将帮助您了解以下内容:
|
||||
1、我们如何收集和使用您的个人信息
|
||||
2、我们如何使用Cookie和同类技术
|
||||
3、我们如何共享、转移、公开披露您的个人信息
|
||||
4、我们如何保存和保护您的个人信息
|
||||
5、您的权利
|
||||
6、我们如何处理未成年人的个人信息
|
||||
7、您的个人信息如何在全球范围转移
|
||||
8、本隐私政策更新及通知
|
||||
9、如何联系我们
|
||||
10、争议解决
|
||||
一、我们如何收集和使用您的个人信息
|
||||
个人信息是指以电子或其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。
|
||||
我们仅会出于本政策所述的以下目的,收集和使用您的个人信息,当我们要将信息用于本政策未载明的其他用途时,会事先征求您的同意。
|
||||
(一) 注册成为用户
|
||||
健康柚平台提供的服务或产品是基于注册用户使用的,如您希望使用健康柚平台提供的服务或产品,则需要通过以下步骤完成账号注册。
|
||||
创建健康柚账户,我们将提供手机号码授权登陆方式,您需要提供您的手机号码。如不提供上述注册信息,您无法使用需注册成为健康柚平台用户方可使用的服务。
|
||||
(二) “成员档案管理”服务
|
||||
用户在使用“成员档案管理”服务时,需先添加成员信息,包括您的姓名、性别、年龄、与成员关系(本人/子女/父母/其他等),目的是协助医生对患者进行管理。
|
||||
(三) “我的服务团队”服务
|
||||
用户在“成员档案管理”中添加成员信息后,使用“团队”服务时,我们可能会收集在健康柚其他平台已与患者建立关系的服务团队信息,目的是与服务团队建立联系,以确保成员聊天咨询的连续性和准确性。我们还可能收集您的登记信息(姓名、身份证号码、性别、年龄),检查检验报告、用药记录、过敏史等个人健康生理信息,以及与个人身体健康状况相关的身高、体重信息,用于了解您的健康状况和咨询需求。如不收集这类信息,我们将无法为您提供健康咨询相关的服务,但不影响您使用其他服务。
|
||||
您与团队建立服务关系后,您理解并同意将添加的个人信息、病历信息、就诊记录将向该团队展示;
|
||||
(四) “回访”服务
|
||||
在您的服务团队人员对您进行回访的过程中,我们可能收集您与团队人员的沟通聊天记录、为您制定的回访计划、向您发送的文章,以及您填写的问卷信息。以支持您在健康柚平台上获得持续、可追溯的回访服务。
|
||||
(五) 客户服务
|
||||
当您向我们提出问题、投诉或建议时,我们需要收集您的通信/通话记录、您提供的联系方式信息、您为了证明相关事实提供的信息以及您参与问卷调查时向我们发送的问卷答复信息。我们收集上述信息的法律依据是基于向您提供健康柚平台服务所必需,为您解决您在使用平台及享受服务过程中所遇到的问题,以及向您提供相关问题的处理方案和结果。如不收集这类信息,您的投诉、建议和反馈将无法得到及时、有效处理,但不影响您使用其它服务。
|
||||
(六) 保障功能运行和风控服务
|
||||
为保障您正常使用我们及我们关联公司、合作伙伴提供的服务,维护我们系统基础功能的正常运行,拦截钓鱼网站、欺诈,防止网络漏洞、计算机病毒、网络攻击、网络侵入,改进及优化我们的服务体验以及保障您的账号安全,我们需要整合我们已根据本隐私政策合法收集的您的个人基本信息(姓名、身份证号码、手机号码、性别、年龄)、个人生理健康信息(既往病史、用药记录、体重),并收集、使用或整合您的网络身份标识信息(BSSID、DNS地址、IP地址、SSID、代理信息、网络类型、网络名称、掩码信息)、个人常用设备信息(IMEI、IMSI、设备ID、MAC地址、IDFA、IDFV、AndroidId、MCC、MNC、UUID、标准国家码、操作系统信息、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楼。
|
||||
十、争议解决
|
||||
因本政策以及我们处理您个人信息事宜引起的任何争议,您可随时联系我公司个人信息保护相关负责人要求给出回复,如果您对我们的回复不满意的,认为我们的个人信息处理行为严重损害了您的合法权益的,您还可以通过向本隐私政策服务提供商健康柚所在地【杭州市】有管辖权的人民法院提起诉讼来寻求解决方案。
|
||||
感谢您对健康柚平台以及健康柚产品和服务的信任和使用!
|
||||
|
||||
`
|
||||
@ -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",
|
||||
});
|
||||
|
||||
79
pages/login/user-agreement.js
Normal file
79
pages/login/user-agreement.js
Normal 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楼。
|
||||
|
||||
|
||||
`
|
||||
@ -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;
|
||||
|
||||
@ -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">{{ '' }}</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>
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const emit = defineEmits(['accept', 'reject']);
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const emit = defineEmits(['apply']);
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const emit = defineEmits(['cancel']);
|
||||
|
||||
|
||||
@ -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}`,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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] || []
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
// 获取群聊信息和成员头像
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// 直接调用getGroupList,它会自动等待SDK就绪
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
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
BIN
static/service-qrcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@ -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('无法获取 openid,IM 初始化失败');
|
||||
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 }
|
||||
})
|
||||
@ -35,7 +35,8 @@ const urlsConfig = {
|
||||
getArticle: 'getArticle',
|
||||
addArticleSendRecord: 'addArticleSendRecord',
|
||||
addArticleReadRecord: 'addArticleReadRecord',
|
||||
getMiniAppReceivedArticleList: 'getMiniAppReceivedArticleList'
|
||||
getMiniAppReceivedArticleList: 'getMiniAppReceivedArticleList',
|
||||
getPageDisease:'getPageDisease'
|
||||
},
|
||||
member: {
|
||||
addCustomer: 'add',
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
135
utils/global-unread-listener.js
Normal file
135
utils/global-unread-listener.js
Normal 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();
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user