This commit is contained in:
Your Name
2026-03-06 14:27:24 +08:00
parent 9a1b67ca18
commit 52bb80e2f1
134 changed files with 5337 additions and 401 deletions
+23
View File
@@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
TUICallKit
+16
View File
@@ -0,0 +1,16 @@
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}
+47
View File
@@ -0,0 +1,47 @@
<script setup>
import { onLaunch, onShow, onHide, onError } from '@dcloudio/uni-app'
import { getCurrentInstance } from 'vue'
// 2. 获取组件实例,通过 proxy 访问全局属性
const { proxy } = getCurrentInstance()
const globalData = {
SDKAppID: 0,
userID: '',
userSig: ''
};
onLaunch(() => {
console.log('---------进入onMounted')
uni.login({
provider: 'weixin',
success: function (loginRes) {
console.log('xx');
console.log(loginRes.code);
wxcode(loginRes.code)
// 获取用户信息
}
});
})
const wxcode = async (code) => {
try {
// 通过 proxy 调用全局的 apiUrl
const res = await proxy.apiUrl({
url: '/api/login/mnpLogin',
method: 'POST',
data: {
code: code
},
}, false) // 不显示加载中
uni.setStorageSync('token', res.data.token)
uni.setStorageSync('userData', res.data)
console.log('请求结果:', res.data)
} catch (err) {
}
}
</script>
+137
View File
@@ -0,0 +1,137 @@
## 简介
本 demo 演示了如何在 uni-app 项目中集成 [TUICallKit](https://www.npmjs.com/package/@trtc/calls-uikit-wx-uniapp) 音视频通话组件。
## 环境准备
- 微信 App iOS 最低版本要求:8.0.40
- 微信 App Android 最低版本要求:8.0.40
- 小程序基础库最低版本要求:2.10.0
- 由于小程序测试号不具备 <live-pusher> 和 <live-player> 的使用权限,请使用企业小程序账号申请相关权限进行开发
- 由于微信开发者工具不支持原生组件(即 <live-pusher> 和 <live-player> 标签),需要在真机上进行运行体验
## 快速跑通
第一步:下载源码,编译运行
1. 克隆或者直接下载此仓库源码。
```
git clone https://github.com/tencentyun/TUICallKit.git
```
2. 进入 demo 目录
```
cd ./TUICallKit/uni-app/TUICallKit-Miniprogram/TUICallKit-Vue3
```
3. 安装依赖
```
npm install
```
mac端
```
mkdir -p ./TUICallKit && cp -r node_modules/@trtc/calls-uikit-wx-uniapp/ ./TUICallKit && cp node_modules/@trtc/call-engine-lite-wx/RTCCallEngine.wasm.br ./static
```
windows端
```
xcopy node_modules\@trtc\calls-uikit-wx-uniapp\ .\TUICallKit /i /e
xcopy node_modules\@trtc\call-engine-lite-wx\RTCCallEngine.wasm.br .\static
```
4. HBuilder 中导入项目
<img src="https://web.sdk.qcloud.com/component/trtccalling/images/miniProgram/hbuilder-vue.png" width="400" align="middle" />
5. 修改 `./TUICallKit/uni-app/TUICallKit-Miniprogram/TUICallKit-Vue3/debug/GenerateTestUserSig-es.js` 文件 的 SDKAPPID 以及 SECRETKEY(阅读文末 [开通服务](#开通服务))
<img src="https://qcloudimg.tencent-cloud.cn/raw/49931d68084b2d79f0f69f278894999b.png" width="400" />
6. 运行到【微信开发者工具】,勾选 **【运行时是否压缩代码】**
<img src="https://web.sdk.qcloud.com/component/trtccalling/images/miniProgram/exportWechatTool.png" width="400" align="middle" />
7. 项目导入到微信开发者工具,目录如下图:
<img src="https://qcloudimg.tencent-cloud.cn/raw/a79cd64ac5be8ee099f2d802f1c847c6.png" width="150" align="middle" />
## 示例体验
> **TipsTUICallKit 通话体验,至少需要两台设备,用户 A、B 代表使用不同的设备。**
**用户 AuserId111**
- 步骤 1:在欢迎页,输入用户名(<font color=red>请确保用户名唯一性,不能与其他用户重复</font>)登录,比如:111。
- 步骤 2:根据不同的场景&业务需求,进入不同的场景界面,比如:视频通话。
- 步骤 3:输入要拨打的用户 B 的 userId(例如:222),点击搜索,然后点击呼叫。
| 步骤1 | 步骤2 | 步骤3 |
| :-: | :-: | :-: |
|<img src="https://qcloudimg.tencent-cloud.cn/raw/13bf844da5636f3640da05020800fff9.jpg" width="240"/>|<img src="https://qcloudimg.tencent-cloud.cn/raw/ceed4039c6ce8f9b8ff48deb89199b6e.jpg" width="240">|<img src="https://qcloudimg.tencent-cloud.cn/raw/dc1984904c9790c8a3355ff1c5dada50.jpg" width="240"/>
**用户 BuserId222**
- 步骤 1:在欢迎页,输入用户名(<font color=red>请确保用户名唯一性,不能与其他用户重复</font>)登录,比如:222。
- 步骤 2:进入主页,等待接听来电即可。
## 接入指引
### 步骤一:开通小程序权限
由于 TUICallKit 所使用的小程序标签有更苛刻的权限要求,因此集成 TUICallKit 的第一步就是要开通小程序的类目和标签使用权限,**否则无法使用**,这包括如下步骤:
- 小程序推拉流标签不支持个人小程序,只支持企业类小程序。需要在 [注册](https://developers.weixin.qq.com/community/business/doc/000200772f81508894e94ec965180d) 时填写主体类型为企业,如下图所示:
<img width="480" height="480" src="https://main.qcloudimg.com/raw/a30f04a8983066fb9fdf179229d3ee31.png">
- 小程序推拉流标签使用权限暂时只开放给有限 [类目](https://developers.weixin.qq.com/miniprogram/dev/component/live-pusher.html)。
- 符合类目要求的小程序,需要在 **[微信公众平台](https://mp.weixin.qq.com/)** > **开发** > **开发管理** > **接口设置**中自助开通该组件权限,如下图所示:
<img width="480" height="360" src="https://main.qcloudimg.com/raw/dc6d3c9102bd81443cb27b9810c8e981.png">
### 步骤二:在小程序控制台配置域名
在 **[微信公众平台](https://mp.weixin.qq.com/)** > **开发** > **开发管理** > **开发设置** > **服务器域名**中设置 **request 合法域名** 和 **socket 合法域名**,如下图所示:
- **request 合法域名**
```javascript
https://official.opensso.tencent-cloud.com
https://yun.tim.qq.com
https://cloud.tencent.com
https://webim.tim.qq.com
https://query.tencent-cloud.com
https://web.sdk.qcloud.com
```
- **socket 合法域名**
```javascript
wss://wss.im.qcloud.com
wss://wss.tim.qq.com
```
<img width="480" height="360" src="https://qcloudimg.tencent-cloud.cn/raw/a79ca9726309bb1fdabb9ef8961ce147.png">
### 步骤三:开通服务
TUICallKit 是基于腾讯云 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440) 和 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 两项付费 PaaS 服务构建出的音视频通信组件。您可以按照如下步骤开通相关的服务并体验 7 天的免费试用服务:
1. 登录到 [即时通信 IM 控制台](https://console.cloud.tencent.com/im),单击**创建新应用**,在弹出的对话框中输入您的应用名称,并单击**确定**。
<img width="360" height="240" src="https://qcloudimg.tencent-cloud.cn/raw/1105c3c339be4f71d72800fe2839b113.png">
2. 单击刚刚创建出的应用,进入**基本配置**页面,并在页面的右下角找到**开通腾讯实时音视频服务**功能区,单击**免费体验**即可开通 TUICallKit 的 7 天免费试用服务。如果需要正式应用上线,可以单击 [**前往加购**](https://buy.cloud.tencent.com/avc) 即可进入购买页面。
<img width="480" src="https://user-images.githubusercontent.com/72854065/205876830-6c8f119e-8d3c-4f1e-b8d3-0a21788fc47a.png">
> IM 音视频通话能力针对不同的业务需求提供了差异化的付费版本供您选择,您可以在 [IM 购买页](https://buy.cloud.tencent.com/avc) 了解包含功能并选购您适合的版本。
3. 在同一页面找到 **SDKAppID** 和**密钥**并记录下来
- SDKAppID:IM 的应用 ID,用于业务隔离,即不同的 SDKAppID 的通话彼此不能互通。
- SecretkeyIM 的应用密钥,需要和 SDKAppID 配对使用,用于签出合法使用 IM 服务的鉴权用票据 UserSig。
<img width="480" src="https://qcloudimg.tencent-cloud.cn/raw/e435332cda8d9ec7fea21bd95f7a0cba.png">
> 即日起至2022年10月01日0点前,购买音视频通话能力包基础版可获赠解锁相同有效期的小程序端通话功能授权。在活动结束前购买的音视频通话能力包在有效期内不受活动结束影响仍可使用小程序通话功能,活动结束后的新购或续期需选择体验版、进阶版、尊享版来获取小程序通话功能授权,基础版亦可单独加购小程序功能授权进行解锁。
> 单击**免费体验**以后,部分之前使用过 [实时音视频 TRTC]
(https://cloud.tencent.com/document/product/647/16788) 服务的用户会提示:
```
[-100013]:TRTC service is suspended. Please check if the package balance is 0 or the Tencent Cloud accountis in arrears
```
因为新的 IM 音视频通话能力是整合了腾讯云[实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 和 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440) 两个基础的 PaaS 服务,所以当 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 的免费额度(10000分钟)已经过期或者耗尽,就会导致开通此项服务失败,这里您可以单击 [TRTC 控制台](https://console.cloud.tencent.com/trtc/app),找到对应 SDKAppID 的应用管理页,示例如图,开通后付费功能后,再次**启用应用**即可正常体验音视频通话能力。
<img width=600px src="https://qcloudimg.tencent-cloud.cn/raw/f74a13a7170cf8894195a1cae6c2f153.png" />
## 附录
- 如果您想要了解TUiCallKit,请阅读 [组件介绍 TUICallKit](https://cloud.tencent.com/document/product/647/78742)
- 如果您想把我们的功能直接嵌入到您的项目中,请阅读 [快速接入 TUICallKit](https://cloud.tencent.com/document/product/647/78912)
- 如果您想要了解详细 API ,请阅读 [ API 概览](https://cloud.tencent.com/document/product/647/78759)
- 如果你遇到了困难,可以先参阅 [常见问题](https://cloud.tencent.com/document/product/647/78912)
- 如果发现了示例代码的 bug,欢迎提交 issue;
@@ -0,0 +1,63 @@
import LibGenerateTestUserSig from './lib-generate-test-usersig-es.min.js';
/**
* 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
*
* 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
* 它是腾讯云用于区分客户的唯一标识。
*/
let SDKAPPID = 1600127710;
/**
* 签名过期时间,建议不要设置的过短
* <p>
* 时间单位:秒
* 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
*/
const EXPIRETIME = 604800;
/**
* 计算签名用的加密密钥,获取步骤如下:
*
* step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
* step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
* step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
*
* 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
* 文档:https://cloud.tencent.com/document/product/647/17275#Server
*/
let SECRETKEY = '8a6b3ce533b0e46b9d6e17d5c77bac240bc0ccdce4e3db93a0d6a1e55160c73d';
/*
* Module: GenerateTestUserSig
*
* Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
* 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
*
* Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
*
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
*
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
*
* Referencehttps://cloud.tencent.com/document/product/647/17275#Server
*/
export function genTestUserSig({ userID, SDKAppID, SecretKey }) {
if (SDKAppID) SDKAPPID = SDKAppID;
if (SecretKey) SECRETKEY = SecretKey;
const generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
const userSig = generator.genTestUserSig(userID);
return {
SDKAppID: SDKAPPID,
userSig,
};
}
export default genTestUserSig;
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
Vue.prototype.$url = 'http://www.d.com'
Vue.prototype.apiUrl =function apiurl(promise, Loading = true){
let token=uni.getStorageSync('token')
promise.header={
token:token,
'content-type':'application/x-www-form-urlencoded',
}
let header = {}
if (promise.header) {
header = promise.header
}
let data = {}
if (promise.data) {
data = promise.data
}
let url=Vue.prototype.$url+promise.url
return new Promise(function(resolve, reject) {
uni.request({
url: url,
data: data,
timeout: 15000,
method: promise.method ? promise.method : 'POST',
header: header,
sslVerify: false,
success: (res) => {
const data=res.data
//-1检测登陆失效删除登陆状态
resolve(data);
},
fail(e) {
reject(e)
},
complete(e) {
//console.log('complete', e)
}
});
});
}
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
// 定义全局基础URL
const baseUrl = 'http://www.d.com'
// 封装Vue3版本的apiUrl方法
function apiUrl(promise, Loading = true) {
// 获取token
const token = uni.getStorageSync('token')
// 合并header(修复原代码直接覆盖的问题)
const header = {
token: token,
'content-type': 'application/x-www-form-urlencoded',
// 合并用户传入的header,用户传入的会覆盖默认值
...(promise.header || {})
}
// 处理请求数据和URL
const data = promise.data || {}
const url = baseUrl + (promise.url || '')
return new Promise((resolve, reject) => {
// 可选:根据Loading参数显示加载中
if (Loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
uni.request({
url: url,
data: data,
timeout: 15000,
method: promise.method || 'POST', // 简化三元表达式
header: header,
sslVerify: false,
success: (res) => {
const data = res.data
// 处理登录失效逻辑(补充完整)
if (data.code === -1) {
uni.removeStorageSync('token') // 删除token
uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' })
// 可选:跳转到登录页
// uni.redirectTo({ url: '/pages/login/login' })
}
resolve(data)
},
fail: (e) => {
uni.showToast({ title: '请求失败,请重试', icon: 'none' })
reject(e)
},
complete: (e) => {
// 关闭加载中
if (Loading) {
uni.hideLoading()
}
// console.log('complete', e)
}
})
})
}
export function createApp() {
const app = createSSRApp(App)
// Vue3挂载全局属性的方式
app.config.globalProperties.$url = baseUrl
app.config.globalProperties.apiUrl = apiUrl
return {
app
}
}
// #endif
+72
View File
@@ -0,0 +1,72 @@
{
"name" : "uniapp-callkit-vue3",
"appid" : "__UNI__5205699",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx6b829b4ffce455b7",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}
+100
View File
@@ -0,0 +1,100 @@
{
"name": "tuicallkit-vue3",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tuicallkit-vue3",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@trtc/calls-uikit-wx-uniapp": "^4.1.0"
}
},
"node_modules/@tencentcloud/lite-chat": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@tencentcloud/lite-chat/-/lite-chat-1.6.9.tgz",
"integrity": "sha512-Pg7Z3YFI+jbv3iQzCZQHlWxBbaTZO2yJ8lFT1fURh5R7JJZz+c8h7Pe/z5NZNdmnsZBKes/s6s+hRXcfwMSbnA==",
"license": "ISC"
},
"node_modules/@tencentcloud/trtc-cloud-wx": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tencentcloud/trtc-cloud-wx/-/trtc-cloud-wx-1.0.8.tgz",
"integrity": "sha512-72TYYt6XNTr9Y401qw23YODfhLC9AGniNqb9ACFTVGDtInApHn139kGhKeudgTYEdTObkG1JjUcdkfcknHGhSA==",
"license": "ISC",
"dependencies": {
"eventemitter3": "^5.0.0",
"trtc-wx-sdk": "^1.1.14"
}
},
"node_modules/@tencentcloud/trtc-component-uniapp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@tencentcloud/trtc-component-uniapp/-/trtc-component-uniapp-1.0.5.tgz",
"integrity": "sha512-vffUArHP9hKsxRgd0R6uC8d538tJimUft/enrT1D9rGpoveRKAFkRpdDH44huNiM2XoS+JnkP6tMUqVCtvyi3g==",
"license": "MIT",
"peerDependencies": {
"@tencentcloud/trtc-cloud-wx": "^1.0.7"
}
},
"node_modules/@tencentcloud/trtc-component-wx": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@tencentcloud/trtc-component-wx/-/trtc-component-wx-1.0.6.tgz",
"integrity": "sha512-3m2ZU3/upx3IJAhSPdElJ2AF1D7L/0JOXUpanYJbrtHlwTWDQ6BOc+p6HgsepE67ltpR7UcYF/BcVHEfpcObkA==",
"license": "MIT",
"dependencies": {
"@tencentcloud/trtc-cloud-wx": "^1.0.7"
}
},
"node_modules/@tencentcloud/tui-core-lite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@tencentcloud/tui-core-lite/-/tui-core-lite-1.0.0.tgz",
"integrity": "sha512-+nmOWQ415Kz6aYJDv4EIdnaLk69SPWscZqmIhQp9GhNGFVq4/u+gC/sWvGHEOICJncFjM4dUxBg9EFfzD78rGQ==",
"license": "ISC",
"dependencies": {
"@tencentcloud/lite-chat": "^1.5.0"
}
},
"node_modules/@trtc/call-engine-lite-wx": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@trtc/call-engine-lite-wx/-/call-engine-lite-wx-3.4.13.tgz",
"integrity": "sha512-Dv3wREtBi7d368FhRuOFOE+erQrLHMo5lnPBIWXSil6sLO6ZqrSn6J6+e44sp7y6/RIJo+e+SxAyAVVxkLZOOQ==",
"license": "ISC",
"dependencies": {
"@tencentcloud/lite-chat": "^1.5.0",
"@tencentcloud/trtc-component-wx": "^1.0.5",
"eventemitter3": "^4.0.7"
}
},
"node_modules/@trtc/call-engine-lite-wx/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/@trtc/calls-uikit-wx-uniapp": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/@trtc/calls-uikit-wx-uniapp/-/calls-uikit-wx-uniapp-4.2.7.tgz",
"integrity": "sha512-Dpgn1kR1UgTP34X9lM1Yf6vXQ4odiTEf5Cc8BLepo7CMYNSvDw80B4bBPTatLT8og818yAVVinS8cZ+n4bxPCA==",
"license": "ISC",
"dependencies": {
"@tencentcloud/lite-chat": "^1.5.0",
"@tencentcloud/trtc-component-uniapp": "^1.0.4",
"@tencentcloud/tui-core-lite": "1.0.0",
"@trtc/call-engine-lite-wx": "~3.4.10"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/trtc-wx-sdk": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/trtc-wx-sdk/-/trtc-wx-sdk-1.1.14.tgz",
"integrity": "sha512-QYV2j5pDlhbM0zykYq8OYaSpjyTn0/YbsK180iNwCLEA61m2KWm2wb1veRBVsTP9Lq0iqbSMU17cOW963vcraA==",
"license": "ISC"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "tuicallkit-vue3",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@trtc/calls-uikit-wx-uniapp": "^4.1.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"pages": [
{
"path": "pages/index/login",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "TUICallKit/src/Components/TUICallKit",
"style": {
"navigationBarTitleText": "视频面诊"
}
},
{
"path": "pages/order/monad/monad",
"style": {
"navigationBarTitleText": "诊单确认"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "甄养堂",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}
}
+153
View File
@@ -0,0 +1,153 @@
<template>
<view class="container">
<view class='guide-box'>
<!-- <button @click="setCallingBell">setCallingBell</button> -->
<!-- <button @click="enableMuteMode">enableMuteMode</button> -->
<view class="single-box" v-for="(item, index) in entryInfos" :key="index" :id="index" @click='handleEntry'>
<image class="icon" mode="aspectFit" :src="item.icon" role="img"></image>
<view class="single-content">
<view class="label">{{ item.title }}</view>
<view class="desc">{{ item.desc }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
// import { TUICallKitAPI } from "../../TUICallKit/src/index";
// import { ref } from "vue";
const template = '1v1';
const entryInfos = [
{
icon: 'https://web.sdk.qcloud.com/component/miniApp/resources/audio-card.png',
title: '语音通话',
desc: '丢包率70%仍可正常语音通话',
navigateTo: '../calling/call?type=1',
},
{
icon: 'https://web.sdk.qcloud.com/component/miniApp/resources/video-card.png',
title: '视频通话',
desc: '丢包率50%仍可正常视频通话',
navigateTo: '../calling/call?type=2',
},
{
icon: 'https://web.sdk.qcloud.com/component/miniApp/resources/audio-card.png',
title: '多人语音通话',
desc: '丢包率70%仍可正常语音通话',
navigateTo: '../calling/groupCall?type=1',
},
{
icon: 'https://web.sdk.qcloud.com/component/miniApp/resources/video-card.png',
title: '多人视频通话',
desc: '丢包率50%仍可正常视频通话',
navigateTo: '../calling/groupCall?type=2',
},
];
// --------铃声相关接口测试-----------------
// let isMute = ref(false);
// const enableMuteMode = async () => {
// isMute.value = !isMute.value;
// await TUICallKitAPI.enableMuteMode(isMute.value);
// }
// const setCallingBell = async () => {
// await TUICallKitAPI.setCallingBell('mp2.mp3');
// }
const handleEntry = (e) => {
const url = entryInfos[e.currentTarget.id].navigateTo;
uni.navigateTo({
url,
});
}
</script>
<style>
.container {
/* background-image: url(https://mc.qcloudimg.com/static/img/7da57e0050d308e2e1b1e31afbc42929/bg.png); */
background: #F4F5F9;
background-repeat: no-repeat;
background-size: cover;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.container .title {
position: relative;
width: 100vw;
font-size: 18px;
color: #000000;
letter-spacing: 0;
text-align: center;
line-height: 28px;
font-weight: 600;
background: #FFFFFF;
margin-top: 3.8vh;
padding: 1.2vh 0;
}
.tips {
color: #ffffff;
font-size: 12px;
text-align: center;
}
.guide-box {
width: 100vw;
box-sizing: border-box;
padding: 16px;
display: flex;
flex-direction: column;
}
.single-box {
flex: 1;
border-radius: 10px;
background-color: #ffffff;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.icon {
display: block;
width: 180px;
height: 144px;
}
.single-content {
padding: 36px 30px 36px 20px;
color: #333333;
}
.label {
display: block;
font-size: 18px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.desc {
display: block;
font-size: 14px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.logo-box {
position: absolute;
width: 100vw;
bottom: 36rpx;
text-align: center;
}
.logo {
width: 160rpx;
height: 44rpx;
}
</style>
+320
View File
@@ -0,0 +1,320 @@
<template>
<view class="container">
<view class="counter-warp">
<view class="header-content">
<image :src="avatarUrl" class="icon-box" v-if="avatarUrl"/>
<view class="text-header">用户登录</view>
</view>
<view class="box">
<view class="list-item">
<label class="list-item-label">用户ID</label>
<input
class="input-box"
type="text"
v-model="userID"
placeholder="请输入用户ID"
placeholder-style="color:#BBBBBB;"
/>
</view>
<view class="login"
><button class="loginBtn" @click="loginHandler">登录</button></view
>
</view>
</view>
</view>
</template>
<script setup>
import { ref,onMounted,getCurrentInstance } from "vue";
// 2. 获取组件实例,通过 proxy 访问全局属性
const { proxy } = getCurrentInstance()
import * as GenerateTestUserSig from "../../debug/GenerateTestUserSig-es.js";
import { CallManager } from "../../TUICallKit/src/TUICallService/serve/callManager";
let userID = ref("4");
let avatarUrl = ref(""); // 声明为响应式变量
uni.CallManager = new CallManager();
onMounted(() => {
uni.login({
provider: 'weixin',
success: function (loginRes) {
console.log('xx');
console.log(loginRes.code);
wxcode(loginRes.code)
// 获取用户信息
uni.getUserInfo({
provider: 'weixin',
withCredentials:true,
success: function (infoRes) {
avatarUrl.value = infoRes.userInfo.avatarUrl; // 使用 .value 赋值
console.log('头像URL:', avatarUrl.value);
console.log(infoRes.userInfo);
}
});
}
});
console.log('---------进入onMounted')
})
const wxcode = async (code) => {
try {
// 通过 proxy 调用全局的 apiUrl
const res = await proxy.apiUrl({
url: '/api/login/mnpLogin',
method: 'POST',
data: {
code: code
},
}, false) // 不显示加载中
} catch (err) {
uni.showToast({ title: '请求失败', icon: 'none' })
console.error(err)
}
}
const loginHandler = async () => {
// 从后端获取签名
const signatureData = await getSignatureFromServer(patientId.value);
console.log('获取签名成功:', signatureData);
const {userId } = signatureData;
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
userID: userId.value,
});
console.warn('--- ', SDKAppID);
getApp().globalData.userID = userId.value;
getApp().globalData.userSig = userSig;
getApp().globalData.SDKAppID = SDKAppID;
await uni.CallManager.init({
sdkAppID: SDKAppID, // 替换为用户自己的 sdkAppID
userID: userId.value, // 替换为用户自己的 userID
userSig: userSig, // 替换为用户自己的 userSig
globalCallPagePath: "TUICallKit/src/Components/TUICallKit", // 替换为步骤一里注册的全局监听页面
});
uni.navigateTo({
url: "./index",
});
};
// 从后端获取签名
const getSignatureFromServer = async (patientId) => {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://api.zzzhengyangtang.cn/api/tcm/getPatientSignature', // 后端接口地址 // 替换为你的后端地址
method: 'GET',
data: {
patient_id: patientId
},
success: (res) => {
if (res.data && res.data.code === 1) {
resolve(res.data.data);
} else {
reject(new Error(res.data.msg || '获取签名失败'));
}
},
fail: (err) => {
reject(err);
}
});
});
};
</script>
<style scoped>
.container {
width: 100vw;
height: 100vh;
background-color: #f4f5f9;
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
.counter-warp {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.background-image {
width: 100%;
}
.header-content {
display: flex;
width: 100vw;
padding: 50px 20px 10px;
box-sizing: border-box;
top: 100rpx;
background-color: #000;
align-items: center;
}
.icon-box {
width: 56px;
height: 56px;
}
.text-header {
height: 72rpx;
font-size: 48rpx;
line-height: 72rpx;
color: #ffffff;
margin: 40px auto;
}
.text-content {
height: 36rpx;
font-size: 24rpx;
line-height: 36rpx;
color: #ffffff;
}
.box {
width: 80%;
height: 50vh;
position: relative;
background: #ffffff;
border-radius: 4px;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: left;
padding: 30px 20px;
}
.input-box {
flex: 1;
display: flex;
font-family: PingFangSC-Regular;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 0;
}
.login {
display: flex;
box-sizing: border-box;
margin-top: 15px;
width: 100%;
}
.login button {
background: rgba(0, 110, 255, 1);
border-radius: 30px;
font-size: 16px;
color: #ffffff;
letter-spacing: 0;
/* text-align: center; */
font-weight: 500;
}
.loginBtn {
margin-top: 64px;
background-color: white;
border-radius: 24px;
border-radius: 24px;
/* display: flex;
justify-content: center; */
width: 100% !important;
font-family: PingFangSC-Regular;
font-size: 16px;
color: #ffffff;
letter-spacing: 0;
}
.list-item {
display: flex;
flex-direction: column;
font-family: PingFangSC-Medium;
font-size: 14px;
color: #333333;
border-bottom: 1px solid #eef0f3;
}
.input-container {
width: 90%;
margin: 50px auto 0;
display: flex;
flex-direction: column;
font-family: PingFangSC-Medium;
font-size: 14px;
color: #333333;
border-bottom: 1px solid #eef0f3;
}
/* .input-box {
height: 20px;
padding: 5px;
width: 100%;
border: 1px solid #999999;;
} */
.list-item .list-item-label {
font-weight: 500;
padding: 10px 0;
}
.guide-box {
width: 100vw;
box-sizing: border-box;
padding: 16px;
display: flex;
flex-direction: column;
}
.single-box {
flex: 1;
border-radius: 10px;
background-color: #ffffff;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.icon {
display: block;
width: 180px;
height: 144px;
}
.single-content {
padding: 36px 30px 36px 20px;
color: #333333;
}
.label {
display: block;
font-size: 18px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.desc {
display: block;
font-size: 14px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
}
.logo-box {
position: absolute;
width: 100vw;
bottom: 36rpx;
text-align: center;
}
</style>
+639
View File
@@ -0,0 +1,639 @@
<template>
<view class="container">
<view class="form-wrapper">
<!-- 基本信息 -->
<view class="section">
<view class="section-title">基本信息</view>
<view class="info-item">
<text class="label">患者姓名</text>
<text class="value">{{ formData.patient_name || '-' }}</text>
</view>
<view class="info-item">
<text class="label">身份证号</text>
<text class="value">{{ formData.id_card || '-' }}</text>
</view>
<view class="info-item">
<text class="label">手机号</text>
<text class="value">{{ formData.phone || '-' }}</text>
</view>
<view class="info-item">
<text class="label">性别</text>
<text class="value">{{ formData.gender === 1 ? '男' : '女' }}</text>
</view>
<view class="info-item">
<text class="label">年龄</text>
<text class="value">{{ formData.age || '-' }}</text>
</view>
<view class="info-item">
<text class="label">诊断日期</text>
<text class="value">{{ formData.diagnosis_date || '-' }}</text>
</view>
<view class="info-item">
<text class="label">诊断类型</text>
<text class="value">{{ getDiagnosisTypeName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">证型</text>
<text class="value">{{ getSyndromeTypeName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">糖尿病期数</text>
<text class="value">{{ getDiabetesTypeName() || '-' }}</text>
</view>
</view>
<!-- 现病史 -->
<view class="section">
<view class="section-title">现病史</view>
<view class="info-item">
<text class="label">口腔感觉</text>
<text class="value">{{ getAppetiteName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">每日饮水量</text>
<text class="value">{{ getWaterIntakeName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">体重变化</text>
<text class="value">{{ getWeightChangeName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">脂肪肝程度</text>
<text class="value">{{ getFattyLiverDegreeName() || '-' }}</text>
</view>
<view class="info-item">
<text class="label">饮食情况</text>
<view class="tag-list">
<text v-for="item in getDietConditionNames()" :key="item" class="tag">{{ item }}</text>
<text v-if="getDietConditionNames().length === 0" class="value">-</text>
</view>
</view>
<view class="info-item">
<text class="label">肢体感觉</text>
<view class="tag-list">
<text v-for="item in getBodyFeelingNames()" :key="item" class="tag">{{ item }}</text>
<text v-if="getBodyFeelingNames().length === 0" class="value">-</text>
</view>
</view>
<view class="info-item">
<text class="label">睡眠情况</text>
<view class="tag-list">
<text v-for="item in getSleepConditionNames()" :key="item" class="tag">{{ item }}</text>
<text v-if="getSleepConditionNames().length === 0" class="value">-</text>
</view>
</view>
</view>
<!-- 既往史 -->
<view class="section">
<view class="section-title">既往史</view>
<view class="info-item">
<text class="label">既往史</text>
<view class="tag-list">
<text v-for="item in getPastHistoryNames()" :key="item" class="tag">{{ item }}</text>
<text v-if="getPastHistoryNames().length === 0" class="value">-</text>
</view>
</view>
</view>
<!-- 其他病史 -->
<view class="section">
<view class="section-title">其他病史</view>
<view class="info-item">
<text class="label">外伤史</text>
<text class="value">{{ formData.trauma_history === 1 ? '有' : '无' }}</text>
</view>
<view class="info-item">
<text class="label">手术史</text>
<text class="value">{{ formData.surgery_history === 1 ? '有' : '无' }}</text>
</view>
<view class="info-item">
<text class="label">过敏史</text>
<text class="value">{{ formData.allergy_history === 1 ? '有' : '无' }}</text>
</view>
</view>
<!-- 诊断信息 -->
<view class="section">
<view class="section-title">诊断信息</view>
<view class="info-item" v-if="formData.tongue_images && formData.tongue_images.length > 0">
<text class="label">舌苔照片</text>
<view class="image-list">
<image
v-for="(img, index) in formData.tongue_images"
:key="index"
:src="img"
class="tongue-image"
mode="aspectFill"
@click="previewImage(img)"
/>
</view>
</view>
<view class="info-item">
<text class="label">症状</text>
<text class="value multi-line">{{ formData.symptoms || '-' }}</text>
</view>
<view class="info-item">
<text class="label">舌苔</text>
<text class="value">{{ formData.tongue_coating || '-' }}</text>
</view>
<view class="info-item">
<text class="label">脉象</text>
<text class="value">{{ formData.pulse || '-' }}</text>
</view>
<view class="info-item">
<text class="label">治则</text>
<text class="value multi-line">{{ formData.treatment_principle || '-' }}</text>
</view>
<view class="info-item">
<text class="label">处方</text>
<text class="value multi-line">{{ formData.prescription || '-' }}</text>
</view>
<view class="info-item">
<text class="label">医嘱</text>
<text class="value multi-line">{{ formData.doctor_advice || '-' }}</text>
</view>
</view>
</view>
<!-- 底部浮动按钮 -->
<view class="fixed-bottom">
<button class="confirm-btn" @click="handleConfirm">确认诊单</button>
</view>
</view>
</template>
<script setup>
import { ref,onMounted,getCurrentInstance } from "vue";
// 2. 获取组件实例,通过 proxy 访问全局属性
const { proxy } = getCurrentInstance()
const formData = ref({
patient_name: '',
id_card: '',
phone: '',
gender: 1,
age: '',
diagnosis_date: '',
diagnosis_type: '',
syndrome_type: '',
diabetes_type: '',
appetite: '',
water_intake: '',
diet_condition: [],
weight_change: '',
body_feeling: [],
sleep_condition: [],
fatty_liver_degree: '',
past_history: [],
trauma_history: 0,
surgery_history: 0,
allergy_history: 0,
tongue_images: [],
symptoms: '',
tongue_coating: '',
pulse: '',
treatment_principle: '',
prescription: '',
doctor_advice: ''
});
// 字典选项
const diagnosisTypeOptions = ref([]);
const syndromeTypeOptions = ref([]);
const diabetesTypeOptions = ref([]);
const pastHistoryOptions = ref([]);
const appetiteOptions = ref([]);
const waterIntakeOptions = ref([]);
const dietConditionOptions = ref([]);
const weightChangeOptions = ref([]);
const bodyFeelingOptions = ref([]);
const sleepConditionOptions = ref([]);
const fattyLiverDegreeOptions = ref([]);
onMounted(() => {
getDictOptions();
loadDiagnosisDetail();
});
// 获取字典数据
const getDictOptions = async () => {
try {
const [
diagnosisType,
syndromeType,
diabetesType,
pastHistory,
appetite,
waterIntake,
dietCondition,
weightChange,
bodyFeeling,
sleepCondition,
fattyLiverDegree
] = await Promise.all([
getDictData('diagnosis_type'),
getDictData('syndrome_type'),
getDictData('diabetes_type'),
getDictData('past_history'),
getDictData('appetite'),
getDictData('water_intake'),
getDictData('diet_condition'),
getDictData('weight_change'),
getDictData('body_feeling'),
getDictData('sleep_condition'),
getDictData('fatty_liver_degree')
]);
diagnosisTypeOptions.value = diagnosisType?.diagnosis_type || [];
syndromeTypeOptions.value = syndromeType?.syndrome_type || [];
diabetesTypeOptions.value = diabetesType?.diabetes_type || [];
pastHistoryOptions.value = pastHistory?.past_history || [];
appetiteOptions.value = appetite?.appetite || [];
waterIntakeOptions.value = waterIntake?.water_intake || [];
dietConditionOptions.value = dietCondition?.diet_condition || [];
weightChangeOptions.value = weightChange?.weight_change || [];
bodyFeelingOptions.value = bodyFeeling?.body_feeling || [];
sleepConditionOptions.value = sleepCondition?.sleep_condition || [];
fattyLiverDegreeOptions.value = fattyLiverDegree?.fatty_liver_degree || [];
} catch (error) {
console.error('获取字典数据失败:', error);
}
};
// 获取单个字典数据
const getDictData = async (type) => {
try {
const res = await proxy.apiUrl({
url: '/api/tcm/getDict',
method: 'GET',
data: { type }
}, false);
if (res.code === 1) {
return res.data;
}
return null;
} catch (error) {
console.error(`获取字典${type}失败:`, error);
return null;
}
};
// 加载诊单详情
const loadDiagnosisDetail = async () => {
// 从页面参数获取诊单ID
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// 假设你的对象是 ai
const scene = currentPage.options.scene;
// 1. 先对 scene 进行 URL 解码
const decodedScene = decodeURIComponent(scene) ||currentPage.options;
// 得到 "id=4&share_user=1"
// 2. 解析成键值对对象
const params = {};
decodedScene.split('&').forEach(item => {
const [key, value] = item.split('=');
params[key] = value;
});
const diagnosisId = currentPage.options.id ||params.id|| 4;
console.log('nihhhhhh',pages,params,diagnosisId)
if (!diagnosisId) {
uni.showToast({ title: '缺少诊单ID', icon: 'none' });
return;
}
uni.showLoading({ title: '加载中...' });
try {
// 通过 proxy 调用全局的 apiUrl
const res = await proxy.apiUrl({
url: '/api/tcm/diagnosisDetail',
method: 'GET',
data: {
id: diagnosisId
},
}, false); // 不显示加载中
if (res.code === 1) {
formData.value = res.data;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'none' });
}
} catch (err) {
uni.showToast({ title: '请求失败', icon: 'none' });
console.error(err);
} finally {
uni.hideLoading();
}
};
// 获取选中项名称
const getDiagnosisTypeName = () => {
const item = diagnosisTypeOptions.value.find(i => i.value === formData.value.diagnosis_type);
return item ? item.name : '';
};
const getSyndromeTypeName = () => {
const item = syndromeTypeOptions.value.find(i => i.value === formData.value.syndrome_type);
return item ? item.name : '';
};
const getDiabetesTypeName = () => {
const item = diabetesTypeOptions.value.find(i => i.value === formData.value.diabetes_type);
return item ? item.name : '';
};
const getAppetiteName = () => {
const item = appetiteOptions.value.find(i => i.value === formData.value.appetite);
return item ? item.name : '';
};
const getWaterIntakeName = () => {
const item = waterIntakeOptions.value.find(i => i.value === formData.value.water_intake);
return item ? item.name : '';
};
const getWeightChangeName = () => {
const item = weightChangeOptions.value.find(i => i.value === formData.value.weight_change);
return item ? item.name : '';
};
const getFattyLiverDegreeName = () => {
const item = fattyLiverDegreeOptions.value.find(i => i.value === formData.value.fatty_liver_degree);
return item ? item.name : '';
};
const getDietConditionNames = () => {
if (!formData.value.diet_condition || formData.value.diet_condition.length === 0) return [];
// 处理字符串类型(逗号分隔)或数组类型
const values = Array.isArray(formData.value.diet_condition)
? formData.value.diet_condition
: formData.value.diet_condition.split(',');
return values.map(val => {
const item = dietConditionOptions.value.find(i => i.value === val);
return item ? item.name : val;
}).filter(Boolean);
};
const getBodyFeelingNames = () => {
if (!formData.value.body_feeling || formData.value.body_feeling.length === 0) return [];
// 处理字符串类型(逗号分隔)或数组类型
const values = Array.isArray(formData.value.body_feeling)
? formData.value.body_feeling
: formData.value.body_feeling.split(',');
return values.map(val => {
const item = bodyFeelingOptions.value.find(i => i.value === val);
return item ? item.name : val;
}).filter(Boolean);
};
const getSleepConditionNames = () => {
if (!formData.value.sleep_condition || formData.value.sleep_condition.length === 0) return [];
// 处理字符串类型(逗号分隔)或数组类型
const values = Array.isArray(formData.value.sleep_condition)
? formData.value.sleep_condition
: formData.value.sleep_condition.split(',');
return values.map(val => {
const item = sleepConditionOptions.value.find(i => i.value === val);
return item ? item.name : val;
}).filter(Boolean);
};
const getPastHistoryNames = () => {
if (!formData.value.past_history || formData.value.past_history.length === 0) return [];
// 处理字符串类型(逗号分隔)或数组类型
const values = Array.isArray(formData.value.past_history)
? formData.value.past_history
: formData.value.past_history.split(',');
return values.map(val => {
const item = pastHistoryOptions.value.find(i => i.value === val);
return item ? item.name : val;
}).filter(Boolean);
};
// 预览图片
const previewImage = (current) => {
uni.previewImage({
current,
urls: formData.value.tongue_images
});
};
// 确认诊单
const handleConfirm = () => {
uni.showModal({
title: '确认诊单',
content: '确认后将无法修改,是否确认?',
success: (res) => {
if (res.confirm) {
confirmDiagnosis();
}
}
});
};
// 提交确认诊单
const confirmDiagnosis = async () => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// 假设你的对象是 ai
const scene = currentPage.options.scene;
// 1. 先对 scene 进行 URL 解码
const decodedScene = decodeURIComponent(scene);
// 得到 "id=4&share_user=1"
// 2. 解析成键值对对象
const params = {};
decodedScene.split('&').forEach(item => {
const [key, value] = item.split('=');
params[key] = value;
});
const diagnosisId = currentPage.options.id ||params.id|| 4;
console.log('nihhhhhh',pages,params,diagnosisId)
if (!params.id) {
uni.showToast({ title: '缺少诊单ID', icon: 'none' });
return;
}
uni.showLoading({ title: '提交中...' });
try {
const res = await proxy.apiUrl({
url: '/api/tcm/confirmDiagnosis',
method: 'POST',
data: {
id: params.id,
share_user: params.share_user
}
}, false);
if (res.code === 1) {
uni.showToast({
title: '确认成功',
icon: 'success',
duration: 2000
});
setTimeout(() => {
uni.navigateBack();
}, 2000);
} else {
uni.showToast({ title: res.msg || '确认失败', icon: 'none' });
}
} catch (err) {
uni.showToast({ title: '请求失败', icon: 'none' });
console.error(err);
} finally {
uni.hideLoading();
}
};
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.form-wrapper {
padding: 20rpx;
padding-bottom: 140rpx; /* 为底部按钮留出空间 */
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.info-item {
display: flex;
margin-bottom: 24rpx;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
}
.label {
flex-shrink: 0;
width: 180rpx;
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
&.multi-line {
white-space: pre-wrap;
line-height: 1.8;
}
}
.tag-list {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.tag {
display: inline-block;
padding: 8rpx 20rpx;
background-color: #f0f5ff;
color: #1890ff;
font-size: 24rpx;
border-radius: 8rpx;
}
.image-list {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.tongue-image {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.95) 80%, rgba(255, 255, 255, 0) 100%);
z-index: 100;
}
.confirm-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 32rpx;
font-weight: 500;
border-radius: 44rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
}
</style>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="375px" height="268px" viewBox="0 0 375 268" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>背景</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="50%" y1="81.9277858%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#006EFF" offset="0%"></stop>
<stop stop-color="#00C8DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="浅色版本" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="主页" fill="url(#linearGradient-1)">
<g id="编组-4" transform="translate(-315.000000, -732.000000)">
<circle id="蒙版" cx="500" cy="500" r="500"></circle>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true
}
}
+10
View File
@@ -0,0 +1,10 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
});
},
});
+76
View File
@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;