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

This commit is contained in:
Jafeng 2026-01-29 16:22:43 +08:00
commit b0080c1df8
88 changed files with 8556 additions and 699 deletions

View File

@ -4,3 +4,4 @@ MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate

8
.env.ip Normal file
View File

@ -0,0 +1,8 @@
MP_API_BASE_URL=http://192.168.60.2:8080
MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate

View File

@ -1,5 +1,7 @@
MP_API_BASE_URL=http://localhost:8080
MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600072268
MP_TIM_SDK_APP_ID=1600123876
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate

42
App.vue
View File

@ -5,9 +5,20 @@ import { globalTimChatManager } from "@/utils/tim-chat.js";
export default {
onLaunch: function () {
// pinia store getActivePinia
const { login } = useAccountStore();
login();
console.log("App Launch: ");
const { account, login, initIMAfterLogin } = useAccountStore();
// IM
if (account && account.openid) {
console.log("App Launch: 已有登录信息,初始化 IM");
initIMAfterLogin().catch(err => {
console.error('IM初始化失败:', err);
});
} else {
console.log("App Launch: 无登录信息,开始登录");
login().catch(err => {
console.error('自动登录失败:', err);
});
}
},
onShow: function () {
console.log("App Show");
@ -29,13 +40,36 @@ export default {
</script>
<style lang="scss">
$primary-color: #0074ff;
$primary-color: #0877F1;
page {
height: 100%;
background: #f5f5f5;
}
/* 全局按钮样式 - 使用项目主题色 */
button[type="primary"],
.button-primary,
uni-button[type="primary"] {
background-color: $primary-color !important;
border-color: $primary-color !important;
color: #fff !important;
}
button[type="primary"]:not([disabled]):active,
.button-primary:active,
uni-button[type="primary"]:not([disabled]):active {
background-color: darken($primary-color, 10%) !important;
border-color: darken($primary-color, 10%) !important;
}
/* 微信小程序按钮样式覆盖 */
.wx-button[type="primary"] {
background-color: $primary-color !important;
border-color: $primary-color !important;
color: #fff !important;
}
.shadow-up {
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
rgba(0, 0, 0, 0.1) 0px -10px 15px -3px, rgba(0, 0, 0, 0.1) 0px -4px 6px -4px;

View File

@ -1,6 +1,6 @@
<template>
<view>
<image class="mx-auto" :style="style" src="/static/empty.svg"></image>
<image class="block mx-auto" :style="style" src="/static/empty.svg"></image>
<view v-if="showText" class="mt-10 text-base text-center text-gray">{{ text }}</view>
</view>
</template>
@ -22,4 +22,9 @@ const props = defineProps({
})
const style = computed(() => `width: ${props.size}rpx;height:${props.size}rpx`)
</script>
</script>
<style>
.block {
display: block;
}
</style>

View File

@ -25,6 +25,7 @@ export default function useGuard() {
const onShowOptions = ref({})
function toLoginPage(options, path) {
const params = Object.keys(options).map(key => `${key}=${options[key]}`).join('&');
const redirectUrl = encodeURIComponent(`${path}?${params}`);
uni.redirectTo({

19
hooks/useInfoCheck.js Normal file
View File

@ -0,0 +1,19 @@
import { storeToRefs } from 'pinia';
import useAccountStore from "@/store/account.js";
import { confirm } from '@/utils/widget';
export default function useInfoCheck() {
const { doctorInfo } = storeToRefs(useAccountStore());
function withInfo(fn) {
return async (...args) => {
if (!doctorInfo.value || !doctorInfo.value.anotherName) {
await confirm('请先完善您的个人信息,方可使用该功能!', { cancelText: '再等等', confirmText: '去完善' })
return uni.navigateTo({ url: '/pages/work/profile' });
}
return fn(...args);
}
}
return { withInfo }
}

View File

@ -1,34 +1,40 @@
{
"name": "ykt-wxapp",
"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"
}
},
"name": "ykt-wxapp",
"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": {}
}
"title": "本地",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
},
"ip": {
"title": "本机ip",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
}
}
},
"devDependencies": {}
}

View File

@ -1,9 +1,9 @@
{
"pages": [
{
"path": "pages/login/login",
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "登录"
"navigationBarTitleText": "消息"
}
},
{
@ -12,20 +12,6 @@
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息",
"navigationStyle": "custom"
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false
}
},
{
"path": "pages/message/common-phrases",
"style": {
@ -38,12 +24,31 @@
"navigationBarTitleText": "宣教文章"
}
},
{
"path": "pages/message/article-detail",
"style": {
"navigationBarTitleText": "宣教文章"
}
},
{
"path": "pages/message/survey-list",
"style": {
"navigationBarTitleText": "问卷列表"
}
},
{
"path": "pages/webview/webview",
"style": {
"navigationBarTitleText": "预览"
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false
}
},
{
"path": "pages/case/search",
"style": {
@ -205,6 +210,48 @@
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "授权登录"
}
},
{
"path": "pages/work/team/invite/invite-patient",
"style": {
"navigationBarTitleText": "邀请患者"
}
},
{
"path": "pages/work/team/invite/invite-teammate",
"style": {
"navigationBarTitleText": "邀请成员"
}
},
{
"path": "pages/work/team/list/team-list",
"style": {
"navigationBarTitleText": "我的团队"
}
},
{
"path": "pages/work/team/edit/team-edit",
"style": {
"navigationBarTitleText": "修改团队信息"
}
},
{
"path": "pages/work/team/detail/team-detail",
"style": {
"navigationBarTitleText": "团队信息"
}
},
{
"path": "pages/work/service/contact-service",
"style": {
"navigationBarTitleText": "联系企微客服"
}
}
],
"globalStyle": {

View File

@ -5,12 +5,12 @@
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view>
<view class="login-btn-wrap">
<button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
<!-- <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
手机号快捷登录
</button> -->
<button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
手机号快捷登录
</button>
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
手机号快捷登录
</button> -->
<button v-else class="login-btn" type="primary" @click="remind()">
手机号快捷登录
</button>
@ -69,8 +69,9 @@ function toHome() {
async function getPhoneNumber(e) {
const phoneCode = e && e.detail && e.detail.code;
if (e && !phoneCode) return;
// if (e && !phoneCode) return;
const res = await login(phoneCode);
if (res && redirectUrl.value) {
await attempToPage(redirectUrl.value);
} else if (res && !(doctorInfo.value && doctorInfo.value.anotherName)) {

View File

@ -4,43 +4,66 @@
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from '@/utils/api';
import { set } from "@/utils/cache";
import { toast } from '@/utils/widget';
const teamId = ref('');
const corpId = ref('');
const loading = ref(false);
const team = ref(null)
const { useLoad } = useGuard()
const { account, doctorInfo } = storeToRefs(useAccountStore());
async function changeTeam({ teamId, corpId, corpName }) {
loading.value = true;
const res = await api('getTeamData', { teamId, corpId, withCorpName: true });
loading.value = false;
if (res && res.data) {
team.value = res.data;
team.value.corpName = corpName;
set('invite-team-info', {
corpId: team.value.corpId,
teamId: team.value.teamId,
corpName: team.value.corpName,
teamName: team.value.name,
avatars: team.value && Array.isArray(team.value.memberList) ? team.value.memberList.map(item => item.avatar || '') : [],
})
uni.redirectTo({
url: '/pages/login/login?source=teamInvite'
})
async function getTeam(teamId) {
if (teamId) {
const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
if (res && res.data) {
return toJoinTeam(teamId)
} else {
await toast(res?.message || '获取团队信息失败')
}
} else {
toast(res?.message || '获取团队信息失败')
await toast('获取团队信息失败')
}
}
onLoad(options => {
teamId.value = options.teamId || '';
corpId.value = options.corpId || '';
changeTeam({ teamId: teamId.value, corpId: corpId.value });
async function toJoinTeam(teamId) {
//
if (doctorInfo.value && doctorInfo.value.userid) {
const data = {
teamId,
corpId: account.value.corpId,
id: doctorInfo.value._id,
userId: doctorInfo.value.userid
}
const res = await api('joinTheInvitedTeam', data);
if (res && res.success) {
await toast('加入团队成功');
return uni.switchTab({
url: '/pages/work/work'
})
} else {
await toast(res?.message || '加入团队失败')
}
} else {
uni.redirectTo({
url: `/pages/work/profile?type=joinTeam&teamId=${teamId}`
})
}
}
useLoad(options => {
const href = typeof options.q === 'string' ? decodeURIComponent(options.q) : '';
const [, url = ''] = href.split('?');
const data = url.split('&').reduce((acc, cur) => {
const [key, value] = cur.split('=');
acc[key] = value;
return acc;
}, {})
if (data.type === 'inviteTeam') {
getTeam(data.teamId)
}
})
</script>
<style>

View File

@ -0,0 +1,201 @@
<template>
<view class="article-detail-page">
<view v-if="loading" class="loading-container">
<uni-icons type="spinner-cycle" size="40" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="error" class="error-container">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="loadArticle">重试</button>
</view>
<scroll-view v-else scroll-y class="article-content">
<view class="article-header">
<text class="article-title">{{ articleData.title }}</text>
<text class="article-date">{{ articleData.date }}</text>
</view>
<view class="article-body">
<view class="rich-text-wrapper">
<rich-text :nodes="articleData.content"></rich-text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import { ref } from "vue";
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const loading = ref(true);
const error = ref("");
const articleData = ref({
title: "",
content: "",
date: "",
});
let articleId = "";
// 使
const processRichTextContent = (html) => {
if (!html) return "";
// img
let processedHtml = html.replace(
/<img/gi,
'<img style="max-width:100%;height:auto;display:block;margin:10px 0;"'
);
//
processedHtml = processedHtml.replace(
/style="[^"]*width:\s*\d+px[^"]*"/gi,
(match) => {
return match.replace(/width:\s*\d+px;?/gi, "max-width:100%;");
}
);
//
processedHtml = processedHtml.replace(
/<table/gi,
'<table style="max-width:100%;overflow-x:auto;display:block;"'
);
//
processedHtml = `<div style="width:100%;overflow-x:hidden;word-wrap:break-word;word-break:break-all;">${processedHtml}</div>`;
return processedHtml;
};
//
const loadArticle = async () => {
loading.value = true;
error.value = "";
try {
const res = await api("getArticle", { id: articleId, corpId });
if (res.success && res.data) {
//
let date = "";
if (res.data.createTime) {
const d = new Date(res.data.createTime);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
date = `${year}-${month}-${day}`;
}
articleData.value = {
title: res.data.title || "宣教文章",
content: processRichTextContent(res.data.content || ""),
date: date,
};
} else {
error.value = res.message || "加载文章失败";
}
} catch (err) {
console.error("加载文章失败:", err);
error.value = "加载失败,请重试";
} finally {
loading.value = false;
}
};
onLoad((options) => {
if (options.id) {
articleId = options.id;
loadArticle();
} else {
error.value = "文章信息不完整";
loading.value = false;
}
});
</script>
<style scoped lang="scss">
.article-detail-page {
width: 100%;
height: 100vh;
background-color: #fff;
}
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 40rpx;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.error-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
text-align: center;
}
.retry-btn {
padding: 16rpx 60rpx;
background-color: #0877f1;
color: #fff;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
}
.article-content {
height: 100vh;
}
.article-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 1px solid #eee;
}
.article-title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 1.6;
margin-bottom: 20rpx;
}
.article-date {
display: block;
font-size: 24rpx;
color: #999;
}
.article-body {
padding: 0;
}
.rich-text-wrapper {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.rich-text-wrapper ::v-deep rich-text {
width: 100%;
}
.rich-text-wrapper ::v-deep rich-text img {
max-width: 100% !important;
height: auto !important;
display: block;
}
</style>

View File

@ -19,10 +19,10 @@
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === (cate._id || 'all') }"
:class="{ active: currentCateId === cate._id }"
@click="selectCategory(cate)"
>
{{ cate.name }}
{{ cate.label }}
</view>
</scroll-view>
</view>
@ -54,17 +54,17 @@
>
<view class="article-content" @click="previewArticle(article)">
<text class="article-title">{{ article.title }}</text>
<text class="article-date">创建时间{{ article.date }}</text>
</view>
<view class="article-action">
<button
class="send-btn"
size="mini"
type="primary"
@click="sendArticle(article)"
>
发送
</button>
<view class="article-footer">
<text class="article-date">{{ article.date }}</text>
<button
class="send-btn"
size="mini"
type="primary"
@click.stop="sendArticle(article)"
>
发送
</button>
</view>
</view>
</view>
@ -94,7 +94,9 @@
</view>
</view>
<scroll-view scroll-y class="preview-content">
<rich-text :nodes="previewArticleData.content"></rich-text>
<view class="rich-text-wrapper">
<rich-text :nodes="previewArticleData.content"></rich-text>
</view>
</scroll-view>
<view class="preview-footer">
<button class="preview-close-btn" @click="closePreview">关闭</button>
@ -106,22 +108,29 @@
<script setup>
import { ref, onMounted } from "vue";
import { getArticleCateList, getArticleList, getArticle } from "@/utils/api.js";
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const accountStore = useAccountStore();
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const userId = ref("");
//
const pageParams = ref({
groupId: "",
userId: "",
corpId: "",
});
//
const searchTitle = ref("");
let searchTimer = null;
//
const categoryList = ref([{ _id: "", name: "全部" }]);
const currentCateId = ref("");
const categoryList = ref([{ _id: "", label: "全部" }]);
const currentCateId = ref(""); // ""_id
//
const articleList = ref([]);
@ -140,10 +149,10 @@ const previewPopup = ref(null);
//
const getCategoryList = async () => {
try {
const res = await getArticleCateList({ corpId: corpId });
const res = await api("getArticleCateList", { corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", name: "全部" }, ...cates];
categoryList.value = [{ _id: "", label: "全部" }, ...cates];
}
} catch (error) {
console.error("获取分类列表失败:", error);
@ -189,7 +198,7 @@ const loadArticleList = async () => {
params.cateIds = [currentCateId.value];
}
const res = await getArticleList(params);
const res = await api("getArticleList", params);
if (res.success && res.list) {
const { list = [], total: count = 0 } = res;
const formattedList = list.map((item) => {
@ -238,17 +247,47 @@ const loadMore = () => {
loadArticleList();
};
// 使
const processRichTextContent = (html) => {
if (!html) return "";
// img
let processedHtml = html.replace(
/<img/gi,
'<img style="max-width:100%;height:auto;display:block;margin:10px 0;"'
);
//
processedHtml = processedHtml.replace(
/style="[^"]*width:\s*\d+px[^"]*"/gi,
(match) => {
return match.replace(/width:\s*\d+px;?/gi, "max-width:100%;");
}
);
//
processedHtml = processedHtml.replace(
/<table/gi,
'<table style="max-width:100%;overflow-x:auto;display:block;"'
);
//
processedHtml = `<div style="width:100%;overflow-x:hidden;word-wrap:break-word;word-break:break-all;">${processedHtml}</div>`;
return processedHtml;
};
//
const previewArticle = async (article) => {
try {
uni.showLoading({ title: "加载中..." });
const res = await getArticle({ id: article._id, corpId: corpId });
const res = await api("getArticle", { id: article._id, corpId: corpId });
uni.hideLoading();
if (res.success && res.data ) {
if (res.success && res.data) {
previewArticleData.value = {
title: res.data.title || article.title,
content: res.data.content || "",
content: processRichTextContent(res.data.content || ""),
};
previewPopup.value?.open();
} else {
@ -273,32 +312,31 @@ const closePreview = () => {
};
//
const sendArticle = (article) => {
//
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage) {
//
const doctorInfo = accountStore.doctorInfo;
userId.value = doctorInfo?.userid || accountStore.openid;
// 线
uni.$emit("sendArticle", {
article: article,
corpId: corpId,
userId: userId.value,
const sendArticle = async (article) => {
try {
const { doctorInfo } = useAccountStore();
const result = await api("sendArticleMessage", {
groupId: pageParams.value.groupId,
fromAccount: doctorInfo.userid,
articleId: article._id,
title: article.title || "宣教文章",
imgUrl: article.cover || "",
desc: "点击查看详情",
});
uni.showToast({
title: "已选择文章",
icon: "success",
});
//
setTimeout(() => {
if (result.success) {
uni.navigateBack();
}, 500);
} else {
throw new Error(result.message || "发送失败");
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
@ -307,6 +345,19 @@ const goBack = () => {
uni.navigateBack();
};
//
onLoad((options) => {
if (options.groupId) {
pageParams.value.groupId = options.groupId;
}
if (options.userId) {
pageParams.value.userId = options.userId;
}
if (options.corpId) {
pageParams.value.corpId = options.corpId;
}
});
onMounted(() => {
getCategoryList();
loadArticleList();
@ -358,18 +409,20 @@ onMounted(() => {
}
.category-item {
padding: 32rpx 24rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
position: relative;
}
.category-item.active {
background-color: #fff;
color: #1890ff;
color: #0877f1;
font-weight: bold;
border-left: 4rpx solid #1890ff;
border-left: 4rpx solid #0877f1;
}
.article-list {
@ -397,39 +450,48 @@ onMounted(() => {
}
.article-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease;
}
.article-item:active {
background-color: #f5f5f5;
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
gap: 16rpx;
}
.article-title {
font-size: 28rpx;
color: #333;
line-height: 1.5;
line-height: 1.6;
word-break: break-all;
margin-bottom: 12rpx;
font-weight: 500;
}
.article-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.article-date {
flex: 1;
font-size: 24rpx;
color: #999;
}
.article-action {
flex-shrink: 0;
}
.send-btn {
padding: 8rpx 32rpx;
flex-shrink: 0;
font-size: 26rpx;
padding: 8rpx 32rpx;
height: auto;
line-height: 1.4;
}
.loading-more,
@ -486,10 +548,28 @@ onMounted(() => {
.preview-content {
flex: 1;
padding: 30rpx;
padding: 0;
overflow-y: auto;
}
.rich-text-wrapper {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
/* rich-text 内部样式 */
.rich-text-wrapper ::v-deep rich-text {
width: 100%;
}
.rich-text-wrapper ::v-deep rich-text img {
max-width: 100% !important;
height: auto !important;
display: block;
}
.preview-footer {
padding: 20rpx;
border-top: 1px solid #eee;

View File

@ -136,7 +136,7 @@ $primary-color: #0877F1;
.message-list {
padding: 0 16rpx;
padding-bottom: 20rpx;
padding-bottom: 60rpx; /* 增加底部内边距,防止被小程序底部横线遮挡 */
}
.message-item {
@ -350,10 +350,10 @@ $primary-color: #0877F1;
flex: 1;
padding: 0 46rpx;
background-color: #f3f5fa;
border-radius: 50rpx;
border-radius: 20rpx;
margin: 0 16rpx;
font-size: 28rpx;
height: 96rpx;
height: 80rpx;
border: none;
outline: none;
box-sizing: border-box;
@ -372,8 +372,8 @@ $primary-color: #0877F1;
justify-content: flex-start;
background: #fff;
border-top: 1rpx solid #eee;
padding: 20rpx 0 8rpx 60rpx;
gap: 40rpx;
padding: 20rpx 0 40rpx 60rpx;
gap: 40rpx 50rpx;
flex-wrap: wrap;
background-color: #f5f5f5;
}
@ -1254,11 +1254,12 @@ $primary-color: #0877F1;
.article-card {
display: flex;
align-items: center;
background-color: #fff;
background-color: transparent;
border-radius: 12rpx;
padding: 24rpx;
max-width: 500rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
box-shadow: none;
background-color: #fff;
}
.article-content {
@ -1297,11 +1298,11 @@ $primary-color: #0877F1;
/* 文章卡片在不同消息流中的样式 */
.message-right .article-card {
background-color: #e8f4ff;
background-color: transparent;
}
.message-left .article-card {
background-color: #fff;
background-color: transparent;
}
/* 问卷卡片样式 */

View File

@ -621,7 +621,7 @@ onMounted(() => {
<style scoped lang="scss">
// 使
$primary-color: #0877f1;
$primary-color: #0877F1;
$primary-light: #e8f3ff;
$primary-gradient-start: #1b5cc8;
$primary-gradient-end: #0877f1;

View File

@ -73,10 +73,13 @@ const props = defineProps({
patientInfo: { type: Object, default: () => ({}) },
chatRoomBusiness: { type: Object, default: () => ({}) },
formatTime: { type: Function, required: true },
groupId: { type: String, default: '' },
userId: { type: String, default: '' },
corpId: { type: String, default: '' },
});
// Emits
const emit = defineEmits(["messageSent", "scrollToBottom"]);
const emit = defineEmits(["messageSent", "scrollToBottom", "endConsult"]);
//
const inputText = ref("");
@ -364,7 +367,7 @@ const goToCommonPhrases = () => {
//
const goToArticleList = () => {
uni.navigateTo({
url: '/pages/message/article-list'
url: `/pages/message/article-list?groupId=${props.groupId}&userId=${props.userId}&corpId=${props.corpId}`
});
};
@ -375,11 +378,29 @@ const goToSurveyList = () => {
});
};
//
const handleEndConsult = () => {
uni.showModal({
title: '确认结束问诊',
content: '确定要结束本次问诊吗?结束后将无法继续对话。',
confirmText: '确定结束',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
//
showMorePanel.value = false;
//
emit('endConsult');
}
}
});
};
const morePanelButtons = [
{ text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker },
{
text: "回访任务",
icon: "/static/icon/zhaopian.png",
icon: "/static/icon/huifangrenwu.png",
action: showImagePicker,
},
{
@ -400,7 +421,7 @@ const morePanelButtons = [
{
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png",
action: showImagePicker,
action: handleEndConsult,
},
];

View File

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

View File

@ -81,7 +81,7 @@
mode="aspectFill"
/>
</view>
<!-- 问卷消息 -->
<view
v-else-if="getCustomMessageType(message) === 'survey'"
@ -99,7 +99,7 @@
mode="aspectFill"
/>
</view>
<!-- 其他自定义消息 -->
<!-- <view
v-else
@ -207,12 +207,12 @@ const getCustomMessageType = (message) => {
try {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return data.type || '';
return data.type || "";
}
} catch (error) {
console.error('解析自定义消息失败:', error);
console.error("解析自定义消息失败:", error);
}
return '';
return "";
};
//
@ -220,35 +220,25 @@ const getArticleData = (message) => {
try {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return {
title: data.title || '宣教文章',
desc: data.desc || '宣教文章',
url: data.url || '',
imgUrl: data.imgUrl || ''
};
return data;
}
} catch (error) {
console.error('解析文章数据失败:', error);
console.error("解析文章数据失败:", error);
}
return {
title: '宣教文章',
desc: '宣教文章',
url: '',
imgUrl: ''
title: "宣教文章",
desc: "宣教文章",
url: "",
imgUrl: "",
};
};
//
const handleArticleClick = (message) => {
const articleData = getArticleData(message);
if (articleData.url) {
//
//
console.log('打开文章:', articleData.url);
// uni.navigateTo({
// url: `/pages/article/detail?url=${encodeURIComponent(articleData.url)}`
// });
}
const { articleId } = getArticleData(message);
uni.navigateTo({
url: `/pages/message/article-detail?id=${articleId}`,
});
};
//
@ -257,20 +247,20 @@ const getSurveyData = (message) => {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return {
title: data.title || '填写问卷',
desc: data.desc || '请填写问卷',
url: data.url || '',
imgUrl: data.imgUrl || ''
title: data.title || "填写问卷",
desc: data.desc || "请填写问卷",
url: data.url || "",
imgUrl: data.imgUrl || "",
};
}
} catch (error) {
console.error('解析问卷数据失败:', error);
console.error("解析问卷数据失败:", error);
}
return {
title: '填写问卷',
desc: '请填写问卷',
url: '',
imgUrl: ''
title: "填写问卷",
desc: "请填写问卷",
url: "",
imgUrl: "",
};
};
@ -279,7 +269,7 @@ const handleSurveyClick = (message) => {
const surveyData = getSurveyData(message);
if (surveyData.url) {
//
console.log('打开问卷:', surveyData.url);
console.log("打开问卷:", surveyData.url);
// uni.navigateTo({
// url: `/pages/survey/fill?url=${encodeURIComponent(surveyData.url)}`
// });

View File

@ -85,7 +85,6 @@
}}</text>
</view>
<!-- 消息气泡 -->
<view class="message-bubble" :class="getBubbleClass(message)">
<!-- 消息内容 -->
<MessageTypes
@ -132,8 +131,16 @@
ref="chatInputRef"
:timChatManager="timChatManager"
:formatTime="formatTime"
:groupId="
chatInfo.conversationID
? chatInfo.conversationID.replace('GROUP', '')
: ''
"
:userId="openid"
:corpId="corpId"
@scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)"
@endConsult="handleEndConsult"
/>
</view>
</template>
@ -161,7 +168,7 @@ import {
handleViewDetail,
checkIMConnectionStatus,
} from "@/utils/chat-utils.js";
import { sendConsultRejectedMessage } from "@/utils/api.js";
import api from "@/utils/api.js";
import useGroupChat from "./hooks/use-group-chat";
import MessageTypes from "./components/message-types.vue";
import ChatInput from "./components/chat-input.vue";
@ -171,6 +178,10 @@ import RejectReasonModal from "./components/reject-reason-modal.vue";
const timChatManager = globalTimChatManager;
//
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID || "";
//
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
const { initIMAfterLogin } = useAccountStore();
@ -194,21 +205,21 @@ const chatInfo = ref({
userID: "",
avatar: "/static/home/avatar.svg",
});
//
const isEvaluationPopupOpen = ref(false);
//
const showConsultAccept = ref(false);
//
const orderStatus = ref("");
// - pending
const showConsultAccept = computed(() => orderStatus.value === "pending");
//
const showRejectReasonModal = ref(false);
//
const messageList = ref([]);
const isLoading = ref(false);
const scrollIntoView = ref("");
//
const isLoadingMore = ref(false);
const isCompleted = ref(false);
@ -223,66 +234,58 @@ function isSystemMessage(message) {
try {
// payload.data
if (message.payload?.data) {
const data = typeof message.payload.data === 'string'
? JSON.parse(message.payload.data)
: message.payload.data;
const data =
typeof message.payload.data === "string"
? JSON.parse(message.payload.data)
: message.payload.data;
//
if (data.type === 'system_message') {
if (data.type === "system_message") {
return true;
}
}
// description
if (message.payload?.description === '系统消息标记') {
if (message.payload?.description === "系统消息标记") {
return true;
}
//
if (message.payload?.description === 'SYSTEM_NOTIFICATION') {
if (message.payload?.description === "SYSTEM_NOTIFICATION") {
return true;
}
} catch (error) {
console.error('判断系统消息失败:', error);
console.error("判断系统消息失败:", error);
}
return false;
}
//
const fetchGroupOrderStatus = async () => {
try {
const result = await api("getGroupListByGroupId", {
groupId: groupId.value,
});
if (result.success && result.data) {
orderStatus.value = result.data.orderStatus || "";
console.log("获取群组订单状态:", {
orderStatus: orderStatus.value,
groupId: groupId.value,
});
} else {
console.error("获取群组订单状态失败:", result.message);
}
} catch (error) {
console.error("获取群组订单状态异常:", error);
}
};
//
function checkConsultPendingStatus() {
//
for (let i = messageList.value.length - 1; i >= 0; i--) {
const message = messageList.value[i];
if (message.type === "TIMCustomElem" && message.payload?.data) {
try {
const data = typeof message.payload.data === 'string'
? JSON.parse(message.payload.data)
: message.payload.data;
// consult_pending
if (data.type === 'system_message' && data.messageType === 'consult_pending') {
showConsultAccept.value = true;
return;
}
//
if (data.type === 'system_message' &&
(data.messageType === 'consult_accepted' ||
data.messageType === 'consult_ended' ||
data.messageType === 'consult_rejected')) {
showConsultAccept.value = false;
return;
}
} catch (error) {
console.error('解析系统消息失败:', error);
}
}
}
//
showConsultAccept.value = false;
//
fetchGroupOrderStatus();
}
//
@ -292,7 +295,7 @@ function getBubbleClass(message) {
return "image-bubble";
}
if (message.type === "TIMCustomElem") {
return message.flow === "out" ? "user-bubble" : "doctor-bubble-blue";
return message.flow === "out" ? "" : "";
}
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
}
@ -310,6 +313,7 @@ onLoad((options) => {
if (options.userID) {
chatInfo.value.userID = options.userID;
}
checkLoginAndInitTIM();
updateNavigationTitle();
});
@ -320,7 +324,7 @@ const checkLoginAndInitTIM = async () => {
uni.showLoading({
title: "连接中...",
});
const success = await initIMAfterLogin(openid.value);
const success = await initIMAfterLogin();
uni.hideLoading();
if (!success) {
uni.showToast({
@ -374,14 +378,35 @@ const initTIMCallbacks = async () => {
if (!existingMessage) {
messageList.value.push(message);
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
//
checkConsultPendingStatus();
//
if (isSystemMessage(message)) {
fetchGroupOrderStatus();
}
// 使
nextTick(() => {
scrollToBottom(true);
});
// 0
if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
timChatManager.tim
.setMessageRead({
conversationID: chatInfo.value.conversationID,
})
.then(() => {
console.log("✓ 收到新消息后已标记为已读");
// 0
timChatManager.triggerCallback('onConversationListUpdated', {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
});
})
.catch((error) => {
console.error("✗ 标记已读失败:", error);
});
}
}
});
@ -498,7 +523,7 @@ const initTIMCallbacks = async () => {
};
//
const loadMessageList = () => {
const loadMessageList = async () => {
if (isLoading.value) {
console.log("正在加载中,跳过重复加载");
return;
@ -515,23 +540,32 @@ const loadMessageList = () => {
mask: false,
});
//
await fetchGroupOrderStatus();
timChatManager.enterConversation(chatInfo.value.conversationID || "test1");
//
// -
if (
timChatManager.tim &&
timChatManager.isLoggedIn &&
chatInfo.value.conversationID
) {
console.log("标记会话为已读:", chatInfo.value.conversationID);
timChatManager.tim
.setMessageRead({
conversationID: chatInfo.value.conversationID,
})
.then(() => {
console.log("会话已标记为已读:", chatInfo.value.conversationID);
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
//
timChatManager.triggerCallback('onConversationListUpdated', {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
});
})
.catch((error) => {
console.error("标记会话已读失败:", error);
console.error("标记会话已读失败:", error);
});
}
};
@ -687,13 +721,11 @@ onHide(() => {
stopIMMonitoring();
});
//
const sendCommonPhrase = (content) => {
if (chatInputRef.value) {
chatInputRef.value.sendTextMessageFromPhrase(content);
}
};
//
defineExpose({
sendCommonPhrase,
@ -703,45 +735,37 @@ defineExpose({
const handleAcceptConsult = async () => {
try {
uni.showLoading({
title: '处理中...',
title: "处理中...",
});
//
const customMessage = {
data: JSON.stringify({
type: 'system_message',
messageType: 'consult_accepted',
content: '医生已接诊',
timestamp: Date.now(),
}),
description: '系统消息标记',
extension: '',
};
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace('GROUP', ''),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
//
const result = await api("acceptConsultation", {
groupId: groupId.value,
adminAccount: account.value?.userId || "",
extraData: {
acceptedBy: account.value?.userId || "",
acceptedByName: account.value?.name || "医生",
},
});
const sendResult = await timChatManager.tim.sendMessage(message);
uni.hideLoading();
if (sendResult.code === 0) {
showConsultAccept.value = false;
uni.hideLoading();
if (result.success) {
//
await fetchGroupOrderStatus();
uni.showToast({
title: '已接受问诊',
icon: 'success',
title: "已接受问诊",
icon: "success",
});
} else {
throw new Error(sendResult.message || '发送失败');
throw new Error(result.message || "操作失败");
}
} catch (error) {
console.error('接受问诊失败:', error);
console.error("接受问诊失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败',
icon: 'none',
title: error.message || "操作失败",
icon: "none",
});
}
};
@ -756,20 +780,20 @@ const handleRejectConsult = () => {
const handleRejectReasonConfirm = async (reason) => {
try {
showRejectReasonModal.value = false;
uni.showLoading({
title: '处理中...',
title: "处理中...",
});
//
const memberName = account.value?.name || '医生';
const memberName = account.value?.name || "医生";
// ID
const groupId = chatInfo.value.conversationID.replace('GROUP', '');
const currentGroupId = chatInfo.value.conversationID.replace("GROUP", "");
//
const result = await sendConsultRejectedMessage({
groupId,
const result = await api("sendConsultRejectedMessage", {
groupId: currentGroupId,
memberName,
reason,
});
@ -777,20 +801,21 @@ const handleRejectReasonConfirm = async (reason) => {
uni.hideLoading();
if (result.success) {
showConsultAccept.value = false;
//
await fetchGroupOrderStatus();
uni.showToast({
title: '已拒绝问诊',
icon: 'success',
title: "已拒绝问诊",
icon: "success",
});
} else {
throw new Error(result.message || '发送失败');
throw new Error(result.message || "发送失败");
}
} catch (error) {
console.error('拒绝问诊失败:', error);
console.error("拒绝问诊失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || '操作失败',
icon: 'none',
title: error.message || "操作失败",
icon: "none",
});
}
};
@ -799,6 +824,40 @@ const handleRejectReasonConfirm = async (reason) => {
const handleRejectReasonCancel = () => {
showRejectReasonModal.value = false;
};
//
const handleEndConsult = async () => {
try {
uni.showLoading({
title: "处理中...",
});
// groupId
const result = await api("endConsultation", {
groupId: groupId.value,
adminAccount: account.value?.userId || "",
extraData: {
endBy: account.value?.userId || "",
endByName: account.value?.name || "医生",
endReason: "问诊完成",
},
});
uni.hideLoading();
if (result.success) {
uni.showToast({
title: "问诊已结束",
icon: "success",
});
} else {
throw new Error(result.message || "操作失败");
}
} catch (error) {
console.error("结束问诊失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
};
//
onUnmounted(() => {
@ -809,81 +868,10 @@ onUnmounted(() => {
timChatManager.setCallback("onMessageReceived", null);
timChatManager.setCallback("onMessageListLoaded", null);
timChatManager.setCallback("onError", null);
//
uni.$off("sendArticle");
//
uni.$off("sendSurvey");
});
//
uni.$on("sendArticle", async (data) => {
const { article, corpId, userId } = data;
if (!article || !article._id) {
uni.showToast({
title: "文章信息不完整",
icon: "none",
});
return;
}
try {
//
const env = __VITE_ENV__;
const baseUrl = env.VITE_PATIENT_PAGE_BASE_URL || "";
//
const articleUrl = `${baseUrl}pages/article/index?id=${article._id}&corpId=${corpId}`;
//
const customMessage = {
data: JSON.stringify({
type: "article",
title: article.title || "宣教文章",
desc: "宣教文章",
url: articleUrl,
imgUrl:
"https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/19-%E9%97%AE%E5%8D%B7.png?sign=55a4cd77c418b2c548b65792a2cf6bce&t=1701328694",
}),
description: "ARTICLE",
extension: "",
};
//
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace("GROUP", ""),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
});
const sendResult = await timChatManager.tim.sendMessage(message);
if (sendResult.code === 0) {
uni.showToast({
title: "发送成功",
icon: "success",
});
//
// await addArticleSendRecord({
// corpId,
// userId,
// articleId: article._id,
// customerId: chatInfo.value.userID
// });
} else {
throw new Error(sendResult.message || "发送失败");
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
}
});
//
uni.$on("sendSurvey", async (data) => {
const { survey, corpId, userId, sendSurveyId } = data;
@ -895,7 +883,6 @@ uni.$on("sendSurvey", async (data) => {
});
return;
}
try {
//
const env = __VITE_ENV__;
@ -907,8 +894,7 @@ uni.$on("sendSurvey", async (data) => {
const customerName = chatInfo.value.customerName || "";
//
const { createSurveyRecord } = await import("@/utils/api.js");
const recordRes = await createSurveyRecord({
const recordRes = await api("createSurveyRecord", {
corpId,
userId,
surveryId: survey._id,

View File

@ -1,5 +1,25 @@
<template>
<view class="message-page">
<!-- 标签页切换 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'processing' }"
@click="switchTab('processing')"
>
<text class="tab-text">处理中</text>
<view v-if="activeTab === 'processing'" class="tab-indicator"></view>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'finished' }"
@click="switchTab('finished')"
>
<text class="tab-text">已结束</text>
<view v-if="activeTab === 'finished'" class="tab-indicator"></view>
</view>
</view>
<!-- 消息列表 -->
<scroll-view
class="message-list"
@ -9,18 +29,10 @@
@refresherrefresh="handleRefresh"
@scrolltolower="handleLoadMore"
>
<!-- 加载状态 -->
<view
v-if="loading && conversationList.length === 0"
class="loading-container"
>
<text class="loading-text">加载中...</text>
</view>
<!-- 消息列表项 -->
<view
v-for="conversation in conversationList"
:key="conversation.conversationID"
v-for="conversation in filteredConversationList"
:key="conversation.groupID || conversation.conversationID"
class="message-item"
@click="handleClickConversation(conversation)"
>
@ -39,7 +51,15 @@
<view class="content">
<view class="header">
<text class="name">{{ conversation.name || "未知群聊" }}</text>
<view class="name-info">
<text class="name">{{ formatPatientName(conversation) }}</text>
<text
v-if="conversation.patientSex || conversation.patientAge"
class="patient-info"
>
{{ formatPatientInfo(conversation) }}
</text>
</view>
<text class="time">{{
formatMessageTime(conversation.lastMessageTime)
}}</text>
@ -54,15 +74,20 @@
<!-- 空状态 -->
<view
v-if="!loading && conversationList.length === 0"
v-if="!loading && filteredConversationList.length === 0"
class="empty-container"
>
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
<text class="empty-text">暂无消息</text>
<text class="empty-text">{{
activeTab === "processing" ? "暂无处理中的会话" : "暂无已结束的会话"
}}</text>
</view>
<!-- 加载更多 -->
<view v-if="hasMore && conversationList.length > 0" class="load-more">
<view
v-if="hasMore && filteredConversationList.length > 0"
class="load-more"
>
<text class="load-more-text">{{
loadingMore ? "加载中..." : "上拉加载更多"
}}</text>
@ -72,22 +97,60 @@
</template>
<script setup>
import { ref } from "vue";
import { ref, watch, computed } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js";
import { globalTimChatManager } from "@/utils/tim-chat.js";
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
//
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
const { initIMAfterLogin } = useAccountStore();
// IM
watch(isIMInitialized, (newValue) => {
console.log("IM初始化状态变化:", newValue);
if (newValue) {
// IM
loadConversationList();
}
});
//
const conversationList = ref([]);
const loading = ref(false);
const loadingMore = ref(false);
const hasMore = ref(false);
const refreshing = ref(false);
const activeTab = ref("processing");
// orderStatus
const filteredConversationList = computed(() => {
if (activeTab.value === "processing") {
// pending() processing()
const filtered = conversationList.value.filter(
(conv) =>
conv.orderStatus === "pending" || conv.orderStatus === "processing"
);
return filtered;
} else {
// cancelled()completed()finished()
const filtered = conversationList.value.filter(
(conv) =>
conv.orderStatus === "cancelled" ||
conv.orderStatus === "completed" ||
conv.orderStatus === "finished"
);
return filtered;
}
});
//
const switchTab = (tab) => {
if (activeTab.value === tab) return;
activeTab.value = tab;
console.log("切换到标签页:", tab);
};
// IM
const initIM = async () => {
@ -95,7 +158,7 @@ const initIM = async () => {
uni.showLoading({
title: "连接中...",
});
const success = await initIMAfterLogin(openid.value);
const success = await initIMAfterLogin();
uni.hideLoading();
if (!success) {
@ -122,31 +185,27 @@ const initIM = async () => {
//
const loadConversationList = async () => {
if (loading.value) return;
// loading.value = true;
loading.value = true;
try {
console.log("开始加载群聊列表");
if (!globalTimChatManager || !globalTimChatManager.getGroupList) {
throw new Error("IM管理器未初始化");
}
// getGroupListSDK
const result = await globalTimChatManager.getGroupList();
if (result && result.success && result.groupList) {
conversationList.value = result.groupList
.map((group) => ({
conversationID: group.conversationID || `GROUP${group.groupID}`,
groupID: group.groupID,
name: group.patientName
? `${group.patientName}的问诊`
: group.name || "问诊群聊",
avatar: group.avatar || "/static/default-avatar.png",
lastMessage: group.lastMessage || "暂无消息",
lastMessageTime: group.lastMessageTime || Date.now(),
unreadCount: group.unreadCount || 0,
doctorId: group.doctorId,
patientName: group.patientName,
}))
.sort((a, b) => b.lastMessageTime - a.lastMessageTime);
//
conversationList.value = await mergeConversationWithGroupDetails(
result.groupList
);
console.log("=== 会话列表加载完成 ===");
console.log("总会话数:", conversationList.value.length);
// 3 orderStatus
conversationList.value.slice(0, 3).forEach((conv, index) => {
console.log(
`会话 ${index} - orderStatus: ${conv.orderStatus}, 名称: ${conv.name}`
);
});
console.log(
"群聊列表加载成功,共",
@ -171,49 +230,8 @@ const loadConversationList = async () => {
}
};
//
const extractMessagePreview = (message) => {
if (!message) return "暂无消息";
const payload = message.payload;
if (!payload) return "暂无消息";
//
if (message.type === "TIMTextElem") {
return payload.text || "暂无消息";
}
//
if (message.type === "TIMImageElem") {
return "[图片]";
}
//
if (message.type === "TIMSoundElem") {
return "[语音]";
}
//
if (message.type === "TIMVideoFileElem") {
return "[视频]";
}
//
if (message.type === "TIMFileElem") {
return "[文件]";
}
//
if (message.type === "TIMCustomElem") {
const description = payload.description;
if (description === "SYSTEM_NOTIFICATION") {
return "[系统消息]";
}
return "[消息]";
}
return "暂无消息";
};
//
let updateTimer = null;
//
const setupConversationListener = () => {
@ -223,54 +241,107 @@ const setupConversationListener = () => {
globalTimChatManager.setCallback("onConversationListUpdated", (eventData) => {
console.log("会话列表更新事件:", eventData);
//
if (eventData.reason === "NEW_MESSAGE_RECEIVED_IN_CURRENT_CONVERSATION" ||
eventData.reason === "NEW_MESSAGE_RECEIVED") {
const conversation = eventData.conversation;
if (!conversation) return;
const conversationID = conversation.conversationID;
const conversationIndex = conversationList.value.findIndex(
//
if (eventData && !Array.isArray(eventData) && eventData.conversationID) {
const conversationID = eventData.conversationID;
const existingIndex = conversationList.value.findIndex(
(conv) => conv.conversationID === conversationID
);
if (conversationIndex !== -1) {
//
const existingConversation = conversationList.value[conversationIndex];
existingConversation.lastMessage = conversation.lastMessage || "暂无消息";
existingConversation.lastMessageTime = conversation.lastMessageTime || Date.now();
existingConversation.unreadCount = conversation.unreadCount || 0;
//
const [updatedConversation] = conversationList.value.splice(
conversationIndex,
1
);
conversationList.value.unshift(updatedConversation);
console.log("已更新会话:", existingConversation.name);
} else {
//
conversationList.value.unshift({
conversationID: conversationID,
groupID: conversation.groupID || conversationID.replace("GROUP", ""),
name: conversation.name || "问诊群聊",
avatar: conversation.avatar || "/static/default-avatar.png",
lastMessage: conversation.lastMessage || "暂无消息",
lastMessageTime: conversation.lastMessageTime || Date.now(),
unreadCount: conversation.unreadCount || 0,
});
console.log("已添加新会话");
if (existingIndex !== -1) {
//
if (eventData.unreadCount !== undefined) {
conversationList.value[existingIndex].unreadCount =
eventData.unreadCount;
console.log(
`已清空会话未读数: ${conversationList.value[existingIndex].name}, unreadCount: ${eventData.unreadCount}`
);
}
}
return;
}
if (!eventData || !Array.isArray(eventData)) {
console.warn("会话列表更新事件数据格式错误");
return;
}
//
if (updateTimer) {
clearTimeout(updateTimer);
}
updateTimer = setTimeout(async () => {
//
const groupConversations = eventData.filter(
(conv) => conv.conversationID && conv.conversationID.startsWith("GROUP")
);
console.log(`收到 ${groupConversations.length} 个群聊会话更新`);
// 使 TimChatManager
const formattedConversations = groupConversations.map((conv) =>
globalTimChatManager.formatConversationData(conv)
);
//
const mergedConversations = await mergeConversationWithGroupDetails(
formattedConversations
);
if (!mergedConversations || mergedConversations.length === 0) {
console.log("合并后的会话数据为空,跳过更新");
return;
}
let needSort = false;
//
mergedConversations.forEach((conversationData) => {
const conversationID = conversationData.conversationID;
const existingIndex = conversationList.value.findIndex(
(conv) => conv.conversationID === conversationID
);
if (existingIndex !== -1) {
const existing = conversationList.value[existingIndex];
if (
existing.lastMessage !== conversationData.lastMessage ||
existing.lastMessageTime !== conversationData.lastMessageTime ||
existing.unreadCount !== conversationData.unreadCount ||
existing.patientName !== conversationData.patientName ||
existing.patientSex !== conversationData.patientSex ||
existing.patientAge !== conversationData.patientAge
) {
Object.assign(
conversationList.value[existingIndex],
conversationData
);
needSort = true;
console.log(
`已更新会话: ${conversationData.name}, unreadCount: ${conversationData.unreadCount}`
);
}
} else {
//
conversationList.value.push(conversationData);
needSort = true;
console.log(`已添加新会话: ${conversationData.name}`);
}
});
//
if (needSort) {
conversationList.value.sort(
(a, b) => b.lastMessageTime - a.lastMessageTime
);
}
}, 100); // 100ms
});
//
globalTimChatManager.setCallback("onMessageReceived", (message) => {
console.log("消息列表页面收到新消息:", message);
//
//
const conversationID = message.conversationID;
const conversationIndex = conversationList.value.findIndex(
(conv) => conv.conversationID === conversationID
@ -278,13 +349,55 @@ const setupConversationListener = () => {
if (conversationIndex !== -1) {
const conversation = conversationList.value[conversationIndex];
// onConversationListUpdated
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const isViewingConversation =
currentPage?.route === "pages/message/index";
//
if (isViewingConversation) {
console.log("用户正在查看该会话,不增加未读数");
return;
}
//
conversation.unreadCount = (conversation.unreadCount || 0) + 1;
console.log("已更新会话未读数:", conversation.name);
console.log(
"已更新会话未读数:",
conversation.name,
"unreadCount:",
conversation.unreadCount
);
}
});
};
//
const formatPatientName = (conversation) => {
return conversation.patientName || "未知患者";
};
// +
const formatPatientInfo = (conversation) => {
const parts = [];
//
if (conversation.patientSex === "男") {
parts.push("男");
} else if (conversation.patientSex === "女") {
parts.push("女");
}
//
if (conversation.patientAge) {
parts.push(`${conversation.patientAge}`);
}
return parts.join(" ");
};
//
const formatMessageTime = (timestamp) => {
if (!timestamp) return "";
@ -331,6 +444,15 @@ const formatMessageTime = (timestamp) => {
const handleClickConversation = (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);
}
//
uni.navigateTo({
url: `/pages/message/index?conversationID=${conversation.conversationID}&groupID=${conversation.groupID}`,
@ -351,7 +473,6 @@ const handleLoadMore = () => {
//
const handleRefresh = async () => {
refreshing.value = true;
try {
await loadConversationList();
} finally {
@ -374,9 +495,6 @@ onShow(async () => {
return;
}
//
await loadConversationList();
//
setupConversationListener();
} catch (error) {
@ -390,6 +508,12 @@ onShow(async () => {
//
onHide(() => {
//
if (updateTimer) {
clearTimeout(updateTimer);
updateTimer = null;
}
//
if (globalTimChatManager) {
globalTimChatManager.setCallback("onConversationListUpdated", null);
@ -403,11 +527,53 @@ onHide(() => {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.tabs-container {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 28rpx 0;
position: relative;
cursor: pointer;
&.active {
.tab-text {
color: #1890ff;
font-weight: 500;
}
}
}
.tab-text {
font-size: 32rpx;
color: #666;
transition: color 0.3s;
}
.tab-indicator {
position: absolute;
bottom: 0;
width: 60rpx;
height: 6rpx;
background-color: #1890ff;
border-radius: 3rpx;
}
.message-list {
width: 100%;
height: 100%;
flex: 1;
}
.loading-container,
@ -438,7 +604,7 @@ onHide(() => {
.message-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
padding: 10rpx 32rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
@ -532,4 +698,10 @@ onHide(() => {
font-size: 24rpx;
color: #999;
}
.patient-info {
font-size: 28rpx;
padding-left: 10rpx;
color: #999;
}
</style>

View File

@ -3,33 +3,41 @@
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input class="search-input" v-model="searchName" placeholder="输入问卷名称搜索" @input="handleSearch" />
<input
class="search-input"
v-model="searchName"
placeholder="输入问卷名称搜索"
@input="handleSearch"
/>
</view>
</view>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === (cate._id || 'all') }"
:class="{ active: currentCateId === cate._id }"
@click="selectCategory(cate)"
>
{{ cate.name }}
{{ cate.label }}
</view>
</scroll-view>
</view>
<view class="survey-list">
<scroll-view
scroll-y
<scroll-view
scroll-y
class="survey-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
>
<view v-if="loading && surveyList.length === 0" class="loading-container">
<view
v-if="loading && surveyList.length === 0"
class="loading-container"
>
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
@ -39,17 +47,24 @@
</view>
<view v-else>
<view
v-for="survey in surveyList"
:key="survey._id"
<view
v-for="survey in surveyList"
:key="survey._id"
class="survey-item"
>
<view class="survey-content" @click="previewSurvey(survey)">
<text class="survey-title">{{ survey.name }}</text>
<text class="survey-desc">{{ survey.description || '暂无问卷说明' }}</text>
<text class="survey-desc">{{
survey.description || "暂无问卷说明"
}}</text>
</view>
<view class="survey-action">
<button class="send-btn" size="mini" type="primary" @click="sendSurvey(survey)">
<button
class="send-btn"
size="mini"
type="primary"
@click="sendSurvey(survey)"
>
发送
</button>
</view>
@ -67,31 +82,27 @@
</scroll-view>
</view>
</view>
<view class="footer">
<button class="cancel-btn" @click="goBack">取消</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getSurveyCateList, getSurveyList, createSurveyRecord } from '@/utils/api.js';
import useAccountStore from '@/store/account.js';
import EmptyData from '@/components/empty-data.vue';
import { ref, onMounted } from "vue";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const accountStore = useAccountStore();
const corpId = env.MP_CORP_ID;
const userId = ref('');
const userId = ref("");
//
const searchName = ref('');
const searchName = ref("");
let searchTimer = null;
//
const categoryList = ref([{ _id: '', name: '全部' }]);
const currentCateId = ref('');
const categoryList = ref([{ _id: "", label: "全部" }]);
const currentCateId = ref("");
//
const surveyList = ref([]);
@ -99,27 +110,24 @@ const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
const emptyText = ref('');
const emptyText = ref("");
//
const getCategoryList = async () => {
try {
const res = await getSurveyCateList({ corpId: corpId });
if (res.success && res.data) {
const cates = res.data.list || [];
categoryList.value = [
{ _id: '', name: '全部' },
...cates
];
const res = await api("getSurveyCateList", { corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", label: "全部" }, ...cates];
}
} catch (error) {
console.error('获取分类列表失败:', error);
console.error("获取分类列表失败:", error);
}
};
//
const selectCategory = (cate) => {
currentCateId.value = cate._id || '';
currentCateId.value = cate._id || "";
page.value = 1;
surveyList.value = [];
loadSurveyList();
@ -140,7 +148,7 @@ const handleSearch = () => {
//
const loadSurveyList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
@ -148,8 +156,8 @@ const loadSurveyList = async () => {
page: page.value,
pageSize: pageSize,
name: searchName.value.trim(),
status: 'enable',
showCount: false
status: "enable",
showCount: false,
};
// ID
@ -157,9 +165,9 @@ const loadSurveyList = async () => {
params.cateIds = [currentCateId.value];
}
const res = await getSurveyList(params);
if (res.success && res.data) {
const { list = [], total: count = 0 } = res.data;
const res = await api("getSurveyList", params);
if (res.success && res) {
const { list = [], total: count = 0 } = res;
if (page.value === 1) {
surveyList.value = list;
@ -167,20 +175,20 @@ const loadSurveyList = async () => {
surveyList.value = [...surveyList.value, ...list];
}
total.value = count;
emptyText.value = '暂无问卷信息';
emptyText.value = "暂无问卷信息";
} else {
emptyText.value = res.message || '加载失败';
emptyText.value = res.message || "加载失败";
uni.showToast({
title: res.message || '获取问卷列表失败',
icon: 'none'
title: res.message || "获取问卷列表失败",
icon: "none",
});
}
} catch (error) {
console.error('加载问卷列表失败:', error);
emptyText.value = '加载失败';
console.error("加载问卷列表失败:", error);
emptyText.value = "加载失败";
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
@ -196,17 +204,29 @@ const loadMore = () => {
//
const previewSurvey = (survey) => {
//
uni.showToast({
title: '预览功能开发中',
icon: 'none'
const timestamp = Date.now();
const previewUrl = `https://www.youcan365.com/surveyDev/#/pages/survey/survey?surveyId=${survey.surveyId}&t=${timestamp}`;
// #ifdef H5
window.open(previewUrl, '_blank');
// #endif
// #ifdef MP-WEIXIN
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(previewUrl)}`
});
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(previewUrl);
// #endif
};
//
const generateRandomString = (length) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
@ -216,45 +236,44 @@ const generateRandomString = (length) => {
//
const sendSurvey = async (survey) => {
if (loading.value) return;
try {
loading.value = true;
//
const doctorInfo = accountStore.doctorInfo;
userId.value = doctorInfo?.userid || accountStore.openid;
// ID
const sendSurveyId = generateRandomString(10);
//
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
//
// 使
uni.$emit('sendSurvey', {
uni.$emit("sendSurvey", {
survey: survey,
corpId: corpId,
userId: userId.value,
sendSurveyId: sendSurveyId
sendSurveyId: sendSurveyId,
});
uni.showToast({
title: '已选择问卷',
icon: 'success'
title: "已选择问卷",
icon: "success",
});
//
setTimeout(() => {
uni.navigateBack();
}, 500);
} catch (error) {
console.error('发送问卷失败:', error);
console.error("发送问卷失败:", error);
uni.showToast({
title: error.message || '发送失败',
icon: 'none'
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
@ -315,20 +334,21 @@ onMounted(() => {
.category-scroll {
height: 100%;
}
.category-item {
padding: 32rpx 24rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
position: relative;
}
.category-item.active {
background-color: #fff;
color: #1890ff;
color: #0877f1;
font-weight: bold;
border-left: 4rpx solid #1890ff;
border-left: 4rpx solid #0877f1;
}
.survey-list {
@ -393,7 +413,6 @@ onMounted(() => {
}
.send-btn {
padding: 8rpx 32rpx;
font-size: 26rpx;
}

24
pages/webview/webview.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<view class="webview-container">
<web-view :src="url"></web-view>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
const url = ref("");
onLoad((options) => {
if (options.url) {
url.value = decodeURIComponent(options.url);
}
});
</script>
<style scoped>
.webview-container {
width: 100%;
height: 100vh;
}
</style>

View File

@ -72,7 +72,9 @@ const { getDoctorInfo } = useAccountStore();
const job = { assistant: '医生助理', doctor: '医生' };
const form = ref({});
const inviteTeamId = ref('')
const type = ref('');
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile }));
const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写');
const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存');
@ -189,6 +191,9 @@ async function save() {
if (doctorInfo.value) {
data.id = doctorInfo.value._id;
}
if (inviteTeamId.value) {
data.inviteTeamId = inviteTeamId.value;
}
const res = await api(apiName, data);
if (res && res.success) {
await getDoctorInfo()
@ -206,6 +211,9 @@ async function save() {
useLoad(opts => {
type.value = opts?.type;
if (type.value === 'joinTeam' && opts.teamId) {
inviteTeamId.value = opts.teamId
}
})
useShow(() => {

View File

@ -0,0 +1,25 @@
<template>
<view class="flex flex-col justify-center h-full bg-white">
<view>
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
柚康企微客服
</view>
<view class="flex justify-center overflow-hidden">
<uqrcode :canvas-id="`qrcode-${idx}`" value="https://uqrcode.cn/doc" :options="options"></uqrcode>
</view>
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
扫码或长按添加柚康企微客服
</view>
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
我们将为您提供软件使用咨询服务并支持补充病历宣教问卷回访等多种工作模板
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
const options = { margin: 10 }
</script>
<style></style>

View File

@ -0,0 +1,207 @@
<template>
<full-page v-if="team" pageClass="bg-white">
<view class="p-15">
<view class="flex items-center h-30 mb-10">
<view class="mr-5 text-dark text-lg font-semibold truncate">{{ team.name }}</view>
<view v-if="isLeader" class="edit-sub flex-shrink-0 flex items-center justify-center rounded-full bg-primary"
@click="toEdit()">
<image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" />
</view>
</view>
<view class="text-base text-dark leading-normal break-all" :class="expand ? '' : 'line-clamp-3'"
@click="expand = !expand">
{{ team.teamTroduce || '暂无简介' }}
</view>
<view class="mt-12 py-12 flex items-center justify-between">
<view class="text-lg text-dark">团队成员</view>
<view class="flex items-center text-primary text-base" @click="invite()">
邀请成员
</view>
</view>
<view v-for="i in teammates" :key="i._id" class="mb-10 p-10 border rounded flex">
<image class="flex-shrink-0 mr-10 avatar" :src="i.avatar || '/static/default-avatar.png'" />
<view class="w-0 flex-grow">
<view class="flex items-center">
<view class="mr-5 text-lg font-semibold text-dark truncate">{{ i.anotherName }}</view>
<view v-if="i.isCreator"
class="mr-5 px-10 flex-shrink-0 border-auto text-sm leading-normal text-primary rounded-full">
创建人
</view>
<view v-if="i.isLeader"
class="px-10 flex-shrink-0 border-auto text-sm leading-normal text-primary rounded-full">
团队负责人
</view>
<view class="flex-grow"></view>
<view v-if="isLeader && doctorInfo && i.userid !== doctorInfo.userid && !i.isCreator" class="px-5"
@click="showActions(i)">
<uni-icons type="more-filled" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="text-base text-dark leading-normal break-all line-clamp-2 mt-5">
{{ i.memberTroduce || '暂无简介' }}
</view>
</view>
</view>
<view class="safe-bottom-padding"></view>
</view>
<template #footer>
<button-footer v-if="canQuit" cancelText="退出当前团队" :showConfirm="false" @cancel="quit()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from '@/utils/api';
import { confirm, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
const { useLoad, useShow } = useGuard();
const { doctorInfo, account } = storeToRefs(useAccountStore());
const teamId = ref('');
const team = ref(null);
const expand = ref(false);
const canQuit = computed(() => team.value && doctorInfo.value && team.value.creator !== doctorInfo.value.userid);
const isLeader = computed(() => {
const userid = doctorInfo.value?.userid;
const member = teammates.value.find(i => i.userid === userid);
return member?.isLeader || member?.isCreator;
})
const memberList = computed(() => team.value && Array.isArray(team.value.memberList) ? team.value.memberList : [])
const teammates = computed(() => {
const memberLeaderList = team.value && Array.isArray(team.value.memberLeaderList) ? team.value.memberLeaderList : [];
const res = memberList.value.reduce((data, item) => {
if (item.userid === doctorInfo.value.userid) {
data.creator.push(item)
} else if (memberLeaderList.includes(item.userid)) {
data.leaders.push(item)
} else {
data.members.push(item)
}
return data
}, { creator: [], leaders: [], members: [] });
return [
...res.creator.map(i => ({ ...i, isCreator: true, isLeader: true })),
...res.leaders.map(i => ({ ...i, isLeader: true })),
...res.members
]
})
function invite() {
uni.navigateTo({
url: `/pages/work/team/invite/invite-teammate?teamId=${teamId.value}`
})
}
function showActions(i) {
uni.showActionSheet({
itemList: [i.isLeader ? '取消负责人' : '设为负责人', '移出团队'],
success: (res) => {
if (res.tapIndex === 0) {
toggleLeaderRole(i.isLeader ? 'cancel' : 'set', i.userid)
} else {
removeTeammate(i.userid);
}
}
})
}
function toEdit() {
uni.navigateTo({
url: `/pages/work/team/edit/team-edit?teamId=${teamId.value}`
})
}
async function getTeam() {
const res = await api('getTeamData', { teamId: teamId.value, corpId: account.value.corpId });
if (res && res.data) {
team.value = res.data;
} else {
toast(res?.message || '获取团队信息失败')
}
}
async function quit() {
await confirm('确定退出当前团队吗?');
removeTeammate(doctorInfo.value.userid);
}
async function toggleLeaderRole(toggleType, mateId) {
await confirm(toggleType === 'set' ? '确定设为负责人吗?' : '确定取消负责人吗?');
const data = {
corpId: account.value.corpId,
teamId: teamId.value,
mateId,
operatorId: doctorInfo.value.userid,
toggleType
}
const res = await api('toggleTeamLeaderRole', data);
if (res && res.success) {
await toast('操作成功');
getTeam();
} else {
toast(res?.message || '操作失败')
}
}
async function removeTeammate(mateId) {
if (mateId !== doctorInfo.value.userid) {
await confirm('确定移出该成员吗?');
}
const data = {
corpId: account.value.corpId,
teamId: teamId.value,
operatorId: doctorInfo.value.userid,
mateId,
}
const res = await api('removeTeammate', data);
if (res && res.success) {
await toast('操作成功');
getTeam();
} else {
toast(res?.message || '操作失败')
}
}
useLoad(options => {
teamId.value = options.teamId;
})
useShow(() => {
getTeam()
});
</script>
<style>
.min-w-100 {
min-width: 200rpx;
}
.avatar {
width: 120rpx;
height: 128rpx;
}
.edit-sub {
width: 36rpx;
height: 36rpx;
}
.edit-icon {
width: 16rpx;
height: 16rpx;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<full-page pageClass="bg-white">
<view class="pt-40 p-15">
<view class="text-base font-semibold text-dark">团队名称:</view>
<view class="mt-12 p-10 border rounded-sm">
<input v-model="team.name" class="w-full text-base" placeholder="请输入团队名称" />
</view>
<view class="mt-15 text-base font-semibold text-dark">团队名称:</view>
<view class="mt-12 p-10 border rounded-sm">
<textarea v-model="team.teamTroduce" class="w-full text-base leading-normal" placeholder-class="text-base"
placeholder="请输入团队介绍" />
</view>
</view>
<template #footer>
<button-footer confirmText="保存" :showCancel="false" @confirm="save()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
const { useLoad } = useGuard();
const { doctorInfo, account } = storeToRefs(useAccountStore());
const team = ref({ name: '', teamTroduce: '' });
function save() {
if (team.value.name.trim() === '') {
return toast('请输入团队名称')
}
team.value.teamId ? updateTeam() : createTeam();
}
async function createTeam() {
const data = {
corpId: account.value.corpId,
id: doctorInfo.value._id,
userid: doctorInfo.value.userid,
teamName: team.value.name,
teamTroduce: team.value.teamTroduce
}
const res = await api('createOwnTeam', data);
if (res && res.success) {
await toast('保存成功');
uni.navigateBack();
} else {
toast(res.message || '保存失败')
}
}
async function getTeamDetail() {
const res = await api('getTeamData', { teamId: team.value.teamId, corpId: account.value.corpId });
if (res && res.success) {
team.value.name = res.data.name;
team.value.teamTroduce = res.data.teamTroduce;
team.value._id = res.data._id;
}else {
await toast(res.message || '获取团队信息失败')
uni.navigateBack();
}
}
async function updateTeam() {
const data = {
corpId: account.value.corpId,
id: team.value._id,
teamId: team.value.teamId,
name: team.value.name,
teamTroduce: team.value.teamTroduce
}
const res = await api('updateTeamInfo', data);
if (res && res.success) {
await toast('保存成功');
uni.navigateBack();
} else {
toast(res.message || '保存失败')
}
}
onLoad(opts => {
if (opts.teamId) {
team.value.teamId = opts.teamId;
}
uni.setNavigationBarTitle({ title: opts.teamId ? '修改团队信息' : '创建团队' })
})
useLoad(opts => {
if (opts.teamId) {
getTeamDetail();
}
})
</script>
<style>
.pt-40 {
padding-top: 80rpx;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<view class="flex flex-col justify-center h-full bg-white">
<view v-if="list.length === 0" class="w-full">
<empty-data text="暂无团队" />
<view class="mt-10 mx-auto w-100 text-base text-primary border-auto leading-normal py-5 text-center rounded">
创建团队
</view>
</view>
<view v-else>
<view class="flex items-center">
<view class="flex-shrink-0 p-15 flex items-center" :class="list.length > 1 ? '' : 'opacity-0'"
@click="toggle(-1)">
<uni-icons type="left" size="32" :color="indicator.prev ? '#0074ff' : '#999'"></uni-icons>
</view>
<view class="w-0 flex-grow text-white overflow-hidden">
<view v-if="list.length > 1" class="py-5 text-center text-lg font-semibold text-dark">
({{ current + 1 }} / {{ list.length }})
</view>
<swiper class="swiper" :current="current">
<swiper-item v-for="(i, idx) in list" :key="i.teamId">
<view class="flex items-center justify-center h-30 mb-10">
<view class="mr-5 text-dark text-lg font-semibold truncate">{{ i.name }}</view>
<view class="edit-sub flex-shrink-0 flex items-center justify-center rounded-full bg-primary"
@click="visible = true">
<image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" />
</view>
</view>
<view v-if="i.qrcode" class="flex justify-center overflow-hidden">
<uqrcode ref="qrcodes" :canvas-id="`qrcode-${idx}`" :value="i.qrcode" :options="options">
</uqrcode>
</view>
</swiper-item>
</swiper>
</view>
<view class="flex-shrink-0 p-15 flex items-center" :class="list.length > 1 ? '' : 'opacity-0'"
@click="toggle(1)">
<uni-icons type="right" size="32" :color="indicator.next ? '#0074ff' : '#999'"></uni-icons>
</view>
</view>
<view class="mt-10 px-15 text-base text-gray leading-normal text-center">
微信扫一扫上面的二维码
</view>
<view class="px-15 text-base text-gray leading-normal text-center">进入团队首页即可发起线上咨询建档授权等服务</view>
<view class="mt-10 flex px-15">
<view class="mr-10 border-auto rounded py-10 text-base text-primary text-center flex-grow">保存图片</view>
<view class="bg-primary rounded py-10 text-base text-white text-center flex-grow">分享微信</view>
</view>
</view>
</view>
<rename-popup :team="team" :visible="visible" @close="visible = false" @change="change" />
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account.js";
import useGuard from '@/hooks/useGuard';
import api from "@/utils/api.js";
import { toast } from "@/utils/widget";
import emptyData from "@/components/empty-data.vue";
import renamePopup from "./rename-popup.vue";
const options = { margin: 10 }
const { useShow } = useGuard();
const { doctorInfo, account } = storeToRefs(useAccountStore());
const current = ref(0);
const list = ref([]);
const visible = ref(false);
const teamId = ref('')
const indicator = computed(() => ({
prev: current.value > 0,
next: current.value < list.value.length - 1
}))
const team = computed(() => list.value[current.value] || null);
function toggle(val) {
const num = current.value + val;
if (num > 0) {
current.value = Math.min(num, list.value.length - 1);
} else {
current.value = Math.max(num, 0);
}
}
async function getTeams() {
const res = await api('getJoinedTeams', { corpId: account.value?.corpId, mateId: doctorInfo.value?.userid });
const arr = res && Array.isArray(res.data) ? res.data.map(i => ({
id: i._id,
teamId: i.teamId,
name: i.name,
qrcode: i.qrcodes && i.qrcodes[0] && i.qrcodes[0].qrcode ? i.qrcodes[0].qrcode : ''
})) : [];
if (teamId.value) {
const idx = arr.findIndex(i => i.teamId === teamId.value);
if (idx > -1) {
current.value = idx
}
teamId.value = '';
}
list.value = arr;
}
async function change(name) {
const res = await api('updateTeamInfo', { teamId: team.value.teamId, name, corpId: account.value.corpId, id: team.value.id });
if (res && res.success) {
await toast('保存成功');
list.value[current.value].name = name;
visible.value = false
} else {
toast('保存失败');
}
}
onLoad(opts => {
teamId.value = opts.teamId || '';
})
useShow(() => {
getTeams()
})
</script>
<style>
.w-100 {
width: 200rpx;
}
.opacity-0 {
opacity: 0;
}
.swiper {
height: 500rpx;
}
.edit-sub {
width: 36rpx;
height: 36rpx;
}
.edit-icon {
width: 16rpx;
height: 16rpx;
}
.h-30 {
height: 60rpx;
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<view v-if="team" class="flex flex-col justify-center h-full bg-white">
<view>
<view class="text-dark text-lg font-semibold text-center mb-10">
{{ team.name }}
</view>
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
成员邀请码
</view>
<view class="flex justify-center overflow-hidden">
<uqrcode canvas-id="qrcode" :value="qrcode" :options="options"></uqrcode>
</view>
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
微信扫一扫上面的二维码
</view>
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
加入我的团队协同开展患者管理服务
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from '@/utils/api';
import { toast } from "@/utils/widget";
const env = __VITE_ENV__;
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
const options = { margin: 10 };
const team = ref(null);
const teamId = ref('');
const { useLoad, useShow } = useGuard();
const { account } = storeToRefs(useAccountStore());
const qrcode = computed(() => `${inviteQrcode}?type=inviteTeam&teamId=${teamId.value}`)
async function getTeam() {
const res = await api('getTeamData', { teamId: teamId.value, corpId: account.value.corpId });
if (res && res.data) {
team.value = res.data;
} else {
toast(res?.message || '获取团队信息失败')
}
}
useLoad(options => {
teamId.value = options.teamId;
})
useShow(() => {
getTeam()
});
</script>
<style></style>

View File

@ -0,0 +1,62 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark">
修改患者邀请码名称
</view>
<view class="mt-10 px-15">
<view class="p-10 border rounded-sm">
<input v-model="name" class="w-full" placeholder="请输入名称" />
</view>
</view>
<button-footer hideden-shadow confirmText="保存" @confirm="confirm()" @cancel="close()" />
</view>
</uni-popup>
</template>
<script setup>
import { ref, watch } from 'vue';
import { toast } from '@/utils/widget';
import ButtonFooter from '@/components/button-footer.vue';
const emits = defineEmits(['close', 'change'])
const props = defineProps({
team: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
})
const popup = ref();
const name = ref('');
function close() {
emits('close')
}
async function confirm() {
if (name.value.trim() === '') {
return toast('请输入名称')
}
emits('change', name.value.trim())
}
watch(() => props.visible, n => {
if (n) {
name.value = props.team && typeof props.team.name === 'string' ? props.team.name.trim() : '';
popup.value && popup.value.open()
} else {
popup.value && popup.value.close()
}
})
</script>
<style lang="scss" scoped>
.pb-50 {
padding-bottom: 100rpx;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<full-page :customScroll="list.length === 0">
<view v-if="list.length === 0" class="w-full h-full flex flex-col justify-center items-center">
<empty-data text="暂无团队" />
</view>
<view v-for="(i, idx) in list" :key="idx" class="p-15 mb-10 flex items-center bg-white shadow-lg">
<view class="mr-10 w-0 flex-grow" @click="toDetail(i)">
<view class="flex items-center">
<view class="team-name text-lg font-semibold truncate mr-5">
{{ i.name }}
</view>
<view v-if="doctorInfo && i.creator === doctorInfo.userid"
class="px-10 leading-normal text-sm border-auto text-primary rounded-full">创建</view>
<view v-else class="px-10 leading-normal text-sm border-auto text-warning rounded-full">加入</view>
</view>
<view class="mt-10 flex">
<view class="min-w-120 text-base text-dark">
成员: {{ i.memberList && i.memberList.length ? i.memberList.length : 0 }}
</view>
<view class="min-w-120 text-base text-dark">
患者: 200
</view>
</view>
</view>
<view class="flex-shrink-0 flex flex-col items-center justify-center" @click.stop="invite(i)">
<image class="mb-5 qrcode" src="/static/work/qrcode.svg" />
<view class="w-full text-sm text-dark text-center">邀请成员</view>
</view>
</view>
<template #footer>
<button-footer confirmText="创建团队" :showCancel="false" @confirm="toCreate()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import buttonFooter from '@/components/button-footer.vue';
import emptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue';
const { useShow } = useGuard();
const { doctorInfo, account } = storeToRefs(useAccountStore());
const list = ref([]);
function invite(team) {
uni.navigateTo({ url: `/pages/work/team/invite/invite-teammate?teamId=${team.teamId || ''}` })
// uni.navigateTo({ url: `/pages/work/team/invite/invite-patient?teamId=${team.teamId || ''}` })
}
function toCreate() {
uni.navigateTo({ url: '/pages/work/team/edit/team-edit' })
}
function toDetail(team) {
uni.navigateTo({ url: `/pages/work/team/detail/team-detail?teamId=${team.teamId || ''}` })
}
async function getTeams() {
const res = await api('getJoinedTeams', { corpId: account.value?.corpId, mateId: doctorInfo.value?.userid });
const arr = res && Array.isArray(res.data) ? res.data.map(i => ({
id: i._id,
teamId: i.teamId,
name: i.name,
memberList: i.memberList,
creator: i.creator
})) : [];
list.value = arr;
}
useShow(() => {
getTeams();
})
</script>
<style>
.min-w-120 {
min-width: 240rpx;
}
.team-name {
max-width: 60%;
}
.qrcode {
width: 60rpx;
height: 60rpx;
}
</style>

View File

@ -2,9 +2,9 @@
<full-page :showSafeArea="false" :customScroll="true">
<template #header>
<view class="user-header bg-white px-15 py-15">
<view class="flex items-center justify-between" @click="editProfile()">
<view class="flex items-center justify-between">
<view class="flex items-center flex-grow">
<view class="relative user-avatar mr-10">
<view class="relative user-avatar mr-10" @click="editProfile()">
<image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
:src="doctorInfo.avatar" mode="aspectFill" />
<image v-else class="avatar-img rounded-full overflow-hidden" src="/static/default-avatar.png"
@ -17,7 +17,7 @@
<text v-if="doctorInfo && doctorInfo.anotherName" class="user-name text-dark text-lg font-semibold">
{{ doctorInfo.anotherName }}
</text>
<text v-else class="user-name text-black text-lg font-semibold">请完善信息</text>
<text v-else class="user-name text-black text-lg font-semibold" @click="editProfile()">请完善信息</text>
<view class="flex items-center mt-5">
<view v-if="!doctorInfo || !doctorInfo.anotherName" class="status-tag tag-orange mr-10">
<text class="tag-text text-white">信息待完善</text>
@ -32,7 +32,7 @@
<!-- 右侧操作按钮 -->
<view class="flex items-center">
<view class="action-btn flex-col items-center mr-10" @click="handleInvitePatient">
<view class="action-btn flex-col items-center mr-10" @click="invitePatient()">
<image class="mb-5 qrcode-icon" src="/static/work/qrcode.svg" />
<text class="action-text text-dark text-sm">邀请</text>
</view>
@ -43,14 +43,55 @@
</view>
</view>
</view>
<view class="mt-15 px-15 py-12 text-dark text-lg font-semibold bg-white border-b">
待办列表11
<view class="mt-15 px-15 py-12 flex items-center justify-between bg-white">
<view class="text-dark text-lg font-semibold">待办列表</view>
<view class="flex text-base rounded-full bg-gray">
<view class="py-5 px-15 rounded-full bg-primary text-white">个人</view>
<view class="py-5 px-15">团队</view>
</view>
</view>
<view class="py-10 px-15 flex items-center">
<view class="flex-shrink-0 text-sm mr-10">
<text class="text-dark"></text>
<text class="text-danger">23</text>
<text class="text-dark"></text>
</view>
<view class="flex">
<view v-for="i in statusList" :key="i.value" class="mr-5 py-5 px-10 bg-white text-sm rounded-sm"
:class="current == i.value ? 'text-primary' : 'text-dark'">
{{ i.label }}
</view>
</view>
<view class="flex-shrink-0 flex-grow flex justify-end" @click="filtered = !filtered">
<image class="icon-filter" :src="`/static/work/icon-filter${filtered ? 'ed' : ''}.svg`" />
</view>
</view>
</template>
<scroll-view v-if="list.length" scroll-y="true" class="h-full bg-white">
<view class="p-15">
<view v-for="i in 10" class="p-15 bg-primary mb-10"></view>
<scroll-view v-if="list.length" scroll-y="true" class="h-full">
<view v-for="i in 10" :key="i" class="mb-10 shadow-lg bg-white">
<view class="flex items-center justify-between px-15 py-10 border-b">
<view class="text-base text-dark">计划执行: 2025-10-22</view>
<view class="flex items-center">
<view class="text-base text-dark">患者: 李珊珊</view>
</view>
</view>
<view class="py-10 px-15 flex items-center">
<view class="mr-5 text-lg font-semibold">患者满意度调查</view>
<view class="bg-opacity px-10 py-3 leading-normal text-base text-success rounded overflow-hidden">
待处理
</view>
</view>
<view class="px-15 text-base leading-normal text-gray">对于门诊就诊患者的满意度做统计以便优化</view>
<view class="mt-10 px-15 flex items-center">
<view class="mr-5 w-0 flex-grow truncate text-base leading-normal text-dark">
发送内容XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</view>
<view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view>
</view>
<view class="mt-10 px-15 text-base leading-normal text-gray">张敏西张敏希服务团队</view>
<view class="px-15 pb-10 text-base leading-normal text-gray">创建2026-01-08 张敏西</view>
</view>
</scroll-view>
@ -68,6 +109,7 @@
import { computed, ref } from 'vue';
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useInfoCheck from '@/hooks/useInfoCheck';
import useAccountStore from "@/store/account.js";
import certPopup from "./components/cert-popup.vue";
@ -81,12 +123,16 @@ const certConfig = {
verifying: { text: '认证中', classnames: 'bg-warning text-white' },
unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
};
const statusList = [{ label: '全部', value: 'all' }, { label: '待处理', value: 'pending' }, { label: '已处理', value: 'processed' }]
const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const { doctorInfo } = storeToRefs(useAccountStore());
const { withInfo } = useInfoCheck();
const list = ref([1]);
const visible = ref(false);
const current = ref('all');
const filtered = ref(false)
const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null)
@ -99,22 +145,17 @@ const handleVerify = () => {
};
//
const handleInvitePatient = () => {
uni.showToast({
title: "邀请患者",
icon: "none",
});
};
const invitePatient = withInfo(() => uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' }));
//
const handleMore = () => {
const handleMore = withInfo(() => {
uni.showActionSheet({
itemList: ["设置", "关于"],
itemList: ["我的团队", "联系客服"], //, ""
success: (res) => {
console.log("选择了第" + (res.tapIndex + 1) + "个按钮");
const url = res.tapIndex === 0 ? '/pages/work/team/list/team-list' : '/pages/work/service/contact-service';
uni.navigateTo({ url });
},
});
};
})
function editProfile() {
uni.navigateTo({
@ -235,12 +276,28 @@ useShow(() => {
}
}
// .empty-state {
// min-height: 600rpx;
// padding: 100rpx 0;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
// width: 100%;
// }</style>
.icon-filter {
width: 42rpx;
height: 42rpx;
}
.bg-opacity {
position: relative;
}
.bg-opacity::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.2;
background: currentColor;
}
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
}
</style>

View File

@ -3,13 +3,37 @@ export default [
path: 'pages/login/login',
meta: { title: '登录', login: false },
},
{
path: 'pages/login/redirect-page',
meta: { title: '登录', login: true },
},
{
path: 'pages/login/redirect-page',
meta: { title: '登录', login: false },
},
{
path: 'pages/message/message',
meta: { title: '消息' }
meta: { title: '消息', login: false }
},
{
path: 'pages/message/common-phrases',
meta: { title: '常用语', login: false }
},
{
path: 'pages/message/article-list',
meta: { title: '宣教文章', login: false }
},
{
path: 'pages/message/article-detail',
meta: { title: '宣教文章', login: false }
},
{
path: 'pages/message/survey-list',
meta: { title: '问卷列表', login: false }
},
{
path: 'pages/webview/webview',
meta: { title: '预览', login: false }
},
{
path: 'pages/message/index',
@ -119,7 +143,7 @@ export default [
},
{
path: 'pages/work/profile',
meta: { title: '完善个人信息' }
meta: { title: '完善个人信息', login: true }
},
{
path: 'pages/work/department-select',
@ -134,7 +158,27 @@ export default [
meta: { title: '上传证照', login: true }
},
{
path: 'pages/login/login',
meta: { title: '授权登录' }
path: 'pages/work/team/invite/invite-patient',
meta: { title: '邀请患者', login: true }
},
{
path: 'pages/work/team/invite/invite-teammate',
meta: { title: '邀请成员', login: true }
},
{
path: 'pages/work/team/list/team-list',
meta: { title: '我的团队', login: true }
},
{
path: 'pages/work/team/edit/team-edit',
meta: { title: '修改团队信息', login: true }
},
{
path: 'pages/work/team/detail/team-detail',
meta: { title: '团队信息', login: true }
},
{
path: 'pages/work/service/contact-service',
meta: { title: '联系企微客服' }
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/icon/wenjuan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1003 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769601768503" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12084" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M265.984 96c-34.752 0-61.44-0.192-84.992 3.008-23.552 3.2-46.592 11.328-62.72 29.12-16.192 17.792-21.888 41.664-22.272 65.152-0.384 23.424 3.2 49.216 7.488 82.752 2.88 22.464 11.264 43.904 26.752 62.336 41.664 49.536 119.168 139.136 228.48 220.864a13.44 13.44 0 0 1 4.416 9.152c11.968 146.56 23.04 246.72 28.864 296.256 2.752 23.488 16.192 43.072 35.648 54.4 19.392 11.264 48.192 12.288 69.824-3.008 16.768-11.84 45.76-27.136 73.024-46.912 27.264-19.712 55.68-45.504 63.104-84.736v-0.128c6.4-33.728 16.192-98.368 26.752-215.488a14.016 14.016 0 0 1 4.48-9.28c109.568-81.792 187.2-171.52 228.928-221.12 15.488-18.432 23.872-39.872 26.752-62.336 4.288-33.536 7.872-59.328 7.488-82.752-0.384-23.488-6.208-47.36-22.4-65.152-16.128-17.728-39.04-25.92-62.592-29.12-23.552-3.2-50.24-3.008-84.992-3.008H265.984z m0 64h492.032c34.752 0 60.224 0.192 76.352 2.368 16.128 2.24 20.48 4.864 24 8.768 3.52 3.84 5.376 7.616 5.632 23.104 0.256 15.488-2.688 40.128-7.04 73.6-2.112 16.64-2.176 17.472-12.16 29.44-41.088 48.64-115.072 133.888-218.24 210.944a76.16 76.16 0 0 0-29.952 54.656c-10.432 115.84-20.096 178.752-25.856 209.6-2.432 12.8-15.744 28.8-37.76 44.8-22.016 15.936-49.664 30.336-72.512 46.464-1.92 1.344 0.832 0.768-0.704-0.128a11.264 11.264 0 0 1-4.16-6.464c-5.76-48.832-16.64-148.16-28.608-293.888a76.16 76.16 0 0 0-29.888-55.232c-103.04-77.056-176.896-162.112-217.856-210.752-10.048-11.968-10.112-12.8-12.288-29.44-4.288-33.472-7.232-58.112-6.976-73.6 0.256-15.488 2.112-19.2 5.632-23.104 3.52-3.84 7.872-6.528 24-8.768 16.128-2.176 41.6-2.368 76.352-2.368z" fill="#333333" p-id="12085"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769601747862" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11031" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m245.76 0l532.48 0q245.76 0 245.76 245.76l0 532.48q0 245.76-245.76 245.76l-532.48 0q-245.76 0-245.76-245.76l0-532.48q0-245.76 245.76-245.76Z" fill="#2975FF" p-id="11032"></path><path d="M758.02624 547.1744h-120.6272c-11.71456 0-21.21728-10.12736-21.25824-22.6304 0-12.51328 9.59488-22.6816 21.18656-22.6816h120.61696c8.59136 0 16.3328 5.51936 19.6608 13.9776a23.9616 23.9616 0 0 1-4.44416 24.7808c-4.00384 4.3008-9.472 6.656-15.13472 6.5536z m-0.2048 81.5616H636.23168c-11.28448-0.72704-20.09088-10.6496-20.09088-22.66112 0-12.00128 8.8064-21.92384 20.09088-22.65088h121.50784c8.66304-0.04096 16.4864 5.45792 19.84512 13.9264a23.76704 23.76704 0 0 1-4.5056 24.79104 20.3776 20.3776 0 0 1-15.2576 6.5536v0.04096z m0.21504 72.4992h-120.6272c-11.71456 0-21.22752-10.1376-21.26848-22.66112 0-12.43136 9.59488-22.65088 21.18656-22.65088h120.6272c11.70432 0 21.2992 10.21952 21.2992 22.65088a23.27552 23.27552 0 0 1-6.144 16.09728c-3.9936 4.27008-9.43104 6.62528-15.07328 6.5536z" fill="#FFFFFF" p-id="11033"></path><path d="M671.63136 239.88224l-1.18784-0.01024h-401.7152a60.4672 60.4672 0 0 0-51.70176 31.31392l-0.4096 0.82944c-11.776 21.56544-11.78624 49.28512 8.72448 70.4512l2.29376 2.36544 149.4528 119.18336v212.15232l0.06144 2.51904c0.78848 20.1216 11.008 39.59808 30.23872 50.51392l66.46784 36.28032a62.13632 62.13632 0 0 0 56.80128 0l1.69984-0.8704 1.6384-1.00352 2.17088-1.39264a61.31712 61.31712 0 0 0 27.136-51.5584l-0.01024-245.71904 152.33024-122.368 3.9936-5.46816a61.1328 61.1328 0 0 0-47.98464-97.21856z m-1.18784 51.18976a9.97376 9.97376 0 0 1 8.57088 14.6432l-0.73728 1.16736L530.1248 425.90208a48.10752 48.10752 0 0 0-17.9712 35.34848l-0.06144 2.60096v247.3472a10.11712 10.11712 0 0 1-4.84352 8.74496c-2.74432 1.41312-5.9392 1.5872-8.8064 0.53248l-1.19808-0.53248-64.1536-35.03104c-2.90816-1.4848-4.44416-4.43392-4.75136-7.55712l-0.0512-1.18784v-213.6064c0-13.93664-6.05184-27.0336-16.4352-36.21888l-1.9968-1.67936-147.74272-117.8112c-2.92864-3.0208-2.3552-7.04512-0.57344-10.30144a9.25696 9.25696 0 0 1 7.424-5.4272l1.2288-0.0512h400.24064z" fill="#FFFFFF" p-id="11034"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -3,20 +3,29 @@ import { defineStore } from "pinia";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
import { initGlobalTIM, globalTimChatManager } from "@/utils/tim-chat.js";
import cache from '@/utils/cache';
const env = __VITE_ENV__;
// 缓存键名
const CACHE_KEYS = {
ACCOUNT: 'account',
OPENID: 'openid',
DOCTOR_INFO: 'doctorInfo'
};
export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const account = ref(null);
// 从缓存中恢复数据
const account = ref(cache.get(CACHE_KEYS.ACCOUNT, null));
const loading = ref(false);
const loginPromise = ref(null);
// IM 相关
const openid = ref("");
const openid = ref(cache.get(CACHE_KEYS.OPENID, ""));
const isIMInitialized = ref(false);
// 医生信息
const doctorInfo = ref(null);
const doctorInfo = ref(cache.get(CACHE_KEYS.DOCTOR_INFO, null));
function getLoginPromise(phoneCode = '') {
if (loginPromise.value) return loginPromise.value;
@ -51,17 +60,14 @@ export default defineStore("accountStore", () => {
}
account.value = res.data;
openid.value = res.data.openid;
// 持久化账户信息
cache.set(CACHE_KEYS.ACCOUNT, res.data);
cache.set(CACHE_KEYS.OPENID, res.data.openid);
// 登录成功后初始化腾讯IM
// try {
// console.log('开始初始化腾讯IMuserID:', res.data.openid);
// await initGlobalTIM(res.data.openid);
// isIMInitialized.value = true;
// console.log('腾讯IM初始化成功');
// } catch (imError) {
// console.error('腾讯IM初始化失败:', imError);
// // IM初始化失败不影响登录流程
// }
await getDoctorInfo(openid.value);
await initIMAfterLogin();
return res.data
}
}
@ -78,19 +84,24 @@ export default defineStore("accountStore", () => {
const res = await api('getCorpMemberData', {
weChatOpenId: account.value.openid,
});
if (res.success && res.data) {
doctorInfo.value = res.data;
doctorInfo.value = res?.data || null;
// 持久化医生信息
if (res?.data) {
cache.set(CACHE_KEYS.DOCTOR_INFO, res.data);
}
} catch (e) {
console.error('获取医生信息失败:', e);
}
}
async function initIMAfterLogin(userID) {
if (isIMInitialized.value) {
return true;
}
async function initIMAfterLogin() {
if (isIMInitialized.value) return true;
if (!doctorInfo.value) return;
try {
const userID = doctorInfo.value.userid;
if (!userID) await getDoctorInfo();
await initGlobalTIM(userID);
isIMInitialized.value = true;
return true;
@ -118,6 +129,11 @@ export default defineStore("accountStore", () => {
openid.value = "";
isIMInitialized.value = false;
doctorInfo.value = null;
// 清空缓存
cache.remove(CACHE_KEYS.ACCOUNT);
cache.remove(CACHE_KEYS.OPENID);
cache.remove(CACHE_KEYS.DOCTOR_INFO);
}
return { account, openid, isIMInitialized, doctorInfo, login, getDoctorInfo, initIMAfterLogin, logout }

31
store/team.js Normal file
View File

@ -0,0 +1,31 @@
import { ref } from "vue";
import { defineStore, storeToRefs } from "pinia";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
import useAccountStore from "./account";
export default defineStore("teamStore", () => {
const { account, doctorInfo } = storeToRefs(useAccountStore());
const teams = ref([]);
async function getTeam(teamId) {
if (!teamId || !account.value?.corpId) return;
const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
if (res && res.data) {
return res.data;
} else {
toast(res?.message || '获取团队信息失败')
}
}
async function getTeams() {
const corpId = account.value?.corpId;
const mateId = doctorInfo.value?.corpId;
if (!corpId || !mateId) return;
const res = await api('getJoinedTeams', { corpId, mateId });
teams.value = res && Array.isArray(res.data) ? res.data : [];
}
return { teams, getTeam, getTeams }
})

58
styles/theme.scss Normal file
View File

@ -0,0 +1,58 @@
/**
* 项目主题色配置
* 统一管理项目中使用的主题色
*/
// 主题色
$primary-color: #0877F1;
$primary-light: #e8f3ff;
$primary-dark: #0660c9;
$primary-gradient-start: #1b5cc8;
$primary-gradient-end: #0877F1;
// 辅助色
$success-color: #4cd964;
$warning-color: #f0ad4e;
$error-color: #dd524d;
$info-color: #909399;
// 文字颜色
$text-color-primary: #333;
$text-color-regular: #666;
$text-color-secondary: #999;
$text-color-placeholder: #c0c4cc;
$text-color-white: #fff;
// 背景颜色
$bg-color-white: #ffffff;
$bg-color-page: #f5f5f5;
$bg-color-hover: #f1f1f1;
$bg-color-mask: rgba(0, 0, 0, 0.4);
// 边框颜色
$border-color-base: #e4e7ed;
$border-color-light: #ebeef5;
$border-color-lighter: #f2f6fc;
// 圆角
$border-radius-small: 4rpx;
$border-radius-base: 8rpx;
$border-radius-large: 16rpx;
$border-radius-round: 999rpx;
// 间距
$spacing-small: 16rpx;
$spacing-base: 24rpx;
$spacing-large: 32rpx;
// 字体大小
$font-size-small: 24rpx;
$font-size-base: 28rpx;
$font-size-medium: 32rpx;
$font-size-large: 36rpx;
$font-size-xlarge: 40rpx;
// 阴影
$box-shadow-light: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
$box-shadow-base: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
$box-shadow-dark: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);

View File

@ -15,7 +15,7 @@
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-primary: #0877F1;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,392 @@
# 介绍
`uQRCode`是一款基于`Javascript`环境开发的二维码生成插件,适用所有`Javascript`运行环境的前端应用和`Node.js`应用。
`uQRCode`可扩展性高,它支持自定义渲染二维码,可通过`uQRCode API`得到二维码绘制关键信息后,使用`canvas``svg``js`操作`dom`的方式绘制二维码图案。还可自定义二维码样式,如随机颜色、圆点、方块、块与块之间的间距等。
欢迎加入群聊【uQRCode交流群】[695070434](https://jq.qq.com/?_wv=1027&k=JRjzDqiw)。
# 设计器
uQRCode发布了配套的可视化设计器可根据自己喜好在设计器中设计二维码样式一键生成配置代码复制到项目中详情请在微信小程序搜索“柚子二维码”或扫描下方小程序码体验。
![uQRCode设计器](https://uqrcode.cn/mp_weixin_code.jpg)
## 设计器模板示例
![uQRCode设计器](https://uqrcode.cn/yz_1.png)
![uQRCode设计器](https://uqrcode.cn/yz_2.png)
![uQRCode设计器](https://uqrcode.cn/yz_3.png)
![uQRCode设计器](https://uqrcode.cn/yz_4.png)
![uQRCode设计器](https://uqrcode.cn/yz_5.png)
![uQRCode设计器](https://uqrcode.cn/yz_6.png)
![uQRCode设计器](https://uqrcode.cn/yz_7.png)
![uQRCode设计器](https://uqrcode.cn/yz_8.png)
![uQRCode设计器](https://uqrcode.cn/yz_9.png)
# 快速上手
> 在`uni-app`中,我们更推荐使用组件方式来生成二维码,组件方式大大提高了页面的可读性以及避开了一些平台容易出问题的地方,当组件无法满足需求的时候,再考虑切换成原生方式。
官方文档:[https://uqrcode.cn/doc](https://uqrcode.cn/doc)。
github地址[https://github.com/Sansnn/uQRCode](https://github.com/Sansnn/uQRCode)。
npm地址[https://www.npmjs.com/package/uqrcodejs](https://www.npmjs.com/package/uqrcodejs)。
uni-app插件市场地址[https://ext.dcloud.net.cn/plugin?id=1287](https://ext.dcloud.net.cn/plugin?id=1287)。
## 原生方式
原生方式仅需要获取`uqrcode.js`文件便可使用。详细配置请移步到:文档 > [原生](https://uqrcode.cn/doc/document/native.html)。
### 安装
1. 通过`npm`安装,成功后即可使用`import``require`进行引用。
``` bash
# npm安装
npm install uqrcodejs
# 或者
npm install @uqrcode/js
```
2. 通过项目开源地址获取`uqrcode.js`,下载`uqrcode.js`后,将其复制到您项目指定目录,在页面中引入`uqrcode.js`文件即可开始使用。
### 引入
- 通过`import`引入。
``` javascript
// npm安装
import UQRCode from 'uqrcodejs'; // npm install uqrcodejs
// 或者
import UQRCode from '@uqrcode/js'; // npm install @uqrcode/js
```
- `Node.js`通过`require`引入。
``` javascript
// npm安装
const UQRCode = require('uqrcodejs'); // npm install uqrcodejs
// 或者
const UQRCode = require('@uqrcode/js'); // npm install @uqrcode/js
```
- 原生浏览器环境在js脚本加载时添加到`window`
``` html
<script type="text/javascript" src="uqrcode.js"></script>
<script>
var UQRCode = window.UQRCode;
</script>
```
### 简单用法
`uQRCode`基于`Canvas API`封装了一套方法,建议开发者使用`canvas`生成,一键调用,非常方便。以下是示例:
- HTML示例
- DOM部分
``` html
<canvas id="qrcode" width="200" height="200"></canvas>
```
- JS部分
``` javascript
// 获取uQRCode实例
var qr = new UQRCode();
// 设置二维码内容
qr.data = "https://uqrcode.cn/doc";
// 设置二维码大小必须与canvas设置的宽高一致
qr.size = 200;
// 调用制作二维码方法
qr.make();
// 获取canvas元素
var canvas = document.getElementById("qrcode");
// 获取canvas上下文
var canvasContext = canvas.getContext("2d");
// 设置uQRCode实例的canvas上下文
qr.canvasContext = canvasContext;
// 调用绘制方法将二维码图案绘制到canvas上
qr.drawCanvas();
```
- uni-app示例
- Template部分
``` html
<canvas id="qrcode" canvas-id="qrcode" style="width: 200px;height: 200px;"></canvas>
```
- JS部分
``` javascript
onReady() {
// 获取uQRCode实例
var qr = new UQRCode();
// 设置二维码内容
qr.data = "https://uqrcode.cn/doc";
// 设置二维码大小必须与canvas设置的宽高一致
qr.size = 200;
// 调用制作二维码方法
qr.make();
// 获取canvas上下文
var canvasContext = uni.createCanvasContext('qrcode', this); // 如果是组件this必须传入
// 设置uQRCode实例的canvas上下文
qr.canvasContext = canvasContext;
// 调用绘制方法将二维码图案绘制到canvas上
qr.drawCanvas();
}
```
- 微信小程序推荐使用Canvas 2D关于Canvas 2D的使用请参考微信开放文档。
### 高级用法
考虑到部分平台可能不支持`canvas`,所以`uQRCode`并没有强制要求和`canvas`一起使用,您还可以选择其他方式来生成二维码,例如使用`js`操作`dom`进行绘制或是使用`svg`绘制等。以下是示例:
- uni-app v-for+view
```html
<template>
<view>
<view class="qrcode">
<view v-for="(row, rowI) in modules" :key="rowI" style="display: flex;flex-direction: row;">
<view v-for="(col, colI) in row" :key="colI">
<view v-if="col.isBlack" style="width: 10px;height: 10px;background-color: black;">
<!-- 黑色码点 -->
</view>
<view v-else style="width: 10px;height: 10px;background-color: white;">
<!-- 白色码点 -->
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import UQRCode from '../../uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js';
export default {
data() {
return {
modules: []
}
},
onLoad() {
const qr = new UQRCode();
qr.data = 'uQRCode';
qr.make();
this.modules = qr.modules;
},
methods: {
}
}
</script>
```
- js操作dom
``` html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>uQRCode二维码生成</title>
</head>
<body>
<div id="qrcode" style="position: relative;"></div>
<script type="text/javascript" src="uqrcode.js"></script>
<script>
// 引入uQRCode
var UQRCode = window.UQRCode;
// 获取uQRCode实例
var qr = new UQRCode();
// 设置二维码内容
qr.data = "https://uqrcode.cn/doc";
// 设置二维码大小必须与canvas设置的宽高一致
qr.size = 200;
// 设置二维码前景图,可以是路径
qr.foregroundImageSrc =
'data:image/png;base64,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';
// 调用制作二维码方法
qr.make();
var drawModules = qr.getDrawModules();
// 遍历drawModules创建dom元素
var qrHtml = '';
for (var i = 0; i < drawModules.length; i++) {
var drawModule = drawModules[i];
switch (drawModule.type) {
case 'tile':
/* 绘制小块 */
qrHtml += `<div style="position: absolute;left: ${drawModule.x}px;top: ${drawModule.y}px;width: ${drawModule.width}px;height: ${drawModule.height}px;background: ${drawModule.color};"></div>`;
break;
case 'image':
/* 绘制图像 */
qrHtml += `<img style="position: absolute;left: ${drawModule.x}px;top: ${drawModule.y}px;width: ${drawModule.width}px;height: ${drawModule.height}px;" src="${drawModule.imageSrc}" />`;
break;
}
}
document.getElementById('qrcode').innerHTML = qrHtml;
</script>
</body>
</html>
```
- svg
``` html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>uQRCode二维码生成</title>
</head>
<body>
<svg id="qrcode" width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1"></svg>
<script type="text/javascript" src="uqrcode.js"></script>
<script>
// 引入uQRCode
var UQRCode = window.UQRCode;
// 获取uQRCode实例
var qr = new UQRCode();
// 设置二维码内容
qr.data = "https://uqrcode.cn/doc";
// 设置二维码大小必须与canvas设置的宽高一致
qr.size = 200;
// 设置二维码前景图,可以是路径
qr.foregroundImageSrc =
'data:image/png;base64,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';
// 调用制作二维码方法
qr.make();
var drawModules = qr.getDrawModules();
// 遍历drawModules创建svg元素
var qrHtml = '';
for (var i = 0; i < drawModules.length; i++) {
var drawModule = drawModules[i];
switch (drawModule.type) {
case 'tile':
/* 绘制小块 */
qrHtml += `<rect x="${drawModule.x}" y="${drawModule.y}" width="${drawModule.width}" height="${drawModule.height}" style="fill: ${drawModule.color};" />`;
break;
case 'image':
/* 绘制图像 */
qrHtml += `<image href="${drawModule.imageSrc}" x="${drawModule.x}" y="${drawModule.y}" width="${drawModule.width}" height="${drawModule.height}" />`;
break;
}
}
document.getElementById('qrcode').innerHTML = qrHtml;
</script>
</body>
</html>
```
> 更多用法大家自行探索咯,期待分享哟~
### 导出临时文件路径
原生方式基于`Canvas`的,请自行参阅各平台`Canvas`的导出方式。以下是部分示例:
- uni-app
```javascript
// 通过uni.createCanvasContext方式创建绘制上下文的对应导出API为uni.canvasToTempFilePath
// 调用完ctx.draw()方法后不能第一时间导出,否则会异常,需要有一定的延时
setTimeout(() => {
uni.canvasToTempFilePath(
{
canvasId: this.canvasId,
fileType: this.fileType,
width: this.canvasWidth,
height: this.canvasHeight,
success: res => {
console.log(res);
},
fail: err => {
console.log(err);
}
},
// this // 组件内使用必传当前实例
);
}, 300);
```
- Canvas2D
```javascript
// 得到base64
console.log(canvas.toDataURL());
// 得到buffer
console.log(canvas.toBuffer());
```
### 保存二维码到本地相册
必须在导出临时文件路径成功后再执行保存。uni-app通用保存方式H5除外
```javascript
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: res => {
console.log(res);
},
fail: err => {
console.log(err);
}
});
```
H5可以通过设置`<a>`标签`href`属性的方式进行保存:
```javascript
const aEle = document.createElement('a');
aEle.download = 'uQRCode'; // 设置下载的文件名,默认是'下载'
aEle.href = tempFilePath;
document.body.appendChild(aEle);
aEle.click();
aEle.remove(); // 下载之后把创建的元素删除
```
经过测试PC端浏览器可以下载部分安卓自带或第三方浏览器可以下载安卓微信浏览器不适用移动端iOS所有浏览器均不适用差异较大还是推荐各位导出文件给图片组件显示然后提示用户通过长按图片进行保存这种方式。
## uni-app组件方式
### 安装
通过uni-app插件市场地址安装[https://ext.dcloud.net.cn/plugin?id=1287](https://ext.dcloud.net.cn/plugin?id=1287)。详细配置请移步到:文档 > [uni-app组件](https://uqrcode.cn/doc/document/uni-app.html)。
### 引入
uni-app默认为easycom模式可直接键入`<uqrcode>`标签。
### 简单用法
安装`uqrcode`组件后,在`template`中键入`<uqrcode/>`。设置`ref`属性可使用组件内部方法,`canvas-id`属性为组件内部的canvas组件标识`value`属性为二维码生成对应内容,`options`为配置选项可配置二维码样式绘制Logo等详见[options](https://uqrcode.cn/doc/document/uni-app.html#options) 。
``` html
<uqrcode ref="uqrcode" canvas-id="qrcode" value="https://uqrcode.cn/doc" :options="{ margin: 10 }"></uqrcode>
```
### 导出临时文件路径
为了保证方法调用成功,请在 [complete](https://uqrcode.cn/doc/document/uni-app.html#complete) 事件返回`success=true`后调用。
```javascript
// uqrcode为组件的ref名称
this.$refs.uqrcode.toTempFilePath({
success: res => {
console.log(res);
}
});
```
### 保存二维码到本地相册
为了保证方法调用成功,请在 [complete](https://uqrcode.cn/doc/document/uni-app.html#complete) 事件返回`success=true`后调用。
```javascript
// uqrcode为组件的ref名称
this.$refs.uqrcode.save({
success: () => {
uni.showToast({
icon: 'success',
title: '保存成功'
});
}
});
```
## 更多使用说明请前往官方文档查看:[https://uqrcode.cn/doc](https://uqrcode.cn/doc)。

View File

@ -0,0 +1,12 @@
## 4.0.62022-12-12
修复`getDrawModules`,第一次获取结果正常,后续获取`tile`模块不存在的问题;
修复安卓type:normal因Canvas API使用了小数或为0的参数导致生成异常的问题安卓非2d Canvas部分API参数不支持携带小数部分API参数必须大于0
## 4.0.12022-11-28
优化组件loading属性的表现
新增组件type选项normal以便于在某些条件编译初始为type=2d时还可以选择使用非2d组件类型
修复组件条件编译在其他编辑器语法提示报错;
修复原生对es5的支持。
## 4.0.02022-11-21
v4版本源代码全面开放开源地址[https://github.com/Sansnn/uQRCode](https://github.com/Sansnn/uQRCode)
升级说明v4为大版本更新虽然已尽可能兼容上一代版本但不可避免的还是存在一些细节差异若更新后出现问题请参考对照[v3 文档](https://uqrcode.cn/doc/v3)[v4 文档](https://uqrcode.cn/doc)进行修改。

View File

@ -0,0 +1 @@
export const cacheImageList = [];

View File

@ -0,0 +1,41 @@
function Queue() {
let waitingQueue = this.waitingQueue = [];
let isRunning = this.isRunning = false; // 记录是否有未完成的任务
function execute(task, resolve, reject) {
task()
.then((data) => {
resolve(data);
})
.catch((e) => {
reject(e);
})
.finally(() => {
// 等待任务队列中如果有任务则触发它否则设置isRunning = false,表示无任务状态
if (waitingQueue.length) {
const next = waitingQueue.shift();
execute(next.task, next.resolve, next.reject);
} else {
isRunning = false;
}
});
}
this.exec = function(task) {
return new Promise((resolve, reject) => {
if (isRunning) {
waitingQueue.push({
task,
resolve,
reject
});
} else {
isRunning = true;
execute(task, resolve, reject);
}
});
}
}
/* 队列实例某些平台一起使用多个组件时需要通过队列逐一绘制否则部分绘制方法异常nvue端的iOS gcanvas尤其明显在不通过队列绘制时会出现图片丢失的情况 */
export const queueDraw = new Queue();
export const queueLoadImage = new Queue();

View File

@ -0,0 +1,3 @@
declare module '*/common/cache' {
export const cacheImageList: Array;
}

View File

@ -0,0 +1,4 @@
declare module '*/common/queue' {
export const queueDraw: any;
export const queueLoadImage: any;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,241 @@
const isWeex = typeof WXEnvironment !== 'undefined';
const isWeexIOS = isWeex && /ios/i.test(WXEnvironment.platform);
const isWeexAndroid = isWeex && !isWeexIOS;
import GLmethod from '../context-webgl/GLmethod';
const GCanvasModule =
(typeof weex !== 'undefined' && weex.requireModule) ? (weex.requireModule('gcanvas')) :
(typeof __weex_require__ !== 'undefined') ? (__weex_require__('@weex-module/gcanvas')) : {};
let isDebugging = false;
let isComboDisabled = false;
const logCommand = (function () {
const methodQuery = [];
Object.keys(GLmethod).forEach(key => {
methodQuery[GLmethod[key]] = key;
})
const queryMethod = (id) => {
return methodQuery[parseInt(id)] || 'NotFoundMethod';
}
const logCommand = (id, cmds) => {
const mId = cmds.split(',')[0];
const mName = queryMethod(mId);
console.log(`=== callNative - componentId:${id}; method: ${mName}; cmds: ${cmds}`);
}
return logCommand;
})();
function joinArray(arr, sep) {
let res = '';
for (let i = 0; i < arr.length; i++) {
if (i !== 0) {
res += sep;
}
res += arr[i];
}
return res;
}
const commandsCache = {}
const GBridge = {
callEnable: (ref, configArray) => {
commandsCache[ref] = [];
return GCanvasModule.enable({
componentId: ref,
config: configArray
});
},
callEnableDebug: () => {
isDebugging = true;
},
callEnableDisableCombo: () => {
isComboDisabled = true;
},
callSetContextType: function (componentId, context_type) {
GCanvasModule.setContextType(context_type, componentId);
},
callReset: function(id){
GCanvasModule.resetComponent && canvasModule.resetComponent(componentId);
},
render: isWeexIOS ? function (componentId) {
return GCanvasModule.extendCallNative({
contextId: componentId,
type: 0x60000001
});
} : function (componentId) {
return callGCanvasLinkNative(componentId, 0x60000001, 'render');
},
render2d: isWeexIOS ? function (componentId, commands, callback) {
if (isDebugging) {
console.log('>>> >>> render2d ===');
console.log('>>> commands: ' + commands);
}
GCanvasModule.render([commands, callback?true:false], componentId, callback);
} : function (componentId, commands,callback) {
if (isDebugging) {
console.log('>>> >>> render2d ===');
console.log('>>> commands: ' + commands);
}
callGCanvasLinkNative(componentId, 0x20000001, commands);
if(callback){
callback();
}
},
callExtendCallNative: isWeexIOS ? function (componentId, cmdArgs) {
throw 'should not be here anymore ' + cmdArgs;
} : function (componentId, cmdArgs) {
throw 'should not be here anymore ' + cmdArgs;
},
flushNative: isWeexIOS ? function (componentId) {
const cmdArgs = joinArray(commandsCache[componentId], ';');
commandsCache[componentId] = [];
if (isDebugging) {
console.log('>>> >>> flush native ===');
console.log('>>> commands: ' + cmdArgs);
}
const result = GCanvasModule.extendCallNative({
"contextId": componentId,
"type": 0x60000000,
"args": cmdArgs
});
const res = result && result.result;
if (isDebugging) {
console.log('>>> result: ' + res);
}
return res;
} : function (componentId) {
const cmdArgs = joinArray(commandsCache[componentId], ';');
commandsCache[componentId] = [];
if (isDebugging) {
console.log('>>> >>> flush native ===');
console.log('>>> commands: ' + cmdArgs);
}
const result = callGCanvasLinkNative(componentId, 0x60000000, cmdArgs);
if (isDebugging) {
console.log('>>> result: ' + result);
}
return result;
},
callNative: function (componentId, cmdArgs, cache) {
if (isDebugging) {
logCommand(componentId, cmdArgs);
}
commandsCache[componentId].push(cmdArgs);
if (!cache || isComboDisabled) {
return GBridge.flushNative(componentId);
} else {
return undefined;
}
},
texImage2D(componentId, ...args) {
if (isWeexIOS) {
if (args.length === 6) {
const [target, level, internalformat, format, type, image] = args;
GBridge.callNative(
componentId,
GLmethod.texImage2D + ',' + 6 + ',' + target + ',' + level + ',' + internalformat + ',' + format + ',' + type + ',' + image.src
)
} else if (args.length === 9) {
const [target, level, internalformat, width, height, border, format, type, image] = args;
GBridge.callNative(
componentId,
GLmethod.texImage2D + ',' + 9 + ',' + target + ',' + level + ',' + internalformat + ',' + width + ',' + height + ',' + border + ',' +
+ format + ',' + type + ',' + (image ? image.src : 0)
)
}
} else if (isWeexAndroid) {
if (args.length === 6) {
const [target, level, internalformat, format, type, image] = args;
GCanvasModule.texImage2D(componentId, target, level, internalformat, format, type, image.src);
} else if (args.length === 9) {
const [target, level, internalformat, width, height, border, format, type, image] = args;
GCanvasModule.texImage2D(componentId, target, level, internalformat, width, height, border, format, type, (image ? image.src : 0));
}
}
},
texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image) {
if (isWeexIOS) {
if (arguments.length === 8) {
GBridge.callNative(
componentId,
GLmethod.texSubImage2D + ',' + 6 + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset, + ',' + format + ',' + type + ',' + image.src
)
}
} else if (isWeexAndroid) {
GCanvasModule.texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image.src);
}
},
bindImageTexture(componentId, src, imageId) {
GCanvasModule.bindImageTexture([src, imageId], componentId);
},
perloadImage([url, id], callback) {
GCanvasModule.preLoadImage([url, id], function (image) {
image.url = url;
image.id = id;
callback(image);
});
},
measureText(text, fontStyle, componentId) {
return GCanvasModule.measureText([text, fontStyle], componentId);
},
getImageData (componentId, x, y, w, h, callback) {
GCanvasModule.getImageData([x, y,w,h],componentId,callback);
},
putImageData (componentId, data, x, y, w, h, callback) {
GCanvasModule.putImageData([x, y,w,h,data],componentId,callback);
},
toTempFilePath(componentId, x, y, width, height, destWidth, destHeight, fileType, quality, callback){
GCanvasModule.toTempFilePath([x, y, width,height, destWidth, destHeight, fileType, quality], componentId, callback);
}
}
export default GBridge;

View File

@ -0,0 +1,18 @@
class FillStyleLinearGradient {
constructor(x0, y0, x1, y1) {
this._start_pos = { _x: x0, _y: y0 };
this._end_pos = { _x: x1, _y: y1 };
this._stop_count = 0;
this._stops = [0, 0, 0, 0, 0];
}
addColorStop = function (pos, color) {
if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) {
this._stops[this._stop_count] = { _pos: pos, _color: color };
this._stop_count++;
}
}
}
export default FillStyleLinearGradient;

View File

@ -0,0 +1,8 @@
class FillStylePattern {
constructor(img, pattern) {
this._style = pattern;
this._img = img;
}
}
export default FillStylePattern;

View File

@ -0,0 +1,17 @@
class FillStyleRadialGradient {
constructor(x0, y0, r0, x1, y1, r1) {
this._start_pos = { _x: x0, _y: y0, _r: r0 };
this._end_pos = { _x: x1, _y: y1, _r: r1 };
this._stop_count = 0;
this._stops = [0, 0, 0, 0, 0];
}
addColorStop(pos, color) {
if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) {
this._stops[this._stop_count] = { _pos: pos, _color: color };
this._stop_count++;
}
}
}
export default FillStyleRadialGradient;

View File

@ -0,0 +1,666 @@
import FillStylePattern from './FillStylePattern';
import FillStyleLinearGradient from './FillStyleLinearGradient';
import FillStyleRadialGradient from './FillStyleRadialGradient';
import GImage from '../env/image.js';
import {
ArrayBufferToBase64,
Base64ToUint8ClampedArray
} from '../env/tool.js';
export default class CanvasRenderingContext2D {
_drawCommands = '';
_globalAlpha = 1.0;
_fillStyle = 'rgb(0,0,0)';
_strokeStyle = 'rgb(0,0,0)';
_lineWidth = 1;
_lineCap = 'butt';
_lineJoin = 'miter';
_miterLimit = 10;
_globalCompositeOperation = 'source-over';
_textAlign = 'start';
_textBaseline = 'alphabetic';
_font = '10px sans-serif';
_savedGlobalAlpha = [];
timer = null;
componentId = null;
_notCommitDrawImageCache = [];
_needRedrawImageCache = [];
_redrawCommands = '';
_autoSaveContext = true;
// _imageMap = new GHashMap();
// _textureMap = new GHashMap();
constructor() {
this.className = 'CanvasRenderingContext2D';
//this.save()
}
setFillStyle(value) {
this.fillStyle = value;
}
set fillStyle(value) {
this._fillStyle = value;
if (typeof(value) == 'string') {
this._drawCommands = this._drawCommands.concat("F" + value + ";");
} else if (value instanceof FillStylePattern) {
const image = value._img;
if (!image.complete) {
image.onload = () => {
var index = this._needRedrawImageCache.indexOf(image);
if (index > -1) {
this._needRedrawImageCache.splice(index, 1);
CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
this._redrawflush(true);
}
}
this._notCommitDrawImageCache.push(image);
} else {
CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
}
//CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";");
} else if (value instanceof FillStyleLinearGradient) {
var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," +
value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," +
value._stop_count;
for (var i = 0; i < value._stop_count; ++i) {
command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
}
this._drawCommands = this._drawCommands.concat(command + ";");
} else if (value instanceof FillStyleRadialGradient) {
var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r
.toFixed(2) + "," +
value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," + value._end_pos._r.toFixed(2) + "," +
value._stop_count;
for (var i = 0; i < value._stop_count; ++i) {
command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
}
this._drawCommands = this._drawCommands.concat(command + ";");
}
}
get fillStyle() {
return this._fillStyle;
}
get globalAlpha() {
return this._globalAlpha;
}
setGlobalAlpha(value) {
this.globalAlpha = value;
}
set globalAlpha(value) {
this._globalAlpha = value;
this._drawCommands = this._drawCommands.concat("a" + value.toFixed(2) + ";");
}
get strokeStyle() {
return this._strokeStyle;
}
setStrokeStyle(value) {
this.strokeStyle = value;
}
set strokeStyle(value) {
this._strokeStyle = value;
if (typeof(value) == 'string') {
this._drawCommands = this._drawCommands.concat("S" + value + ";");
} else if (value instanceof FillStylePattern) {
CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";");
} else if (value instanceof FillStyleLinearGradient) {
var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," +
value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," +
value._stop_count;
for (var i = 0; i < value._stop_count; ++i) {
command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
}
this._drawCommands = this._drawCommands.concat(command + ";");
} else if (value instanceof FillStyleRadialGradient) {
var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r
.toFixed(2) + "," +
value._end_pos._x.toFixed(2) + "," + value._end_pos._y + ",".toFixed(2) + value._end_pos._r.toFixed(2) + "," +
value._stop_count;
for (var i = 0; i < value._stop_count; ++i) {
command += ("," + value._stops[i]._pos + "," + value._stops[i]._color);
}
this._drawCommands = this._drawCommands.concat(command + ";");
}
}
get lineWidth() {
return this._lineWidth;
}
setLineWidth(value) {
this.lineWidth = value;
}
set lineWidth(value) {
this._lineWidth = value;
this._drawCommands = this._drawCommands.concat("W" + value + ";");
}
get lineCap() {
return this._lineCap;
}
setLineCap(value) {
this.lineCap = value;
}
set lineCap(value) {
this._lineCap = value;
this._drawCommands = this._drawCommands.concat("C" + value + ";");
}
get lineJoin() {
return this._lineJoin;
}
setLineJoin(value) {
this.lineJoin = value
}
set lineJoin(value) {
this._lineJoin = value;
this._drawCommands = this._drawCommands.concat("J" + value + ";");
}
get miterLimit() {
return this._miterLimit;
}
setMiterLimit(value) {
this.miterLimit = value
}
set miterLimit(value) {
this._miterLimit = value;
this._drawCommands = this._drawCommands.concat("M" + value + ";");
}
get globalCompositeOperation() {
return this._globalCompositeOperation;
}
set globalCompositeOperation(value) {
this._globalCompositeOperation = value;
let mode = 0;
switch (value) {
case "source-over":
mode = 0;
break;
case "source-atop":
mode = 5;
break;
case "source-in":
mode = 0;
break;
case "source-out":
mode = 2;
break;
case "destination-over":
mode = 4;
break;
case "destination-atop":
mode = 4;
break;
case "destination-in":
mode = 4;
break;
case "destination-out":
mode = 3;
break;
case "lighter":
mode = 1;
break;
case "copy":
mode = 2;
break;
case "xor":
mode = 6;
break;
default:
mode = 0;
}
this._drawCommands = this._drawCommands.concat("B" + mode + ";");
}
get textAlign() {
return this._textAlign;
}
setTextAlign(value) {
this.textAlign = value
}
set textAlign(value) {
this._textAlign = value;
let Align = 0;
switch (value) {
case "start":
Align = 0;
break;
case "end":
Align = 1;
break;
case "left":
Align = 2;
break;
case "center":
Align = 3;
break;
case "right":
Align = 4;
break;
default:
Align = 0;
}
this._drawCommands = this._drawCommands.concat("A" + Align + ";");
}
get textBaseline() {
return this._textBaseline;
}
setTextBaseline(value) {
this.textBaseline = value
}
set textBaseline(value) {
this._textBaseline = value;
let baseline = 0;
switch (value) {
case "alphabetic":
baseline = 0;
break;
case "middle":
baseline = 1;
break;
case "top":
baseline = 2;
break;
case "hanging":
baseline = 3;
break;
case "bottom":
baseline = 4;
break;
case "ideographic":
baseline = 5;
break;
default:
baseline = 0;
break;
}
this._drawCommands = this._drawCommands.concat("E" + baseline + ";");
}
get font() {
return this._font;
}
setFontSize(size) {
var str = this._font;
var strs = str.trim().split(/\s+/);
for (var i = 0; i < strs.length; i++) {
var values = ["normal", "italic", "oblique", "normal", "small-caps", "normal", "bold",
"bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", "900",
"normal", "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
"semi-expanded", "expanded", "extra-expanded", "ultra-expanded"
];
if (-1 == values.indexOf(strs[i].trim())) {
if (typeof size === 'string') {
strs[i] = size;
} else if (typeof size === 'number') {
strs[i] = String(size) + 'px';
}
break;
}
}
this.font = strs.join(" ");
}
set font(value) {
this._font = value;
this._drawCommands = this._drawCommands.concat("j" + value + ";");
}
setTransform(a, b, c, d, tx, ty) {
this._drawCommands = this._drawCommands.concat("t" +
(a === 1 ? "1" : a.toFixed(2)) + "," +
(b === 0 ? "0" : b.toFixed(2)) + "," +
(c === 0 ? "0" : c.toFixed(2)) + "," +
(d === 1 ? "1" : d.toFixed(2)) + "," + tx.toFixed(2) + "," + ty.toFixed(2) + ";");
}
transform(a, b, c, d, tx, ty) {
this._drawCommands = this._drawCommands.concat("f" +
(a === 1 ? "1" : a.toFixed(2)) + "," +
(b === 0 ? "0" : b.toFixed(2)) + "," +
(c === 0 ? "0" : c.toFixed(2)) + "," +
(d === 1 ? "1" : d.toFixed(2)) + "," + tx + "," + ty + ";");
}
resetTransform() {
this._drawCommands = this._drawCommands.concat("m;");
}
scale(a, d) {
this._drawCommands = this._drawCommands.concat("k" + a.toFixed(2) + "," +
d.toFixed(2) + ";");
}
rotate(angle) {
this._drawCommands = this._drawCommands
.concat("r" + angle.toFixed(6) + ";");
}
translate(tx, ty) {
this._drawCommands = this._drawCommands.concat("l" + tx.toFixed(2) + "," + ty.toFixed(2) + ";");
}
save() {
this._savedGlobalAlpha.push(this._globalAlpha);
this._drawCommands = this._drawCommands.concat("v;");
}
restore() {
this._drawCommands = this._drawCommands.concat("e;");
this._globalAlpha = this._savedGlobalAlpha.pop();
}
createPattern(img, pattern) {
if (typeof img === 'string') {
var imgObj = new GImage();
imgObj.src = img;
img = imgObj;
}
return new FillStylePattern(img, pattern);
}
createLinearGradient(x0, y0, x1, y1) {
return new FillStyleLinearGradient(x0, y0, x1, y1);
}
createRadialGradient = function(x0, y0, r0, x1, y1, r1) {
return new FillStyleRadialGradient(x0, y0, r0, x1, y1, r1);
};
createCircularGradient = function(x0, y0, r0) {
return new FillStyleRadialGradient(x0, y0, 0, x0, y0, r0);
};
strokeRect(x, y, w, h) {
this._drawCommands = this._drawCommands.concat("s" + x + "," + y + "," + w + "," + h + ";");
}
clearRect(x, y, w, h) {
this._drawCommands = this._drawCommands.concat("c" + x + "," + y + "," + w +
"," + h + ";");
}
clip() {
this._drawCommands = this._drawCommands.concat("p;");
}
resetClip() {
this._drawCommands = this._drawCommands.concat("q;");
}
closePath() {
this._drawCommands = this._drawCommands.concat("o;");
}
moveTo(x, y) {
this._drawCommands = this._drawCommands.concat("g" + x.toFixed(2) + "," + y.toFixed(2) + ";");
}
lineTo(x, y) {
this._drawCommands = this._drawCommands.concat("i" + x.toFixed(2) + "," + y.toFixed(2) + ";");
}
quadraticCurveTo = function(cpx, cpy, x, y) {
this._drawCommands = this._drawCommands.concat("u" + cpx + "," + cpy + "," + x + "," + y + ";");
}
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, ) {
this._drawCommands = this._drawCommands.concat(
"z" + cp1x.toFixed(2) + "," + cp1y.toFixed(2) + "," + cp2x.toFixed(2) + "," + cp2y.toFixed(2) + "," +
x.toFixed(2) + "," + y.toFixed(2) + ";");
}
arcTo(x1, y1, x2, y2, radius) {
this._drawCommands = this._drawCommands.concat("h" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + radius + ";");
}
beginPath() {
this._drawCommands = this._drawCommands.concat("b;");
}
fillRect(x, y, w, h) {
this._drawCommands = this._drawCommands.concat("n" + x + "," + y + "," + w +
"," + h + ";");
}
rect(x, y, w, h) {
this._drawCommands = this._drawCommands.concat("w" + x + "," + y + "," + w + "," + h + ";");
}
fill() {
this._drawCommands = this._drawCommands.concat("L;");
}
stroke(path) {
this._drawCommands = this._drawCommands.concat("x;");
}
arc(x, y, radius, startAngle, endAngle, anticlockwise) {
let ianticlockwise = 0;
if (anticlockwise) {
ianticlockwise = 1;
}
this._drawCommands = this._drawCommands.concat(
"y" + x.toFixed(2) + "," + y.toFixed(2) + "," +
radius.toFixed(2) + "," + startAngle + "," + endAngle + "," + ianticlockwise +
";"
);
}
fillText(text, x, y) {
let tmptext = text.replace(/!/g, "!!");
tmptext = tmptext.replace(/,/g, "!,");
tmptext = tmptext.replace(/;/g, "!;");
this._drawCommands = this._drawCommands.concat("T" + tmptext + "," + x + "," + y + ",0.0;");
}
strokeText = function(text, x, y) {
let tmptext = text.replace(/!/g, "!!");
tmptext = tmptext.replace(/,/g, "!,");
tmptext = tmptext.replace(/;/g, "!;");
this._drawCommands = this._drawCommands.concat("U" + tmptext + "," + x + "," + y + ",0.0;");
}
measureText(text) {
return CanvasRenderingContext2D.GBridge.measureText(text, this.font, this.componentId);
}
isPointInPath = function(x, y) {
throw new Error('GCanvas not supported yet');
}
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (typeof image === 'string') {
var imgObj = new GImage();
imgObj.src = image;
image = imgObj;
}
if (image instanceof GImage) {
if (!image.complete) {
imgObj.onload = () => {
var index = this._needRedrawImageCache.indexOf(image);
if (index > -1) {
this._needRedrawImageCache.splice(index, 1);
CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
this._redrawflush(true);
}
}
this._notCommitDrawImageCache.push(image);
} else {
CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id);
}
var srcArgs = [image, sx, sy, sw, sh, dx, dy, dw, dh];
var args = [];
for (var arg in srcArgs) {
if (typeof(srcArgs[arg]) != 'undefined') {
args.push(srcArgs[arg]);
}
}
this.__drawImage.apply(this, args);
//this.__drawImage(image,sx, sy, sw, sh, dx, dy, dw, dh);
}
}
__drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
const numArgs = arguments.length;
function drawImageCommands() {
if (numArgs === 3) {
const x = parseFloat(sx) || 0.0;
const y = parseFloat(sy) || 0.0;
return ("d" + image._id + ",0,0," +
image.width + "," + image.height + "," +
x + "," + y + "," + image.width + "," + image.height + ";");
} else if (numArgs === 5) {
const x = parseFloat(sx) || 0.0;
const y = parseFloat(sy) || 0.0;
const width = parseInt(sw) || image.width;
const height = parseInt(sh) || image.height;
return ("d" + image._id + ",0,0," +
image.width + "," + image.height + "," +
x + "," + y + "," + width + "," + height + ";");
} else if (numArgs === 9) {
sx = parseFloat(sx) || 0.0;
sy = parseFloat(sy) || 0.0;
sw = parseInt(sw) || image.width;
sh = parseInt(sh) || image.height;
dx = parseFloat(dx) || 0.0;
dy = parseFloat(dy) || 0.0;
dw = parseInt(dw) || image.width;
dh = parseInt(dh) || image.height;
return ("d" + image._id + "," +
sx + "," + sy + "," + sw + "," + sh + "," +
dx + "," + dy + "," + dw + "," + dh + ";");
}
}
this._drawCommands += drawImageCommands();
}
_flush(reserve, callback) {
const commands = this._drawCommands;
this._drawCommands = '';
CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback);
this._needRender = false;
}
_redrawflush(reserve, callback) {
const commands = this._redrawCommands;
CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback);
if (this._needRedrawImageCache.length == 0) {
this._redrawCommands = '';
}
}
draw(reserve, callback) {
if (!reserve) {
this._globalAlpha = this._savedGlobalAlpha.pop();
this._savedGlobalAlpha.push(this._globalAlpha);
this._redrawCommands = this._drawCommands;
this._needRedrawImageCache = this._notCommitDrawImageCache;
if (this._autoSaveContext) {
this._drawCommands = ("v;" + this._drawCommands);
this._autoSaveContext = false;
} else {
this._drawCommands = ("e;X;v;" + this._drawCommands);
}
} else {
this._needRedrawImageCache = this._needRedrawImageCache.concat(this._notCommitDrawImageCache);
this._redrawCommands += this._drawCommands;
if (this._autoSaveContext) {
this._drawCommands = ("v;" + this._drawCommands);
this._autoSaveContext = false;
}
}
this._notCommitDrawImageCache = [];
if (this._flush) {
this._flush(reserve, callback);
}
}
getImageData(x, y, w, h, callback) {
CanvasRenderingContext2D.GBridge.getImageData(this.componentId, x, y, w, h, function(res) {
res.data = Base64ToUint8ClampedArray(res.data);
if (typeof(callback) == 'function') {
callback(res);
}
});
}
putImageData(data, x, y, w, h, callback) {
if (data instanceof Uint8ClampedArray) {
data = ArrayBufferToBase64(data);
CanvasRenderingContext2D.GBridge.putImageData(this.componentId, data, x, y, w, h, function(res) {
if (typeof(callback) == 'function') {
callback(res);
}
});
}
}
toTempFilePath(x, y, width, height, destWidth, destHeight, fileType, quality, callback) {
CanvasRenderingContext2D.GBridge.toTempFilePath(this.componentId, x, y, width, height, destWidth, destHeight,
fileType, quality,
function(res) {
if (typeof(callback) == 'function') {
callback(res);
}
});
}
}

View File

@ -0,0 +1,11 @@
export default class WebGLActiveInfo {
className = 'WebGLActiveInfo';
constructor({
type, name, size
}) {
this.type = type;
this.name = name;
this.size = size;
}
}

View File

@ -0,0 +1,21 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLBuffer';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLBuffer {
className = name;
constructor(id) {
this.id = id;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,21 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLFrameBuffer';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLFramebuffer {
className = name;
constructor(id) {
this.id = id;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,298 @@
export default {
"DEPTH_BUFFER_BIT": 256,
"STENCIL_BUFFER_BIT": 1024,
"COLOR_BUFFER_BIT": 16384,
"POINTS": 0,
"LINES": 1,
"LINE_LOOP": 2,
"LINE_STRIP": 3,
"TRIANGLES": 4,
"TRIANGLE_STRIP": 5,
"TRIANGLE_FAN": 6,
"ZERO": 0,
"ONE": 1,
"SRC_COLOR": 768,
"ONE_MINUS_SRC_COLOR": 769,
"SRC_ALPHA": 770,
"ONE_MINUS_SRC_ALPHA": 771,
"DST_ALPHA": 772,
"ONE_MINUS_DST_ALPHA": 773,
"DST_COLOR": 774,
"ONE_MINUS_DST_COLOR": 775,
"SRC_ALPHA_SATURATE": 776,
"FUNC_ADD": 32774,
"BLEND_EQUATION": 32777,
"BLEND_EQUATION_RGB": 32777,
"BLEND_EQUATION_ALPHA": 34877,
"FUNC_SUBTRACT": 32778,
"FUNC_REVERSE_SUBTRACT": 32779,
"BLEND_DST_RGB": 32968,
"BLEND_SRC_RGB": 32969,
"BLEND_DST_ALPHA": 32970,
"BLEND_SRC_ALPHA": 32971,
"CONSTANT_COLOR": 32769,
"ONE_MINUS_CONSTANT_COLOR": 32770,
"CONSTANT_ALPHA": 32771,
"ONE_MINUS_CONSTANT_ALPHA": 32772,
"BLEND_COLOR": 32773,
"ARRAY_BUFFER": 34962,
"ELEMENT_ARRAY_BUFFER": 34963,
"ARRAY_BUFFER_BINDING": 34964,
"ELEMENT_ARRAY_BUFFER_BINDING": 34965,
"STREAM_DRAW": 35040,
"STATIC_DRAW": 35044,
"DYNAMIC_DRAW": 35048,
"BUFFER_SIZE": 34660,
"BUFFER_USAGE": 34661,
"CURRENT_VERTEX_ATTRIB": 34342,
"FRONT": 1028,
"BACK": 1029,
"FRONT_AND_BACK": 1032,
"TEXTURE_2D": 3553,
"CULL_FACE": 2884,
"BLEND": 3042,
"DITHER": 3024,
"STENCIL_TEST": 2960,
"DEPTH_TEST": 2929,
"SCISSOR_TEST": 3089,
"POLYGON_OFFSET_FILL": 32823,
"SAMPLE_ALPHA_TO_COVERAGE": 32926,
"SAMPLE_COVERAGE": 32928,
"NO_ERROR": 0,
"INVALID_ENUM": 1280,
"INVALID_VALUE": 1281,
"INVALID_OPERATION": 1282,
"OUT_OF_MEMORY": 1285,
"CW": 2304,
"CCW": 2305,
"LINE_WIDTH": 2849,
"ALIASED_POINT_SIZE_RANGE": 33901,
"ALIASED_LINE_WIDTH_RANGE": 33902,
"CULL_FACE_MODE": 2885,
"FRONT_FACE": 2886,
"DEPTH_RANGE": 2928,
"DEPTH_WRITEMASK": 2930,
"DEPTH_CLEAR_VALUE": 2931,
"DEPTH_FUNC": 2932,
"STENCIL_CLEAR_VALUE": 2961,
"STENCIL_FUNC": 2962,
"STENCIL_FAIL": 2964,
"STENCIL_PASS_DEPTH_FAIL": 2965,
"STENCIL_PASS_DEPTH_PASS": 2966,
"STENCIL_REF": 2967,
"STENCIL_VALUE_MASK": 2963,
"STENCIL_WRITEMASK": 2968,
"STENCIL_BACK_FUNC": 34816,
"STENCIL_BACK_FAIL": 34817,
"STENCIL_BACK_PASS_DEPTH_FAIL": 34818,
"STENCIL_BACK_PASS_DEPTH_PASS": 34819,
"STENCIL_BACK_REF": 36003,
"STENCIL_BACK_VALUE_MASK": 36004,
"STENCIL_BACK_WRITEMASK": 36005,
"VIEWPORT": 2978,
"SCISSOR_BOX": 3088,
"COLOR_CLEAR_VALUE": 3106,
"COLOR_WRITEMASK": 3107,
"UNPACK_ALIGNMENT": 3317,
"PACK_ALIGNMENT": 3333,
"MAX_TEXTURE_SIZE": 3379,
"MAX_VIEWPORT_DIMS": 3386,
"SUBPIXEL_BITS": 3408,
"RED_BITS": 3410,
"GREEN_BITS": 3411,
"BLUE_BITS": 3412,
"ALPHA_BITS": 3413,
"DEPTH_BITS": 3414,
"STENCIL_BITS": 3415,
"POLYGON_OFFSET_UNITS": 10752,
"POLYGON_OFFSET_FACTOR": 32824,
"TEXTURE_BINDING_2D": 32873,
"SAMPLE_BUFFERS": 32936,
"SAMPLES": 32937,
"SAMPLE_COVERAGE_VALUE": 32938,
"SAMPLE_COVERAGE_INVERT": 32939,
"COMPRESSED_TEXTURE_FORMATS": 34467,
"DONT_CARE": 4352,
"FASTEST": 4353,
"NICEST": 4354,
"GENERATE_MIPMAP_HINT": 33170,
"BYTE": 5120,
"UNSIGNED_BYTE": 5121,
"SHORT": 5122,
"UNSIGNED_SHORT": 5123,
"INT": 5124,
"UNSIGNED_INT": 5125,
"FLOAT": 5126,
"DEPTH_COMPONENT": 6402,
"ALPHA": 6406,
"RGB": 6407,
"RGBA": 6408,
"LUMINANCE": 6409,
"LUMINANCE_ALPHA": 6410,
"UNSIGNED_SHORT_4_4_4_4": 32819,
"UNSIGNED_SHORT_5_5_5_1": 32820,
"UNSIGNED_SHORT_5_6_5": 33635,
"FRAGMENT_SHADER": 35632,
"VERTEX_SHADER": 35633,
"MAX_VERTEX_ATTRIBS": 34921,
"MAX_VERTEX_UNIFORM_VECTORS": 36347,
"MAX_VARYING_VECTORS": 36348,
"MAX_COMBINED_TEXTURE_IMAGE_UNITS": 35661,
"MAX_VERTEX_TEXTURE_IMAGE_UNITS": 35660,
"MAX_TEXTURE_IMAGE_UNITS": 34930,
"MAX_FRAGMENT_UNIFORM_VECTORS": 36349,
"SHADER_TYPE": 35663,
"DELETE_STATUS": 35712,
"LINK_STATUS": 35714,
"VALIDATE_STATUS": 35715,
"ATTACHED_SHADERS": 35717,
"ACTIVE_UNIFORMS": 35718,
"ACTIVE_ATTRIBUTES": 35721,
"SHADING_LANGUAGE_VERSION": 35724,
"CURRENT_PROGRAM": 35725,
"NEVER": 512,
"LESS": 513,
"EQUAL": 514,
"LEQUAL": 515,
"GREATER": 516,
"NOTEQUAL": 517,
"GEQUAL": 518,
"ALWAYS": 519,
"KEEP": 7680,
"REPLACE": 7681,
"INCR": 7682,
"DECR": 7683,
"INVERT": 5386,
"INCR_WRAP": 34055,
"DECR_WRAP": 34056,
"VENDOR": 7936,
"RENDERER": 7937,
"VERSION": 7938,
"NEAREST": 9728,
"LINEAR": 9729,
"NEAREST_MIPMAP_NEAREST": 9984,
"LINEAR_MIPMAP_NEAREST": 9985,
"NEAREST_MIPMAP_LINEAR": 9986,
"LINEAR_MIPMAP_LINEAR": 9987,
"TEXTURE_MAG_FILTER": 10240,
"TEXTURE_MIN_FILTER": 10241,
"TEXTURE_WRAP_S": 10242,
"TEXTURE_WRAP_T": 10243,
"TEXTURE": 5890,
"TEXTURE_CUBE_MAP": 34067,
"TEXTURE_BINDING_CUBE_MAP": 34068,
"TEXTURE_CUBE_MAP_POSITIVE_X": 34069,
"TEXTURE_CUBE_MAP_NEGATIVE_X": 34070,
"TEXTURE_CUBE_MAP_POSITIVE_Y": 34071,
"TEXTURE_CUBE_MAP_NEGATIVE_Y": 34072,
"TEXTURE_CUBE_MAP_POSITIVE_Z": 34073,
"TEXTURE_CUBE_MAP_NEGATIVE_Z": 34074,
"MAX_CUBE_MAP_TEXTURE_SIZE": 34076,
"TEXTURE0": 33984,
"TEXTURE1": 33985,
"TEXTURE2": 33986,
"TEXTURE3": 33987,
"TEXTURE4": 33988,
"TEXTURE5": 33989,
"TEXTURE6": 33990,
"TEXTURE7": 33991,
"TEXTURE8": 33992,
"TEXTURE9": 33993,
"TEXTURE10": 33994,
"TEXTURE11": 33995,
"TEXTURE12": 33996,
"TEXTURE13": 33997,
"TEXTURE14": 33998,
"TEXTURE15": 33999,
"TEXTURE16": 34000,
"TEXTURE17": 34001,
"TEXTURE18": 34002,
"TEXTURE19": 34003,
"TEXTURE20": 34004,
"TEXTURE21": 34005,
"TEXTURE22": 34006,
"TEXTURE23": 34007,
"TEXTURE24": 34008,
"TEXTURE25": 34009,
"TEXTURE26": 34010,
"TEXTURE27": 34011,
"TEXTURE28": 34012,
"TEXTURE29": 34013,
"TEXTURE30": 34014,
"TEXTURE31": 34015,
"ACTIVE_TEXTURE": 34016,
"REPEAT": 10497,
"CLAMP_TO_EDGE": 33071,
"MIRRORED_REPEAT": 33648,
"FLOAT_VEC2": 35664,
"FLOAT_VEC3": 35665,
"FLOAT_VEC4": 35666,
"INT_VEC2": 35667,
"INT_VEC3": 35668,
"INT_VEC4": 35669,
"BOOL": 35670,
"BOOL_VEC2": 35671,
"BOOL_VEC3": 35672,
"BOOL_VEC4": 35673,
"FLOAT_MAT2": 35674,
"FLOAT_MAT3": 35675,
"FLOAT_MAT4": 35676,
"SAMPLER_2D": 35678,
"SAMPLER_CUBE": 35680,
"VERTEX_ATTRIB_ARRAY_ENABLED": 34338,
"VERTEX_ATTRIB_ARRAY_SIZE": 34339,
"VERTEX_ATTRIB_ARRAY_STRIDE": 34340,
"VERTEX_ATTRIB_ARRAY_TYPE": 34341,
"VERTEX_ATTRIB_ARRAY_NORMALIZED": 34922,
"VERTEX_ATTRIB_ARRAY_POINTER": 34373,
"VERTEX_ATTRIB_ARRAY_BUFFER_BINDING": 34975,
"IMPLEMENTATION_COLOR_READ_TYPE": 35738,
"IMPLEMENTATION_COLOR_READ_FORMAT": 35739,
"COMPILE_STATUS": 35713,
"LOW_FLOAT": 36336,
"MEDIUM_FLOAT": 36337,
"HIGH_FLOAT": 36338,
"LOW_INT": 36339,
"MEDIUM_INT": 36340,
"HIGH_INT": 36341,
"FRAMEBUFFER": 36160,
"RENDERBUFFER": 36161,
"RGBA4": 32854,
"RGB5_A1": 32855,
"RGB565": 36194,
"DEPTH_COMPONENT16": 33189,
"STENCIL_INDEX8": 36168,
"DEPTH_STENCIL": 34041,
"RENDERBUFFER_WIDTH": 36162,
"RENDERBUFFER_HEIGHT": 36163,
"RENDERBUFFER_INTERNAL_FORMAT": 36164,
"RENDERBUFFER_RED_SIZE": 36176,
"RENDERBUFFER_GREEN_SIZE": 36177,
"RENDERBUFFER_BLUE_SIZE": 36178,
"RENDERBUFFER_ALPHA_SIZE": 36179,
"RENDERBUFFER_DEPTH_SIZE": 36180,
"RENDERBUFFER_STENCIL_SIZE": 36181,
"FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE": 36048,
"FRAMEBUFFER_ATTACHMENT_OBJECT_NAME": 36049,
"FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL": 36050,
"FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE": 36051,
"COLOR_ATTACHMENT0": 36064,
"DEPTH_ATTACHMENT": 36096,
"STENCIL_ATTACHMENT": 36128,
"DEPTH_STENCIL_ATTACHMENT": 33306,
"NONE": 0,
"FRAMEBUFFER_COMPLETE": 36053,
"FRAMEBUFFER_INCOMPLETE_ATTACHMENT": 36054,
"FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT": 36055,
"FRAMEBUFFER_INCOMPLETE_DIMENSIONS": 36057,
"FRAMEBUFFER_UNSUPPORTED": 36061,
"FRAMEBUFFER_BINDING": 36006,
"RENDERBUFFER_BINDING": 36007,
"MAX_RENDERBUFFER_SIZE": 34024,
"INVALID_FRAMEBUFFER_OPERATION": 1286,
"UNPACK_FLIP_Y_WEBGL": 37440,
"UNPACK_PREMULTIPLY_ALPHA_WEBGL": 37441,
"CONTEXT_LOST_WEBGL": 37442,
"UNPACK_COLORSPACE_CONVERSION_WEBGL": 37443,
"BROWSER_DEFAULT_WEBGL": 37444
};

View File

@ -0,0 +1,142 @@
let i = 1;
const GLmethod = {};
GLmethod.activeTexture = i++; //1
GLmethod.attachShader = i++;
GLmethod.bindAttribLocation = i++;
GLmethod.bindBuffer = i++;
GLmethod.bindFramebuffer = i++;
GLmethod.bindRenderbuffer = i++;
GLmethod.bindTexture = i++;
GLmethod.blendColor = i++;
GLmethod.blendEquation = i++;
GLmethod.blendEquationSeparate = i++; //10
GLmethod.blendFunc = i++;
GLmethod.blendFuncSeparate = i++;
GLmethod.bufferData = i++;
GLmethod.bufferSubData = i++;
GLmethod.checkFramebufferStatus = i++;
GLmethod.clear = i++;
GLmethod.clearColor = i++;
GLmethod.clearDepth = i++;
GLmethod.clearStencil = i++;
GLmethod.colorMask = i++; //20
GLmethod.compileShader = i++;
GLmethod.compressedTexImage2D = i++;
GLmethod.compressedTexSubImage2D = i++;
GLmethod.copyTexImage2D = i++;
GLmethod.copyTexSubImage2D = i++;
GLmethod.createBuffer = i++;
GLmethod.createFramebuffer = i++;
GLmethod.createProgram = i++;
GLmethod.createRenderbuffer = i++;
GLmethod.createShader = i++; //30
GLmethod.createTexture = i++;
GLmethod.cullFace = i++;
GLmethod.deleteBuffer = i++;
GLmethod.deleteFramebuffer = i++;
GLmethod.deleteProgram = i++;
GLmethod.deleteRenderbuffer = i++;
GLmethod.deleteShader = i++;
GLmethod.deleteTexture = i++;
GLmethod.depthFunc = i++;
GLmethod.depthMask = i++; //40
GLmethod.depthRange = i++;
GLmethod.detachShader = i++;
GLmethod.disable = i++;
GLmethod.disableVertexAttribArray = i++;
GLmethod.drawArrays = i++;
GLmethod.drawArraysInstancedANGLE = i++;
GLmethod.drawElements = i++;
GLmethod.drawElementsInstancedANGLE = i++;
GLmethod.enable = i++;
GLmethod.enableVertexAttribArray = i++; //50
GLmethod.flush = i++;
GLmethod.framebufferRenderbuffer = i++;
GLmethod.framebufferTexture2D = i++;
GLmethod.frontFace = i++;
GLmethod.generateMipmap = i++;
GLmethod.getActiveAttrib = i++;
GLmethod.getActiveUniform = i++;
GLmethod.getAttachedShaders = i++;
GLmethod.getAttribLocation = i++;
GLmethod.getBufferParameter = i++; //60
GLmethod.getContextAttributes = i++;
GLmethod.getError = i++;
GLmethod.getExtension = i++;
GLmethod.getFramebufferAttachmentParameter = i++;
GLmethod.getParameter = i++;
GLmethod.getProgramInfoLog = i++;
GLmethod.getProgramParameter = i++;
GLmethod.getRenderbufferParameter = i++;
GLmethod.getShaderInfoLog = i++;
GLmethod.getShaderParameter = i++; //70
GLmethod.getShaderPrecisionFormat = i++;
GLmethod.getShaderSource = i++;
GLmethod.getSupportedExtensions = i++;
GLmethod.getTexParameter = i++;
GLmethod.getUniform = i++;
GLmethod.getUniformLocation = i++;
GLmethod.getVertexAttrib = i++;
GLmethod.getVertexAttribOffset = i++;
GLmethod.isBuffer = i++;
GLmethod.isContextLost = i++; //80
GLmethod.isEnabled = i++;
GLmethod.isFramebuffer = i++;
GLmethod.isProgram = i++;
GLmethod.isRenderbuffer = i++;
GLmethod.isShader = i++;
GLmethod.isTexture = i++;
GLmethod.lineWidth = i++;
GLmethod.linkProgram = i++;
GLmethod.pixelStorei = i++;
GLmethod.polygonOffset = i++; //90
GLmethod.readPixels = i++;
GLmethod.renderbufferStorage = i++;
GLmethod.sampleCoverage = i++;
GLmethod.scissor = i++;
GLmethod.shaderSource = i++;
GLmethod.stencilFunc = i++;
GLmethod.stencilFuncSeparate = i++;
GLmethod.stencilMask = i++;
GLmethod.stencilMaskSeparate = i++;
GLmethod.stencilOp = i++; //100
GLmethod.stencilOpSeparate = i++;
GLmethod.texImage2D = i++;
GLmethod.texParameterf = i++;
GLmethod.texParameteri = i++;
GLmethod.texSubImage2D = i++;
GLmethod.uniform1f = i++;
GLmethod.uniform1fv = i++;
GLmethod.uniform1i = i++;
GLmethod.uniform1iv = i++;
GLmethod.uniform2f = i++; //110
GLmethod.uniform2fv = i++;
GLmethod.uniform2i = i++;
GLmethod.uniform2iv = i++;
GLmethod.uniform3f = i++;
GLmethod.uniform3fv = i++;
GLmethod.uniform3i = i++;
GLmethod.uniform3iv = i++;
GLmethod.uniform4f = i++;
GLmethod.uniform4fv = i++;
GLmethod.uniform4i = i++; //120
GLmethod.uniform4iv = i++;
GLmethod.uniformMatrix2fv = i++;
GLmethod.uniformMatrix3fv = i++;
GLmethod.uniformMatrix4fv = i++;
GLmethod.useProgram = i++;
GLmethod.validateProgram = i++;
GLmethod.vertexAttrib1f = i++; //new
GLmethod.vertexAttrib2f = i++; //new
GLmethod.vertexAttrib3f = i++; //new
GLmethod.vertexAttrib4f = i++; //new //130
GLmethod.vertexAttrib1fv = i++; //new
GLmethod.vertexAttrib2fv = i++; //new
GLmethod.vertexAttrib3fv = i++; //new
GLmethod.vertexAttrib4fv = i++; //new
GLmethod.vertexAttribPointer = i++;
GLmethod.viewport = i++;
export default GLmethod;

View File

@ -0,0 +1,23 @@
const GLtype = {};
[
"GLbitfield",
"GLboolean",
"GLbyte",
"GLclampf",
"GLenum",
"GLfloat",
"GLint",
"GLintptr",
"GLsizei",
"GLsizeiptr",
"GLshort",
"GLubyte",
"GLuint",
"GLushort"
].sort().map((typeName, i) => GLtype[typeName] = 1 >> (i + 1));
export default GLtype;

View File

@ -0,0 +1,21 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLProgram';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLProgram {
className = name;
constructor(id) {
this.id = id;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,21 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLRenderBuffer';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLRenderbuffer {
className = name;
constructor(id) {
this.id = id;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLShader';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLShader {
className = name;
constructor(id, type) {
this.id = id;
this.type = type;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,11 @@
export default class WebGLShaderPrecisionFormat {
className = 'WebGLShaderPrecisionFormat';
constructor({
rangeMin, rangeMax, precision
}) {
this.rangeMin = rangeMin;
this.rangeMax = rangeMax;
this.precision = precision;
}
}

View File

@ -0,0 +1,22 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLTexture';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLTexture {
className = name;
constructor(id, type) {
this.id = id;
this.type = type;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,22 @@
import {getTransferedObjectUUID} from './classUtils';
const name = 'WebGLUniformLocation';
function uuid(id) {
return getTransferedObjectUUID(name, id);
}
export default class WebGLUniformLocation {
className = name;
constructor(id, type) {
this.id = id;
this.type = type;
}
static uuid = uuid;
uuid() {
return uuid(this.id);
}
}

View File

@ -0,0 +1,3 @@
export function getTransferedObjectUUID(name, id) {
return `${name.toLowerCase()}-${id}`;
}

View File

@ -0,0 +1,74 @@
import GContext2D from '../context-2d/RenderingContext';
import GContextWebGL from '../context-webgl/RenderingContext';
export default class GCanvas {
// static GBridge = null;
id = null;
_needRender = true;
constructor(id, { disableAutoSwap }) {
this.id = id;
this._disableAutoSwap = disableAutoSwap;
if (disableAutoSwap) {
this._swapBuffers = () => {
GCanvas.GBridge.render(this.id);
}
}
}
getContext(type) {
let context = null;
if (type.match(/webgl/i)) {
context = new GContextWebGL(this);
context.componentId = this.id;
if (!this._disableAutoSwap) {
const render = () => {
if (this._needRender) {
GCanvas.GBridge.render(this.id);
this._needRender = false;
}
}
setInterval(render, 16);
}
GCanvas.GBridge.callSetContextType(this.id, 1); // 0 for 2d; 1 for webgl
} else if (type.match(/2d/i)) {
context = new GContext2D(this);
context.componentId = this.id;
// const render = ( callback ) => {
//
// const commands = context._drawCommands;
// context._drawCommands = '';
//
// GCanvas.GBridge.render2d(this.id, commands, callback);
// this._needRender = false;
// }
// //draw方法触发
// context._flush = render;
// //setInterval(render, 16);
GCanvas.GBridge.callSetContextType(this.id, 0);
} else {
throw new Error('not supported context ' + type);
}
return context;
}
reset() {
GCanvas.GBridge.callReset(this.id);
}
}

View File

@ -0,0 +1,96 @@
let incId = 1;
const noop = function () { };
class GImage {
static GBridge = null;
constructor() {
this._id = incId++;
this._width = 0;
this._height = 0;
this._src = undefined;
this._onload = noop;
this._onerror = noop;
this.complete = false;
}
get width() {
return this._width;
}
set width(v) {
this._width = v;
}
get height() {
return this._height;
}
set height(v) {
this._height = v;
}
get src() {
return this._src;
}
set src(v) {
if (v.startsWith('//')) {
v = 'http:' + v;
}
this._src = v;
GImage.GBridge.perloadImage([this._src, this._id], (data) => {
if (typeof data === 'string') {
data = JSON.parse(data);
}
if (data.error) {
var evt = { type: 'error', target: this };
this.onerror(evt);
} else {
this.complete = true;
this.width = typeof data.width === 'number' ? data.width : 0;
this.height = typeof data.height === 'number' ? data.height : 0;
var evt = { type: 'load', target: this };
this.onload(evt);
}
});
}
addEventListener(name, listener) {
if (name === 'load') {
this.onload = listener;
} else if (name === 'error') {
this.onerror = listener;
}
}
removeEventListener(name, listener) {
if (name === 'load') {
this.onload = noop;
} else if (name === 'error') {
this.onerror = noop;
}
}
get onload() {
return this._onload;
}
set onload(v) {
this._onload = v;
}
get onerror() {
return this._onerror;
}
set onerror(v) {
this._onerror = v;
}
}
export default GImage;

View File

@ -0,0 +1,24 @@
export function ArrayBufferToBase64 (buffer) {
var binary = '';
var bytes = new Uint8ClampedArray(buffer);
for (var len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
export function Base64ToUint8ClampedArray(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = atob(base64);
const outputArray = new Uint8ClampedArray(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

View File

@ -0,0 +1,39 @@
import GCanvas from './env/canvas';
import GImage from './env/image';
import GWebGLRenderingContext from './context-webgl/RenderingContext';
import GContext2D from './context-2d/RenderingContext';
import GBridgeWeex from './bridge/bridge-weex';
export let Image = GImage;
export let WeexBridge = GBridgeWeex;
export function enable(el, { bridge, debug, disableAutoSwap, disableComboCommands } = {}) {
const GBridge = GImage.GBridge = GCanvas.GBridge = GWebGLRenderingContext.GBridge = GContext2D.GBridge = bridge;
GBridge.callEnable(el.ref, [
0, // renderMode: 0--RENDERMODE_WHEN_DIRTY, 1--RENDERMODE_CONTINUOUSLY
-1, // hybridLayerType: 0--LAYER_TYPE_NONE 1--LAYER_TYPE_SOFTWARE 2--LAYER_TYPE_HARDWARE
false, // supportScroll
false, // newCanvasMode
1, // compatible
'white',// clearColor
false // sameLevel: newCanvasMode = true && true => GCanvasView and Webview is same level
]);
if (debug === true) {
GBridge.callEnableDebug();
}
if (disableComboCommands) {
GBridge.callEnableDisableCombo();
}
var canvas = new GCanvas(el.ref, { disableAutoSwap });
canvas.width = el.style.width;
canvas.height = el.style.height;
return canvas;
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,79 @@
{
"id": "Sansnn-uQRCode",
"displayName": "uQRCode 全端二维码生成插件 支持nvue 支持nodejs服务端",
"version": "4.0.6",
"description": "uQRCode是一款基于Javascript环境开发的二维码生成插件适用所有Javascript运行环境的前端应用和Node.js。",
"keywords": [
"二维码",
"uQRCode",
"qrcode",
"qr"
],
"repository": "https://github.com/Sansnn/uQRCode",
"engines": {
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/uqrcodejs",
"type": "sdk-js"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -19,7 +19,13 @@ const urlsConfig = {
addCorpMemberFromWxapp: "addCorpMemberFromWxapp",
updateCorpMemberFromWxapp: "updateCorpMemberFromWxapp",
submitCertProfile: 'submitCertProfile',
getMemberVerifyStatus: "getMemberVerifyStatus"
getMemberVerifyStatus: "getMemberVerifyStatus",
getJoinedTeams: "getJoinedTeams",
updateTeamInfo: "updateTeamInfo",
createOwnTeam: 'createOwnTeam',
removeTeammate: "removeTeammate",
toggleTeamLeaderRole: "toggleTeamLeaderRole",
joinTheInvitedTeam: 'joinTheInvitedTeam'
},
knowledgeBase: {
@ -43,7 +49,7 @@ const urlsConfig = {
getArticle: 'getArticle',
addArticleSendRecord: 'addArticleSendRecord'
},
survery: {
getSurveyCateList: 'getSurveryCateList',
getSurveyList: 'getList',
@ -83,7 +89,14 @@ const urlsConfig = {
im: {
getUserSig: 'getUserSig',
sendSystemMessage: "sendSystemMessage",
getChatRecordsByGroupId: "getChatRecordsByGroupId"
getChatRecordsByGroupId: "getChatRecordsByGroupId",
sendConsultRejectedMessage: "sendConsultRejectedMessage",
endConsultation: "endConsultation",
getGroupListByGroupId: "getGroupListByGroupId",
acceptConsultation: "acceptConsultation",
sendArticleMessage: "sendArticleMessage",
getChatRecordsByGroupId: "getChatRecordsByGroupId",
getGroupList: "getGroupList"
},
todo: {
getCustomerTodos: 'getCustomerTodos',
@ -102,7 +115,7 @@ const urlsConfig = {
addServiceRecord: 'addServiceRecord',
updateServiceRecord: 'updateServiceRecord',
removeServiceRecord: 'removeServiceRecord',
sendConsultRejectedMessage: "sendConsultRejectedMessage"
// sendConsultRejectedMessage: "sendConsultRejectedMessage"
}
}
@ -112,7 +125,7 @@ const urls = Object.keys(urlsConfig).reduce((acc, path) => {
keys.forEach((key) => {
const data = acc[key];
if (data) {
throw new Error(`${data[0]}.${data[1]}${path}.${url}重复了`)
throw new Error(`${data[0]}.${data[1]}${path}.${key}重复了`)
}
acc[key] = [path, config[key]]
return acc
@ -136,49 +149,3 @@ export default async function api(urlId, data) {
})
}
// 宣教文章相关 API
export async function getArticleCateList(data) {
return api('getArticleCateList', data);
}
export async function getArticleList(data) {
return api('getArticleList', data);
}
export async function getArticle(data) {
return api('getArticle', data);
}
export async function addArticleSendRecord(data) {
return api('addArticleSendRecord', data);
}
// 问卷相关 API
export async function getSurveyCateList(data) {
return api('getSurveyCateList', data);
}
export async function getSurveyList(data) {
return api('getSurveyList', data);
}
export async function createSurveyRecord(data) {
return api('createSurveyRecord', data);
}
export async function getSurveyDetail(data) {
return api('getSurveyDetail', data);
}
// IM 系统消息相关 API
export async function sendConsultRejectedMessage(data) {
return request({
url: '/getYoucanData/im',
data: {
type: 'sendConsultRejectedMessage',
...data
}
});
}

View File

@ -0,0 +1,212 @@
/**
* 会话列表与群组详细信息合并工具
*
* 功能
* 1. conversationList 中提取所有 groupID
* 2. 调用后端 getGroupList 接口获取群组详细信息
* 3. 合并会话数据和患者信息
* 4. 过滤掉后端不存在的会话
*/
import api from "@/utils/api.js"
/**
* 合并会话列表和群组详细信息
*
* @param {Array} conversationList - 前端会话列表
* @param {Object} options - 可选参数
* @param {string} options.corpId - 企业ID可选
* @param {string} options.teamId - 团队ID可选
* @param {string} options.keyword - 搜索关键词可选
* @returns {Promise<Array>} 合并后的会话列表
*/
export async function mergeConversationWithGroupDetails(conversationList, options = {}) {
try {
// 1. 参数校验
if (!Array.isArray(conversationList) || conversationList.length === 0) {
console.log('会话列表为空,无需合并')
return []
}
// 2. 提取所有 groupID
const groupIds = conversationList
.map(conv => conv.groupID);
if (groupIds.length === 0) {
console.log('没有有效的 groupID无需合并')
return []
}
console.log('提取到的 groupID 列表:', groupIds)
// 3. 调用后端接口获取群组详细信息
const requestData = {
groupIds,
...options // 支持传入额外的查询参数corpId, teamId, keyword等
}
const response = await api('getGroupList', requestData)
// 4. 检查响应
if (!response || !response.success) {
console.error('获取群组详细信息失败:', response?.message || '未知错误')
return []
}
const groupDetailsMap = createGroupDetailsMap(response.data?.list || [])
console.log('获取到的群组详细信息数量:', Object.keys(groupDetailsMap).size)
// 5. 合并数据并过滤
const mergedList = conversationList
.map(conversation => mergeConversationData(conversation, groupDetailsMap))
.filter(item => item !== null) // 过滤掉后端不存在的会话
console.log('合并后的会话列表数量:', mergedList.length)
console.log('过滤掉的会话数量:', conversationList.length - mergedList.length)
// 6. 格式化并排序会话列表
const formattedList = mergedList
.map((group) => ({
conversationID: group.conversationID || `GROUP${group.groupID}`,
groupID: group.groupID,
name: group.patientName
? `${group.patientName}的问诊`
: group.name || "问诊群聊",
avatar: group.avatar || "/static/default-avatar.png",
lastMessage: group.lastMessage || "暂无消息",
lastMessageTime: group.lastMessageTime || Date.now(),
unreadCount: group.unreadCount || 0,
doctorId: group.doctorId,
patientName: group.patientName,
patientSex: group.patientSex,
patientAge: group.patientAge,
orderStatus: group.orderStatus,
}))
.sort((a, b) => b.lastMessageTime - a.lastMessageTime)
return formattedList
} catch (error) {
console.error('合并会话列表失败:', error)
// 发生错误时返回空数组,避免影响页面渲染
return []
}
}
/**
* 创建群组详细信息映射表
*
* @param {Array} groupDetailsList - 后端返回的群组详细信息列表
* @returns {Map} groupID -> 群组详细信息的映射
*/
function createGroupDetailsMap(groupDetailsList) {
const map = new Map()
groupDetailsList.forEach(groupDetail => {
if (groupDetail.groupId) {
map.set(groupDetail.groupId, groupDetail)
}
})
return map
}
/**
* 合并单个会话数据
*
* @param {Object} conversation - 前端会话数据
* @param {Map} groupDetailsMap - 群组详细信息映射表
* @returns {Object|null} 合并后的会话数据如果后端不存在则返回 null
*/
function mergeConversationData(conversation, groupDetailsMap) {
const groupDetail = groupDetailsMap.get(conversation.groupID)
// 如果后端没有该群组信息,返回 null会被过滤掉
if (!groupDetail) {
console.log(`会话 ${conversation.groupID} 在后端不存在,已过滤`)
return null
}
// 合并数据
return {
// 保留原有的会话信息
...conversation,
// 合并后端的群组信息
_id: groupDetail._id,
corpId: groupDetail.corpId,
teamId: groupDetail.teamId,
customerId: groupDetail.customerId,
doctorId: groupDetail.doctorId,
patientId: groupDetail.patientId,
orderStatus: groupDetail.orderStatus,
// 合并患者信息(优先使用后端数据)
patientName: groupDetail.patientName || conversation.patientName,
patientSex: groupDetail.patient?.sex,
patientAge: groupDetail.patient?.age,
patientMobile: groupDetail.patient?.mobile,
patientAvatar: groupDetail.patient?.avatar || conversation.avatar,
// 合并团队信息
teamName: groupDetail.team?.name,
teamMemberList: groupDetail.team?.memberList,
teamDescription: groupDetail.team?.description,
// 时间信息
createdAt: groupDetail.createdAt,
updatedAt: groupDetail.updatedAt,
// 更新显示名称(使用后端的患者信息)
name: formatConversationName(groupDetail),
// 更新头像
avatar: groupDetail.patient?.avatar || conversation.avatar || '/static/default-avatar.png'
}
}
/**
* 格式化会话显示名称
*
* @param {Object} groupDetail - 群组详细信息
* @returns {string} 格式化后的名称
*/
function formatConversationName(groupDetail) {
const patientName = groupDetail.patientName || '患者'
const sex = groupDetail.patient?.sex === 1 ? '男' : groupDetail.patient?.sex === 2 ? '女' : ''
const age = groupDetail.patient?.age ? `${groupDetail.patient.age}` : ''
// 拼接名称:患者名 性别 年龄
const nameParts = [patientName, sex, age].filter(part => part)
const displayName = nameParts.join(' ')
return `${displayName}的问诊`
}
/**
* 批量合并会话列表支持分页
*
* @param {Array} conversationList - 前端会话列表
* @param {Object} options - 可选参数
* @param {number} options.batchSize - 每批处理的数量默认50
* @returns {Promise<Array>} 合并后的会话列表
*/
export async function mergeConversationWithGroupDetailsBatch(conversationList, options = {}) {
const { batchSize = 50, ...otherOptions } = options
if (!Array.isArray(conversationList) || conversationList.length === 0) {
return []
}
// 分批处理
const batches = []
for (let i = 0; i < conversationList.length; i += batchSize) {
batches.push(conversationList.slice(i, i + batchSize))
}
console.log(`会话列表分为 ${batches.length} 批处理,每批 ${batchSize}`)
// 并发处理所有批次
const results = await Promise.all(
batches.map(batch => mergeConversationWithGroupDetails(batch, otherOptions))
)
// 合并所有结果
return results.flat()
}

View File

@ -2488,32 +2488,22 @@ class TimChatManager {
return message
}
// 格式化最后一条消息(支持更多消息类型)
formatLastMessage(message) {
try {
switch (message.type) {
case 'TIMTextElem':
return message.payload.text || '[文本消息]'
return message.payload?.text || '[文本消息]'
case 'TIMImageElem':
return '[图片]'
case 'TIMSoundElem':
return '[语音]'
case 'TIMVideoFileElem':
return '[视频]'
case 'TIMFileElem':
return '[文件]'
case 'TIMCustomElem':
try {
const customData = JSON.parse(message.payload.data)
if (customData.messageType === 'symptom') {
return '[病情描述]'
} else if (customData.messageType === 'prescription') {
return '[处方单]'
} else if (customData.messageType === 'refill') {
return '[续方申请]'
} else if (customData.messageType === 'survey') {
return '[问卷调查]'
} else {
return customData.content || '[自定义消息]'
}
} catch (error) {
return '[自定义消息]'
}
return this.formatCustomMessage(message.payload)
default:
return '[未知消息类型]'
}
@ -2523,6 +2513,85 @@ class TimChatManager {
}
}
// 格式化自定义消息
formatCustomMessage(payload) {
try {
if (!payload || !payload.data) {
return '[自定义消息]'
}
const customData = typeof payload.data === 'string'
? JSON.parse(payload.data)
: payload.data
const messageType = customData.messageType || customData.type
const messageTypeMap = {
system_message: '[系统消息]',
symptom: '[病情描述]',
prescription: '[处方单]',
refill: '[续方申请]',
survey: '[问卷调查]',
article: '[文章]',
consult_pending: '患者向团队发起咨询请在1小时内接诊',
consult_rejected: '咨询已被拒绝',
consult_timeout: '咨询已超时自动关闭',
consult_accepted: '已接诊,会话已开始',
consult_ended: '已结束当前会话',
}
return messageTypeMap[messageType] || customData.content || '[自定义消息]'
} catch (error) {
console.error('格式化自定义消息失败:', error)
return '[自定义消息]'
}
}
// 格式化会话数据(用于会话列表)
formatConversationData(conversation) {
try {
const conversationID = conversation.conversationID
const groupName = conversation.groupProfile?.name || ''
const [doctorId, patientName] = groupName.split('|')
const groupID = conversationID.replace('GROUP', '')
// 解析最后一条消息
let lastMessage = '暂无消息'
let lastMessageTime = Date.now()
if (conversation.lastMessage) {
const msg = conversation.lastMessage
lastMessageTime = (msg.lastTime || msg.time || 0) * 1000
lastMessage = this.formatLastMessage(msg)
}
return {
conversationID,
groupID,
name: patientName ? `${patientName}的问诊` : groupName || '问诊群聊',
avatar: '/static/default-avatar.png',
lastMessage,
lastMessageTime,
unreadCount: conversation.unreadCount || 0,
doctorId,
patientName,
}
} catch (error) {
console.error('格式化会话数据失败:', error)
return {
conversationID: conversation.conversationID,
groupID: conversation.conversationID?.replace('GROUP', '') || '',
name: '问诊群聊',
avatar: '/static/default-avatar.png',
lastMessage: '暂无消息',
lastMessageTime: Date.now(),
unreadCount: 0,
doctorId: '',
patientName: '',
}
}
}
getImageUrl(imageFile) {
// 处理 tempFiles 数组格式
if (imageFile?.tempFiles?.length > 0) {
@ -2600,7 +2669,10 @@ class TimChatManager {
// 标记会话为已读
markConversationAsRead(conversationID) {
if (!this.tim || !this.isLoggedIn) return
if (!this.tim || !this.isLoggedIn) {
console.log('⚠️ TIM未初始化或未登录无法标记会话已读');
return;
}
try {
let formattedConversationID = conversationID
@ -2608,18 +2680,22 @@ class TimChatManager {
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)
console.error('标记会话已读失败:', error)
})
} catch (error) {
console.error('标记会话已读异常:', error)
console.error('标记会话已读异常:', error)
}
}

View File

@ -21,6 +21,9 @@ export default (command, mode) => {
case "localhost":
mode = "localhost";
break;
case "ip":
mode = "ip";
break;
default:
mode = "production";
break;