Merge remote-tracking branch 'origin/dev-wdb' into dev-hjf
@ -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
@ -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
|
||||
|
||||
@ -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
@ -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;
|
||||
|
||||
@ -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>
|
||||
@ -23,3 +23,8 @@ const props = defineProps({
|
||||
|
||||
const style = computed(() => `width: ${props.size}rpx;height:${props.size}rpx`)
|
||||
</script>
|
||||
<style>
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@ -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
@ -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 }
|
||||
}
|
||||
@ -27,6 +27,12 @@
|
||||
"env": {
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
"title": "本机ip",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
79
pages.json
@ -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": {
|
||||
|
||||
@ -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>
|
||||
<!-- <button v-if="checked" class="login-btn" type="primary" @click="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-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)) {
|
||||
|
||||
@ -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;
|
||||
async function getTeam(teamId) {
|
||||
if (teamId) {
|
||||
const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
|
||||
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'
|
||||
return toJoinTeam(teamId)
|
||||
} else {
|
||||
await toast(res?.message || '获取团队信息失败')
|
||||
}
|
||||
} else {
|
||||
await toast('获取团队信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
toast(res?.message || '获取团队信息失败')
|
||||
await toast(res?.message || '加入团队失败')
|
||||
}
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url: `/pages/work/profile?type=joinTeam&teamId=${teamId}`
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(options => {
|
||||
teamId.value = options.teamId || '';
|
||||
corpId.value = options.corpId || '';
|
||||
changeTeam({ teamId: teamId.value, corpId: corpId.value });
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
|
||||
201
pages/message/article-detail.vue
Normal 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>
|
||||
@ -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,19 +54,19 @@
|
||||
>
|
||||
<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">
|
||||
<view class="article-footer">
|
||||
<text class="article-date">{{ article.date }}</text>
|
||||
<button
|
||||
class="send-btn"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="sendArticle(article)"
|
||||
@click.stop="sendArticle(article)"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && articleList.length > 0" class="loading-more">
|
||||
<uni-icons type="spinner-cycle" size="20" color="#999" />
|
||||
@ -94,7 +94,9 @@
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="preview-content">
|
||||
<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) {
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/* 问卷卡片样式 */
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const emit = defineEmits(['accept', 'reject']);
|
||||
|
||||
|
||||
@ -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)}`
|
||||
// });
|
||||
|
||||
@ -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'
|
||||
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;
|
||||
}
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
function checkConsultPendingStatus() {
|
||||
// 查找最新的系统消息
|
||||
for (let i = messageList.value.length - 1; i >= 0; i--) {
|
||||
const message = messageList.value[i];
|
||||
|
||||
if (message.type === "TIMCustomElem" && message.payload?.data) {
|
||||
// 获取群组订单状态
|
||||
const fetchGroupOrderStatus = async () => {
|
||||
try {
|
||||
const data = typeof message.payload.data === 'string'
|
||||
? JSON.parse(message.payload.data)
|
||||
: message.payload.data;
|
||||
const result = await api("getGroupListByGroupId", {
|
||||
groupId: groupId.value,
|
||||
});
|
||||
|
||||
// 如果是 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
console.error("获取群组订单状态异常:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果没有找到相关系统消息,隐藏接受组件
|
||||
showConsultAccept.value = false;
|
||||
// 检查是否有待接诊的系统消息
|
||||
function checkConsultPendingStatus() {
|
||||
// 直接获取最新的订单状态
|
||||
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({
|
||||
@ -375,13 +379,34 @@ const initTIMCallbacks = async () => {
|
||||
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);
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -758,18 +782,18 @@ const handleRejectReasonConfirm = async (reason) => {
|
||||
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,
|
||||
|
||||
@ -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管理器未初始化");
|
||||
}
|
||||
// 直接调用getGroupList,它会自动等待SDK就绪
|
||||
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
|
||||
if (existingIndex !== -1) {
|
||||
// 直接更新未读数,避免触发整个对象的响应式更新
|
||||
if (eventData.unreadCount !== undefined) {
|
||||
conversationList.value[existingIndex].unreadCount =
|
||||
eventData.unreadCount;
|
||||
console.log(
|
||||
`已清空会话未读数: ${conversationList.value[existingIndex].name}, unreadCount: ${eventData.unreadCount}`
|
||||
);
|
||||
conversationList.value.unshift(updatedConversation);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("已更新会话:", existingConversation.name);
|
||||
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.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,
|
||||
// 添加新会话
|
||||
conversationList.value.push(conversationData);
|
||||
needSort = true;
|
||||
console.log(`已添加新会话: ${conversationData.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("已添加新会话");
|
||||
}
|
||||
// 只在需要时才排序
|
||||
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>
|
||||
|
||||
@ -3,7 +3,12 @@
|
||||
<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>
|
||||
|
||||
@ -14,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>
|
||||
@ -29,7 +34,10 @@
|
||||
@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>
|
||||
@ -46,10 +54,17 @@
|
||||
>
|
||||
<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();
|
||||
@ -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));
|
||||
}
|
||||
@ -233,28 +253,27 @@ const sendSurvey = async (survey) => {
|
||||
|
||||
// 这里需要从聊天页面获取客户信息
|
||||
// 暂时使用事件传递方式
|
||||
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
@ -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>
|
||||
@ -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(() => {
|
||||
|
||||
25
pages/work/service/contact-service.vue
Normal 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>
|
||||
207
pages/work/team/detail/team-detail.vue
Normal 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>
|
||||
106
pages/work/team/edit/team-edit.vue
Normal 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>
|
||||
151
pages/work/team/invite/invite-patient.vue
Normal 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>
|
||||
57
pages/work/team/invite/invite-teammate.vue
Normal 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>
|
||||
62
pages/work/team/invite/rename-popup.vue
Normal 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>
|
||||
94
pages/work/team/list/team-list.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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: '联系企微客服' }
|
||||
}
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
static/icon/huifangrenwu.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
static/icon/wenjuan.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 685 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 558 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 519 B |
|
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1003 B |
1
static/work/icon-filter.svg
Normal 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 |
1
static/work/icon-filtered.svg
Normal 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 |
@ -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('开始初始化腾讯IM,userID:', 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
@ -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
@ -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);
|
||||
2
uni.scss
@ -15,7 +15,7 @@
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-primary: #0877F1;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
201
uni_modules/Sansnn-uQRCode/LICENSE.md
Normal 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.
|
||||
392
uni_modules/Sansnn-uQRCode/README.md
Normal 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发布了配套的可视化设计器,可根据自己喜好在设计器中设计二维码样式,一键生成配置代码复制到项目中,详情请在微信小程序搜索“柚子二维码”,或扫描下方小程序码体验。
|
||||
|
||||

|
||||
|
||||
## 设计器模板示例
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 快速上手
|
||||
|
||||
> 在`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,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAC3xJREFUeJztnd1vFNcZxodSJ3y3EL7SYIQwu15wI5FSAkqVkISKgEkuSIEC6127RrloL9r8D4n5UFUZp/9C24A/okqUOzCmSdoohQtkXIkiRS1VC7YQF41Kbe/unL7PzHt2z45ndnZmd1l75hzrSSwzMzvn+c15z8ee3dcwdIlkWaRlqSnF62a+4dDiiMtZ36cKyc183NQ3WS2sZ2IqWX/phwTWEDhuEKT5S0hLSctJK1grWasiLllPWe9l7MUSowTJDU7oopKVICSEZXwz3yKtJj1HWkdaT9pA2hgTbeA6r2MPVrMnEpCEI8HU1FpUGC18cbQEPB1r+Ea+Q2olbSFtJbWREqxkxCXr2cZ1hwebSM+zN2vYq+XsXYtRQ2uRJ8hWgaa4kl8ET0Ur30SK9F3STtL3SLtJL5P2kPZGXHu4rru57vCgg9TO3mxir1azd0uNUmuRUALBWKzAAOm1pBcM+4nYwTeBG3uNtJ/0FukQqZP0NuudiErWr5PrfID0JulVwwb1Enu0lT1byx6qUKpqJWoH3qLAQIzcbNhNFU/CKwzhMOld0o9JaVKW1EP6CamXdDqi6uU69nCdUffjpCPsyZvs0U72bDN7KKHI8OULRcIAQcQ9NDXQRYhCeNpF2ocXPXjw4M8uX748eP/+/b9NT08/ETEv8ABekCcXDx069FMGs489SzGUtezpEqPK0KWGKnRGiH8vMGVc+I1UKnXy3r17N5ttwHwvd+/e/bKjo+Mkt5bvG3bfAi/RD69gj2Ur8YQhO/Il3LzQKbVx09t35MiR9x4/fvzvZld2oRTy6l8HDhxAiHvdsPsVeInhMobGSw2fvkTtO5YxSYQqdE6Ih4cnJiY+b3YlF1q5ffv2p4Y9APiBYY/CELqe4wj0TKWwpYYrxLn1TBSjqf1Hjx79eYGK3w1sGz4VK/kVeHbs2LFfkIc/ZC/b2FtEoGcrhS01XKFJYdKHzghD28NjY2N/0BDCwSHvrnAreYU9RV/ybUfY8gSyVAlXmPRhnvHuw4cP/65hhIPy4MGDf5CHPzLsUdeLHLbWVAKi9h/LOcZtMezOHPONE25D22ZXfr7KWeAdeXiSw9ZO9nYte91iuPQjEgj6DwzJMInBLBNDXczA07p1hAeCQh52sZe7lH5EDn99geDgbYa9ToOlgayGURsU8rCbvdzN3voCUUdYmH9gJRPrMphx9mggNQPpYS/3sLcb2GvXCaITyEYFCEYHvRpIzUB62UsJZGO1QFbxwVgu2auB1B3IXvZ2I3sdGAiWm09rIDUDOc1eaiAaSEWlHQp7ntc1Kh0XRlEHMtQ1V2HPm3N+uvJxYRQSyoIB0j6Ymash/0onyBy3c5MkeUzS45haFFEg9pOLCk6LgsgJs0xPxKxIDbu1lNITn7l2hs7N0U/p/Bn6vf/OkEgM28dcuDMy59rhlbfuKzmUCdaSFxoQVNZZUHk/INlrZ+mo8tV/k34GCMI2BvLRnU/mXDt8MQlHLs5AMhWBdI+e00CeJpDtw9lQQD7SQBoBJCdSQ+FaSHVA5r6m/xExB6KOtBIj6boBMemUWTNntUIvTZP1pmnOuboG0gAgOKebBgQpeu3UYNZVHRd7ilA0kAYDwTHZ0TPWtXBdN7XTuTlqRc4zNZAGAelmIF73ZwPJayBOICUQ9evUqwYiNBAFCM3U6d+bBSTlASSngTSrhaTFZ1Pj4k+TE+LPk39lTYhPJ8et9bEYAslb85BmAYESCJmkJC9YQok4LC66AUGsbqfhpysQa42ri0ZKtY6yqrxPfj0oEd3l98pA/idmRM+1cyJ7vc9Tv/ziY5rgFQhJ6fzq5iGmOP+X34nM9Q+L18qQuki7fv9e8f4y1z4Q6bEPRfqPfSJ9g/597Az9rY+um41fyMKELFeA2bbhc1UQecAwTQtCECA4JmedW37tWfpv1/UPrPtDuHwi/kvwgM8Wjp+hR2X7pTgC4Se5UjGLP+V/81/LkhDKC/6GloJ7w7B31pwph02/YrJovUkVNyDVFJNNDA7EvRSB0HlJC0hOOJcY8zRZTGkg7sVUJP9gAxkuARkPCGS0z+q4k4MAMivKgJgxATLDz3mYH+eZCEMDAMKGDYyPVH0tvBUMIEkJhPqLvBBlr5WnMLb9UoRHWRjb908Mi4GJESvU1KZhC8YJ6pgTDCRNIylce8DnXBxzge7jjSvv88QvI341fkn00/UusHD9/vFhe6YePSAlJZRxfs0aknMFBXzA8+VWn4TrvYar44ICUvd9U04goc4PvyFuAQNJW+HhghU2Pqld1IGjz0CYkrsM0zRqCnc995DYf2eQW3TwXYzzHEjtoyy30uhdJ7Fd7Q1vmd4GVCzzBYjeBsRFA4kwEGzVyftMGPPFlaxgi4s4vGD6Xd1l4miaYpomhqUN17Hp1E1rHQlbdbKjZ0W3m66fE+e//K29ahsQCGCcvfUbmpWfcb+2i3AfOB7L720jJwPWdcED4XcMBzOe23QgLJXbS+gqyiqACNMyN1FhG5Cr6Pi2EfcJY2yAVLoG1p0KjnPr+RZuvRURIN4fLfMC4jfs1UBqAeK5tNFlvfWqgTxFIDsuZSt+tKyHOli87ZoXpbdhc9YnqJT3QzSQ+gCBaV8U90O5a+irMWolNPLB5gP8n0JYF+n1K+8XW5IGUicg1ZTPpyZEu/WhHu9VWw2kKUBcOv0KQDAl7L16TrQPZQKqy9px0jYS7jPr8QEyZzPdqcothF5umrDMWgshwX7+Y20D6o7f0ollnB+QyQnryW0LCoShlJZdqhP2is0QyFiuZeG7TnPWNrWCpz6bvE1AsmRQt/UBUfyOkJL0AVJLwagudkBMq+Kz4sWPs9b+3hSMdihFELJXz1trXnkIXx5g5kUuVxAD40MaSG1A8qIsNNDPDJmMz/p5rTfh/OzVPguCiaVhbCnFulbBFL8eL98G5Ni9FbogzM2aCFmnot2pP6HIPGt9IkRqRnxtPqF/6/asNBb4eq7iqzVmLJOKn6Cl3/uphST4Kb5AcMo/YVuoQXnxNb3ijsFgLWOBACk9ZUk5rEQ/MIw+ICO2Y9lkxP989BkpGvWkBruLn6BNKMNf/J4sqqs2DWWs19kazeV3RRW38TTgvCZJA5lnWjhAYiINZJ6pkUD018TWB0jor4nVX6TcWCCBv0hZf9V4Y4D0GAG/alx/GX9jgQT+Mn6drqJBMBiIM13FumqA6IQuDQDikdBFJgZzTegiociUR8hfWJbyaGpq6p+6lQSHgRIm5ZEKRCYFQ9bjYlKwGzdu6KRgIWCguCQFQ8K1qpKCqSOt9dyPICHi/uPHj+u0eQEgyALPkHLQmJs2Dx77ps2rlFiy89atW9d870CXsnLz5s1RpXUETiyphi2ZehWtxEq9unnz5mOPHj263+xKLpQyOTn5VWtrKzJp7zPKU6/KrNG+abzVsOWanLijo+OETk7sX+AREjkb7smJZevwDFfOsAVyiG9e6bs7OX33RZ2+2y5K+u5LnL6706hT+m61L1ET3Lca7gnukdRdJ7ivnOC+1QiZ4F6FIkOXhAK6aHKIg+joMWLAkPg1vgHMQrE0gCfjbdY7EZWsXyfX+QB78Kphr1W9xB5tZc/WKjDgqW/f4SxqBy+hoKkh/qGj38QvhriIySOeBADCOs3LfFN7I649XNfdXHd40MGebGWP4NVq9k6F4Ruq3IraUtDEEPfQGYE0wGAsjckjmuMWvgm0ngQrGXHJerZx3bewF8+zN2vYK3j2rBEwTLmVRUY5FNlaAAbzFFDHjB5PAMbV6/hG8FRsjIk2cJ3XsQer2ZOV7NESo9QqVBihgMiidvQSTItRgoOmiKdgBWsla1XEJesp672MvZAQWowSCBmiagKhlkUOqXAkIAkpjpL1l344IdQVhrM4X0SFpGpxxOWsr5cvTSleNxM36RK18n+GJEwNAYal3QAAAABJRU5ErkJggg==';
|
||||
// 调用制作二维码方法
|
||||
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,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAC3xJREFUeJztnd1vFNcZxodSJ3y3EL7SYIQwu15wI5FSAkqVkISKgEkuSIEC6127RrloL9r8D4n5UFUZp/9C24A/okqUOzCmSdoohQtkXIkiRS1VC7YQF41Kbe/unL7PzHt2z45ndnZmd1l75hzrSSwzMzvn+c15z8ee3dcwdIlkWaRlqSnF62a+4dDiiMtZ36cKyc183NQ3WS2sZ2IqWX/phwTWEDhuEKT5S0hLSctJK1grWasiLllPWe9l7MUSowTJDU7oopKVICSEZXwz3yKtJj1HWkdaT9pA2hgTbeA6r2MPVrMnEpCEI8HU1FpUGC18cbQEPB1r+Ea+Q2olbSFtJbWREqxkxCXr2cZ1hwebSM+zN2vYq+XsXYtRQ2uRJ8hWgaa4kl8ET0Ur30SK9F3STtL3SLtJL5P2kPZGXHu4rru57vCgg9TO3mxir1azd0uNUmuRUALBWKzAAOm1pBcM+4nYwTeBG3uNtJ/0FukQqZP0NuudiErWr5PrfID0JulVwwb1Enu0lT1byx6qUKpqJWoH3qLAQIzcbNhNFU/CKwzhMOld0o9JaVKW1EP6CamXdDqi6uU69nCdUffjpCPsyZvs0U72bDN7KKHI8OULRcIAQcQ9NDXQRYhCeNpF2ocXPXjw4M8uX748eP/+/b9NT08/ETEv8ABekCcXDx069FMGs489SzGUtezpEqPK0KWGKnRGiH8vMGVc+I1UKnXy3r17N5ttwHwvd+/e/bKjo+Mkt5bvG3bfAi/RD69gj2Ur8YQhO/Il3LzQKbVx09t35MiR9x4/fvzvZld2oRTy6l8HDhxAiHvdsPsVeInhMobGSw2fvkTtO5YxSYQqdE6Ih4cnJiY+b3YlF1q5ffv2p4Y9APiBYY/CELqe4wj0TKWwpYYrxLn1TBSjqf1Hjx79eYGK3w1sGz4VK/kVeHbs2LFfkIc/ZC/b2FtEoGcrhS01XKFJYdKHzghD28NjY2N/0BDCwSHvrnAreYU9RV/ybUfY8gSyVAlXmPRhnvHuw4cP/65hhIPy4MGDf5CHPzLsUdeLHLbWVAKi9h/LOcZtMezOHPONE25D22ZXfr7KWeAdeXiSw9ZO9nYte91iuPQjEgj6DwzJMInBLBNDXczA07p1hAeCQh52sZe7lH5EDn99geDgbYa9ToOlgayGURsU8rCbvdzN3voCUUdYmH9gJRPrMphx9mggNQPpYS/3sLcb2GvXCaITyEYFCEYHvRpIzUB62UsJZGO1QFbxwVgu2auB1B3IXvZ2I3sdGAiWm09rIDUDOc1eaiAaSEWlHQp7ntc1Kh0XRlEHMtQ1V2HPm3N+uvJxYRQSyoIB0j6Ymash/0onyBy3c5MkeUzS45haFFEg9pOLCk6LgsgJs0xPxKxIDbu1lNITn7l2hs7N0U/p/Bn6vf/OkEgM28dcuDMy59rhlbfuKzmUCdaSFxoQVNZZUHk/INlrZ+mo8tV/k34GCMI2BvLRnU/mXDt8MQlHLs5AMhWBdI+e00CeJpDtw9lQQD7SQBoBJCdSQ+FaSHVA5r6m/xExB6KOtBIj6boBMemUWTNntUIvTZP1pmnOuboG0gAgOKebBgQpeu3UYNZVHRd7ilA0kAYDwTHZ0TPWtXBdN7XTuTlqRc4zNZAGAelmIF73ZwPJayBOICUQ9evUqwYiNBAFCM3U6d+bBSTlASSngTSrhaTFZ1Pj4k+TE+LPk39lTYhPJ8et9bEYAslb85BmAYESCJmkJC9YQok4LC66AUGsbqfhpysQa42ri0ZKtY6yqrxPfj0oEd3l98pA/idmRM+1cyJ7vc9Tv/ziY5rgFQhJ6fzq5iGmOP+X34nM9Q+L18qQuki7fv9e8f4y1z4Q6bEPRfqPfSJ9g/597Az9rY+um41fyMKELFeA2bbhc1UQecAwTQtCECA4JmedW37tWfpv1/UPrPtDuHwi/kvwgM8Wjp+hR2X7pTgC4Se5UjGLP+V/81/LkhDKC/6GloJ7w7B31pwph02/YrJovUkVNyDVFJNNDA7EvRSB0HlJC0hOOJcY8zRZTGkg7sVUJP9gAxkuARkPCGS0z+q4k4MAMivKgJgxATLDz3mYH+eZCEMDAMKGDYyPVH0tvBUMIEkJhPqLvBBlr5WnMLb9UoRHWRjb908Mi4GJESvU1KZhC8YJ6pgTDCRNIylce8DnXBxzge7jjSvv88QvI341fkn00/UusHD9/vFhe6YePSAlJZRxfs0aknMFBXzA8+VWn4TrvYar44ICUvd9U04goc4PvyFuAQNJW+HhghU2Pqld1IGjz0CYkrsM0zRqCnc995DYf2eQW3TwXYzzHEjtoyy30uhdJ7Fd7Q1vmd4GVCzzBYjeBsRFA4kwEGzVyftMGPPFlaxgi4s4vGD6Xd1l4miaYpomhqUN17Hp1E1rHQlbdbKjZ0W3m66fE+e//K29ahsQCGCcvfUbmpWfcb+2i3AfOB7L720jJwPWdcED4XcMBzOe23QgLJXbS+gqyiqACNMyN1FhG5Cr6Pi2EfcJY2yAVLoG1p0KjnPr+RZuvRURIN4fLfMC4jfs1UBqAeK5tNFlvfWqgTxFIDsuZSt+tKyHOli87ZoXpbdhc9YnqJT3QzSQ+gCBaV8U90O5a+irMWolNPLB5gP8n0JYF+n1K+8XW5IGUicg1ZTPpyZEu/WhHu9VWw2kKUBcOv0KQDAl7L16TrQPZQKqy9px0jYS7jPr8QEyZzPdqcothF5umrDMWgshwX7+Y20D6o7f0ollnB+QyQnryW0LCoShlJZdqhP2is0QyFiuZeG7TnPWNrWCpz6bvE1AsmRQt/UBUfyOkJL0AVJLwagudkBMq+Kz4sWPs9b+3hSMdihFELJXz1trXnkIXx5g5kUuVxAD40MaSG1A8qIsNNDPDJmMz/p5rTfh/OzVPguCiaVhbCnFulbBFL8eL98G5Ni9FbogzM2aCFmnot2pP6HIPGt9IkRqRnxtPqF/6/asNBb4eq7iqzVmLJOKn6Cl3/uphST4Kb5AcMo/YVuoQXnxNb3ijsFgLWOBACk9ZUk5rEQ/MIw+ICO2Y9lkxP989BkpGvWkBruLn6BNKMNf/J4sqqs2DWWs19kazeV3RRW38TTgvCZJA5lnWjhAYiINZJ6pkUD018TWB0jor4nVX6TcWCCBv0hZf9V4Y4D0GAG/alx/GX9jgQT+Mn6drqJBMBiIM13FumqA6IQuDQDikdBFJgZzTegiociUR8hfWJbyaGpq6p+6lQSHgRIm5ZEKRCYFQ9bjYlKwGzdu6KRgIWCguCQFQ8K1qpKCqSOt9dyPICHi/uPHj+u0eQEgyALPkHLQmJs2Dx77ps2rlFiy89atW9d870CXsnLz5s1RpXUETiyphi2ZehWtxEq9unnz5mOPHj263+xKLpQyOTn5VWtrKzJp7zPKU6/KrNG+abzVsOWanLijo+OETk7sX+AREjkb7smJZevwDFfOsAVyiG9e6bs7OX33RZ2+2y5K+u5LnL6706hT+m61L1ET3Lca7gnukdRdJ7ivnOC+1QiZ4F6FIkOXhAK6aHKIg+joMWLAkPg1vgHMQrE0gCfjbdY7EZWsXyfX+QB78Kphr1W9xB5tZc/WKjDgqW/f4SxqBy+hoKkh/qGj38QvhriIySOeBADCOs3LfFN7I649XNfdXHd40MGebGWP4NVq9k6F4Ruq3IraUtDEEPfQGYE0wGAsjckjmuMWvgm0ngQrGXHJerZx3bewF8+zN2vYK3j2rBEwTLmVRUY5FNlaAAbzFFDHjB5PAMbV6/hG8FRsjIk2cJ3XsQer2ZOV7NESo9QqVBihgMiidvQSTItRgoOmiKdgBWsla1XEJesp672MvZAQWowSCBmiagKhlkUOqXAkIAkpjpL1l344IdQVhrM4X0SFpGpxxOWsr5cvTSleNxM36RK18n+GJEwNAYal3QAAAABJRU5ErkJggg==';
|
||||
// 调用制作二维码方法
|
||||
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)。
|
||||
12
uni_modules/Sansnn-uQRCode/changelog.md
Normal file
@ -0,0 +1,12 @@
|
||||
## 4.0.6(2022-12-12)
|
||||
修复`getDrawModules`,第一次获取结果正常,后续获取`tile`模块不存在的问题;
|
||||
修复安卓type:normal因Canvas API使用了小数或为0的参数导致生成异常的问题(注:安卓非2d Canvas部分API参数不支持携带小数,部分API参数必须大于0)。
|
||||
## 4.0.1(2022-11-28)
|
||||
优化组件loading属性的表现;
|
||||
新增组件type选项normal,以便于在某些条件编译初始为type=2d时还可以选择使用非2d组件类型;
|
||||
修复组件条件编译在其他编辑器语法提示报错;
|
||||
修复原生对es5的支持。
|
||||
## 4.0.0(2022-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)进行修改。
|
||||
1
uni_modules/Sansnn-uQRCode/common/cache.js
Normal file
@ -0,0 +1 @@
|
||||
export const cacheImageList = [];
|
||||
41
uni_modules/Sansnn-uQRCode/common/queue.js
Normal 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();
|
||||
3
uni_modules/Sansnn-uQRCode/common/types/cache.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module '*/common/cache' {
|
||||
export const cacheImageList: Array;
|
||||
}
|
||||
4
uni_modules/Sansnn-uQRCode/common/types/queue.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*/common/queue' {
|
||||
export const queueDraw: any;
|
||||
export const queueLoadImage: any;
|
||||
}
|
||||
1131
uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue
Normal file
1131
uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue
Normal file
241
uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js
Normal 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;
|
||||
@ -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;
|
||||
@ -0,0 +1,8 @@
|
||||
class FillStylePattern {
|
||||
constructor(img, pattern) {
|
||||
this._style = pattern;
|
||||
this._img = img;
|
||||
}
|
||||
}
|
||||
|
||||
export default FillStylePattern;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
export default class WebGLActiveInfo {
|
||||
className = 'WebGLActiveInfo';
|
||||
|
||||
constructor({
|
||||
type, name, size
|
||||
}) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
export default class WebGLShaderPrecisionFormat {
|
||||
className = 'WebGLShaderPrecisionFormat';
|
||||
|
||||
constructor({
|
||||
rangeMin, rangeMax, precision
|
||||
}) {
|
||||
this.rangeMin = rangeMin;
|
||||
this.rangeMax = rangeMax;
|
||||
this.precision = precision;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export function getTransferedObjectUUID(name, id) {
|
||||
return `${name.toLowerCase()}-${id}`;
|
||||
}
|
||||
74
uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js
vendored
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
96
uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js
vendored
Normal 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;
|
||||
24
uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js
vendored
Normal 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;
|
||||
}
|
||||
39
uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js
Normal 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;
|
||||
};
|
||||
34
uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js
Normal file
79
uni_modules/Sansnn-uQRCode/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
utils/api.js
@ -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: {
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
212
utils/conversation-merger.js
Normal 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()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,9 @@ export default (command, mode) => {
|
||||
case "localhost":
|
||||
mode = "localhost";
|
||||
break;
|
||||
case "ip":
|
||||
mode = "ip";
|
||||
break;
|
||||
default:
|
||||
mode = "production";
|
||||
break;
|
||||
|
||||