This commit is contained in:
zhu
2026-03-27 17:55:18 +08:00
parent ee03132cee
commit 382f6b9811
34 changed files with 1406 additions and 376 deletions

View File

@@ -1 +0,0 @@
{"containers":[],"config":{}}

1
app.js
View File

@@ -1,4 +1,3 @@
// app.js
App({ App({
towxml: require('/towxml/index'), towxml: require('/towxml/index'),
globalData: { globalData: {

View File

@@ -3,16 +3,18 @@
"pages/system/welcome/index", "pages/system/welcome/index",
"pages/home/index", "pages/home/index",
"pages/test/index", "pages/test/index",
"pages/joinFlow/access/index",
"pages/joinFlow/manual/index",
"pages/joinFlow/person/index", "pages/joinFlow/person/index",
"pages/expert/index" "pages/expert/index",
"pages/chat/index"
], ],
"tabBar": { "tabBar": {
"custom": true, "custom": true,
"list": [ "list": [
{ {
"pagePath": "pages/home/index" "pagePath": "pages/home/index"
},
{
"pagePath": "pages/chat/index"
}, },
{ {
"pagePath": "pages/expert/index" "pagePath": "pages/expert/index"
@@ -65,6 +67,7 @@
"t-cascader": "tdesign-miniprogram/cascader/cascader", "t-cascader": "tdesign-miniprogram/cascader/cascader",
"t-form": "tdesign-miniprogram/form/form", "t-form": "tdesign-miniprogram/form/form",
"t-form-item": "tdesign-miniprogram/form-item/form-item", "t-form-item": "tdesign-miniprogram/form-item/form-item",
"t-divider": "tdesign-miniprogram/divider/divider" "t-divider": "tdesign-miniprogram/divider/divider",
"t-loading": "tdesign-miniprogram/loading/loading"
} }
} }

View File

@@ -15,7 +15,9 @@ navigator {
page { page {
--td-brand-color: rgba(0, 106, 106, 1);
--background: linear-gradient(135deg, rgba(0, 33, 33, 1), rgba(0, 106, 106, 1)); --background: linear-gradient(135deg, rgba(0, 33, 33, 1), rgba(0, 106, 106, 1));
--area-bottom: calc(130rpx + env(safe-area-inset-bottom));
.t-navbar__capsule::before { .t-navbar__capsule::before {
display: none; display: none;
} }

View File

@@ -19,6 +19,11 @@ Component({
label: '首页', label: '首页',
icon: 'home' icon: 'home'
}, },
{
path: "/pages/chat/index",
label: 'AI助手',
icon: 'chat-bubble'
},
{ {
path: "/pages/expert/index", path: "/pages/expert/index",
label: '专家服务', label: '专家服务',

292
pages/chat/index.js Normal file
View File

@@ -0,0 +1,292 @@
import request, { streamRequest } from "@/utils/request";
import { copyText } from "@/utils/common"
let outTimer; //输出定时器
let outStrList = [];//输出字符串数组
const app = getApp()
let chat_uuid
//工具
let toolText = ''
let toolInfo = {}
Page({
data: {
chatList: [], //会话记录
aiStatus: 3,//1表示接口响应中2表示接口响应完毕3表示完全输出完毕
keyboardBottom: 0, //底部键盘高度
scrollTop: 0, //滚动条位置
inputText: '',//输入框内容
},
onLoad() {
let str = `您好我是术极守护AI助手 🤖\n\n我可以为您解答术后康复相关的问题。请注意,我只能提供基于康复指南的一般性建议,如遇紧急情况请立即就医或联系您的主治医生。`
let mdResult = app.towxml(str, 'markdown', {
base: "www.xxx.com",
});
let list = [
{
loading: false,
end: true,
chat_type: 2,
md_content: mdResult,
chat_content: str,
quick_btn: [
{ text: '🩹伤口有轻微渗液怎么办?' },
{ text: '🌡️术后轻微发烧正常吗?' },
{ text: '🚿什么时候可以洗澡?' },
{ text: '🥗饮食需要注意什么?' }
]
}
]
this.setData({
chatList: list
})
},
onShow() {
this.getTabBar((tabBar) => {
tabBar.setData({
selected: 'AI助手',
})
})
},
//监听键盘弹出
bindkeyboardheightchange(event) {
let height = event.detail.height
let { keyboardBottom } = this.data
let scrollTop = 0
//获取当前滚动距离
const query = wx.createSelectorQuery().in(this)
query.select(".silder").scrollOffset()
query.exec(function (res) {
scrollTop = res[0].scrollTop
})
this.setData({
keyboardBottom: height > 0 ? height - 50 : keyboardBottom,
}, () => {
if (height > 0) {
setTimeout(() => {
this.setData({
scrollTop: scrollTop + height
})
}, 300)
}
})
},
//键盘失去焦点
bindkeyboardBlur() {
let { scrollTop, keyboardBottom } = this.data
let endScrollTop = scrollTop - keyboardBottom
this.setData({
keyboardBottom: 0
}, () => {
this.setData({
scrollTop: endScrollTop
})
})
},
//监听输入框内容
changeInput(e) {
let value = e.detail.value
this.setData({
inputText: value
})
},
async handQuick(e) {
let { data } = e.currentTarget.dataset
this.pushUserTemplate(data.text)
this.sendMessage(data.text)
},
// 文字提交发送
submitInput() {
let { inputText, aiStatus } = this.data
if (!inputText.trim() == '' && aiStatus) {
console.log('111--');
this.pushUserTemplate(inputText)
this.sendMessage(inputText)
}
},
//发送聊天
async sendMessage(text) {
let { chatList, aiStatus } = this.data
if (aiStatus == 3) {
let that = this
//初始化变量
toolText = ''
toolInfo = {}
chat_uuid = String(new Date().getTime())
this.setData({
aiStatus: 1
})
//输出
this.timerOutput()
//请求
streamRequest("/ai/chat", {
message: text,
chat_uuid: chat_uuid,
}, this.onChunkReceived).then(() => {
console.log('-------------成功-----------');
if (this.data.aiStatus != 3) {
that.setData({
aiStatus: 2
})
}
}).catch(() => {
console.log("------失败1---------");
that.data.chatList.at(-1).loading = false
that.setData({
aiStatus: 3,
chatList: chatList
})
})
}
},
//填充用户的聊天数据
pushUserTemplate(text) {
let { chatList } = this.data
let userItem = {
loading: false,
chat_type: 1,
con_type: 1,
chat_content: text,
md_content: '',
}
let aiItem = {
chat_type: 2,
chat_content: '',
md_content: '',
con_type: 1,
loading: true,
}
chatList.push(userItem)
chatList.push(aiItem)
//要修改的数据
let setData = {
chatList,
scrollTop: 80000,
inputText: ''
}
this.setData(setData, () => {
this.setData({
scrollTop: 80000
})
})
},
//只填充ai的默认回复
pushAiTemplate(text, options = {}) {
let { chatList } = this.data
let mdResult = app.towxml(text, 'markdown', {
base: "www.xxx.com",
});
let aiItem = {
chat_type: 2,
chat_content: text,
md_content: mdResult,
con_type: 1,
quick_btn: options.quick_btn || [],
...options
}
chatList.push(aiItem)
//要修改的数据
let setData = {
chatList,
scrollTop: 80000,
inputText: ''
}
this.setData(setData, () => {
this.setData({
scrollTop: 80000
})
})
},
//定时器输出
timerOutput() {
let { chatList } = this.data
let that = this
clearInterval(outTimer)
outStrList = []
let lastChat = this.data.chatList.at(-1)
//定时
outTimer = setInterval(async () => {
if ((outStrList.length == 0) && that.data.aiStatus == 2) {
clearInterval(outTimer)
lastChat.end = true
//如果有工具,设置工具对象
let text = lastChat.chat_content
// 上传AI响应结果
if (text) {
await request.post("/ai/history/upload", {
chat_content: text,
chat_uuid: chat_uuid,
})
}
//调用工具
if (toolText) { }
that.setData({
chatList,
aiStatus: 3
})
} else if (outStrList.length > 0) {
let firstValue = outStrList.shift();
lastChat.chat_content += firstValue
//转换md
let mdResult = app.towxml(lastChat.chat_content, 'markdown', {
base: "www.xxx.com",
});
lastChat.md_content = mdResult
lastChat.loading = false
that.setData({
chatList: chatList,
scrollTop: 80000
})
}
}, 50)
},
//流回调
onChunkReceived(data) {
data.forEach((item) => {
let value = item?.choices[0].delta.content ?? ''
let call = item?.choices[0].delta.tool_calls
//储存tool的id和名称
if (call) {
if (call[0].id) {
toolInfo.tool_call_id = call[0].id
toolInfo.name = call[0].function.name
}
let tool = call[0].function.arguments ?? ''
toolText += tool
}
if (value) {
outStrList.push(value)
}
})
},
//停止
async stopMessage() {
clearInterval(outTimer)
let lastChat = this.data.chatList.at(-1)
lastChat.chat_status = 2
this.setData({
aiStatus: 3,
chatList: this.data.chatList
})
await request.post("/ai/history/upload", {
chat_content: lastChat.chat_content || 'nocontent',
chat_status: 2,
chat_uuid: chat_uuid
})
},
//复制文字
copy(e) {
let { content, chat_content } = e.currentTarget.dataset.text
console.log(e);
if (!content) {
content = chat_content
}
copyText(content)
},
})

6
pages/chat/index.json Normal file
View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"towxml": "/towxml/towxml"
},
"navigationStyle": "custom"
}

179
pages/chat/index.scss Normal file
View File

@@ -0,0 +1,179 @@
@import "./makedown.scss";
.c-container {
display: flex;
flex-direction: column;
height: 100vh;
padding-bottom: var(--area-bottom);
.t-navbar__content {
backdrop-filter: blur(18px);
}
}
.silder {
overflow: auto;
flex: 1;
padding: 30rpx 0;
box-sizing: border-box;
.chat-item {
padding: 10rpx 30rpx;
&+.chat-item {
margin-top: 30rpx;
}
.lawyer-info {
margin-bottom: 20rpx;
font-size: 0.75rem;
image {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin-right: 10rpx;
display: block;
}
}
.chat-image {
width: 250rpx;
height: 250rpx;
border-radius: 20rpx;
}
.chat-message {
max-width: calc(100% - 100rpx);
min-height: 60rpx;
width: fit-content;
padding: 20rpx;
border-radius: 10rpx;
font-size: 0.9rem;
overflow: hidden;
.loading {
height: 40rpx;
width: 40rpx;
border-radius: 50%;
box-shadow: inset 0 0 0 var(--td-brand-color);
animation: load 2s linear infinite alternate;
}
.stop-tip {
margin-top: 20rpx;
font-size: 0.64rem;
color: var(--text-3);
}
.m-footer {
margin-top: 10rpx;
.tip {
@extend .stop-tip;
margin-top: 0;
opacity: 0.1;
}
.sound-icon {
color: var(--text-2);
font-size: 1rem;
}
}
}
.quick-warpper {
font-size: 0.75rem;
gap: 30rpx;
margin-top: 20rpx;
display: flex;
flex-wrap: wrap;
.quick-item {
background-color: white;
padding: 10rpx 20rpx;
border-radius: 50rpx;
border: 1px solid #dfdddd;
&.color {
color: white;
border-color: transparent;
;
}
}
}
}
.ai-msg {
.chat-message {
background-color: white;
border-radius: 20rpx 30rpx 30rpx 10rpx;
box-shadow: 0 0 15rpx #e2e2e2;
}
}
.user-msg {
display: flex;
flex-direction: column;
align-items: flex-end;
.chat-message {
background-color: var(--td-brand-color);
color: white;
border-radius: 30rpx 30rpx 0 30rpx;
}
}
}
.bottom-position {
margin: 0 30rpx;
position: relative;
//输入框
.input-warpper {
box-shadow: 0 0 15rpx #e2e2e2;
padding: 20rpx 30rpx;
background-color: white;
border-radius: 50rpx;
input {
flex: 1;
}
.push-btn {
border-radius: 50%;
width: 65rpx;
height: 65rpx;
color: var(--text-3);
transition: opacity 0.3s;
&.active-push {
background-color: var(--td-brand-color);
color: white;
}
}
.stop-btn {
font-size: 65rpx;
color: var(--td-brand-color);
}
}
}
//占位
.empty {
transition: height 0.3s;
}
//ai响应loading
@keyframes load {
0% {
box-shadow: inset -20rpx 40rpx 0 var(--td-brand-color);
}
100% {
box-shadow: inset 20rpx -40rpx 0 var(--td-brand-color);
}
}

103
pages/chat/index.wxml Normal file
View File

@@ -0,0 +1,103 @@
<page-meta root-font-size="system"></page-meta>
<view class="bg-gradient c-container">
<t-navbar class="custom-top-navbar"
fixed="{{false}}"
title="AI健康管家">
</t-navbar>
<scroll-view class="silder"
scroll-top="{{scrollTop}}"
scroll-y>
<view class="chat-item {{item.chat_type == 1 ? 'user-msg' : 'ai-msg'}}"
id="chat-${{index}}"
wx:for="{{chatList}}"
wx:key="index">
<view class="lawyer-info flex-align"
wx:if="{{item.chat_type !=1}}">
<text>AI助手</text>
</view>
<!-- 显示图片格式 -->
<view wx:if="{{item.con_type == 2}}">
<image src="{{item.chat_content}}"
class="chat-image"
data-url="{{item.chat_content}}"
bind:tap="viewImg"
mode="aspectFill" />
</view>
<view class="chat-message "
wx:else>
<view class="loading"
wx:if="{{item.loading}}"></view>
<view wx:elif="{{item.tool}}">
{{item.tool}}
</view>
<view wx:elif="{{item.chat_type == 1}}">
{{item.chat_content}}
</view>
<towxml wx:elif="{{item.chat_type == 2}}"
data-text="{{item}}"
bind:longpress="copy"
nodes="{{item.md_content}}" />
<!-- 底部功能 -->
<view wx:if="{{item.chat_status == 2}}"
class="stop-tip">
(已停止)
</view>
<view class="m-footer flex-between"
wx:elif="{{item.end}}">
<view class="tip">
AI助手仅提供基于康复指南的一般性建议不构成医疗诊断。如遇紧急情况请立即就医
</view>
<view class="flex-align">
<t-icon name="file-copy"
class="sound-icon"
data-text="{{item}}"
bind:tap="copy" />
</view>
</view>
</view>
<!-- 快捷按钮 -->
<view class="quick-warpper"
wx:if="{{item.quick_btn.length > 0 && item.end}}">
<view class="quick-item quick-{{quick.type}} {{quick.color ? 'color' : ''}}"
wx:for="{{item.quick_btn}}"
style="background-color: {{quick.color}};"
wx:for-item="quick"
wx:key="index"
bind:tap="handQuick"
data-parent="{{item}}"
data-data="{{quick}}">
{{quick.text}}
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="bottom-position">
<!-- 输入框 -->
<view class="input-warpper flex-align">
<input placeholder="有什么问题尽管问我"
bindblur="bindkeyboardBlur"
bindinput="changeInput"
bindconfirm="submitInput"
value="{{inputText}}"
hold-keyboard="{{false}}"
adjust-position="{{false}}"
bindkeyboardheightchange="bindkeyboardheightchange" />
<view class="push-btn flex-center {{inputText !='' ? 'active-push' : ''}} "
wx:if="{{aiStatus==3}}"
bind:tap="submitInput">
<t-icon name="send-filled" />
</view>
<t-icon name="stop-circle-filled"
class="stop-btn"
bind:tap="stopMessage"
wx:else />
</view>
</view>
<!-- 占位撑高 -->
<view style="height: {{keyboardBottom}}px;"
class="empty"></view>
</view>

17
pages/chat/makedown.scss Normal file
View File

@@ -0,0 +1,17 @@
.h2w__main,
.h2w__p {
padding: 0 !important;
margin: 0 !important;
font-size: 0.9rem;
}
.h2w__li {
margin-top: 20rpx;
}
.h2w__h3 {
font-size: 32rpx !important;
margin-top: 20rpx !important;
}

15
pages/chat/read.md Normal file
View File

@@ -0,0 +1,15 @@
聊天列表类型
``` typescript
interface ChatType{
loading: boolean, // 是否开始响应
end:boolean,//响应输出完成
chat_type: 1 | 2, // 1用户2ai普通响应
chat_content: string, //文本内容
md_content: string, //md格式
con_type:1, //消息类型1文本2图片
tool:string, //用工具时的默认内容,为空代表不是工具
chat_status?:number, //2停止
quick_btn:[], //快捷菜单按钮,为空不显示
drugList:[],//药品信息,只用来设置提醒
}
```

View File

@@ -1,9 +1,77 @@
import request from "@/utils/request"
const app = getApp()
Page({ Page({
data: {
list: [
{
s: "李",
name: "李医生",
title: "主任医师 · 骨科专家",
desc: "20年临床经验"
},
{
s: "王",
name: "王医生",
title: "副主任医师 · 康复医学",
desc: "15年临床经验"
},
{
s: "张",
name: "张护士长",
title: "主管护师 · 伤口护理",
desc: "18年护理经验"
}
],
count: 0
},
onShow() { onShow() {
let userInfo = app.globalData.userInfo
this.setData({
count: userInfo.consult_remains
})
this.getTabBar((tabBar) => { this.getTabBar((tabBar) => {
tabBar.setData({ tabBar.setData({
selected: '专家服务', selected: '专家服务',
}) })
}) })
},
async onPlay() {
if (this.data.count <= 0) {
wx.showToast({
title: '免费次数已用完',
icon: 'none'
});
return;
}
// 2. 弹窗确认
wx.showModal({
title: '拨打专家热线',
content: '确认拨打专家咨询电话吗本次咨询将使用1次免费机会',
success: async (res) => {
if (res.confirm) {
try {
wx.showLoading({ title: '正在呼叫...', mask: true });
const info = await request.get("/expert/phone");
this.setData({
count: this.data.count - 1
});
wx.hideLoading();
// 5. 吊起拨号盘
if (info && info.expert_phone) {
wx.makePhoneCall({
phoneNumber: info.expert_phone,
});
} else {
wx.showToast({ title: '暂无专家电话', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
}
}
}
});
} }
}) })

View File

@@ -1,5 +1,6 @@
page { page {
background-color: rgba(247, 249, 252, 1); background-color: rgba(247, 249, 252, 1);
padding-bottom: 200rpx;
} }
.head { .head {
@@ -47,11 +48,175 @@ page {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
} }
.btn{
.btn {
padding: 50rpx 0; padding: 50rpx 0;
background-color: rgba(29, 120, 116, 1); background-color: rgba(29, 120, 116, 1);
} }
.c-2{
.service-card {
width: 100%;
background-color: #fff;
border-radius: 12px;
padding: 16px;
box-sizing: border-box;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
margin-top: 30rpx;
.flex-align {
display: flex;
align-items: center;
}
.header {
.icon {
width: 32px;
height: 32px;
background-color: #f0f5f0; // 图标背景色
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
.title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.content {
margin-top: 12px;
.row {
display: flex;
justify-content: space-between;
margin-top: 6px;
.label {
font-size: 14px;
color: #666;
}
.time {
font-size: 14px;
font-weight: 600;
color: #18a74f; // 绿色
}
}
}
.status {
display: flex;
align-items: center;
margin-top: 12px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #18a74f;
margin-right: 6px;
}
.text {
font-size: 12px;
color: #18a74f;
}
}
}
.emergency-card {
background: rgba(255, 235, 238, 1);
border-radius: 16rpx;
padding: 32rpx;
margin-top: 30rpx;
gap: 30rpx;
display: flex;
align-items: flex-start;
--color: rgba(198, 40, 40, 1);
color: var(--color);
.icon {
width: 90rpx;
aspect-ratio: 1;
background-color: rgba(186, 26, 26, 0.15);
border-radius: 20rpx;
}
.t-1 {
font-weight: 700;
font-size: 34rpx;
margin-bottom: 20rpx;
}
.num {
font-weight: 700;
font-size: 40rpx;
}
.item {
font-size: 28rpx;
line-height: 60rpx;
display: flex;
align-items: center;
gap: 20rpx;
&::before {
content: "";
display: block;
width: 8rpx;
height: 8rpx;
background-color: var(--color);
border-radius: 50%;
}
}
}
.service {
background-color: white;
--color: black;
.icon {
background-color: rgba(0, 106, 106, 0.1);
}
}
.express {
background-color: white;
padding: 30rpx;
border-radius: 16rpx;
margin-top: 30rpx;
.item {
gap: 30rpx;
margin-top: 40rpx;
.icon {
width: 100rpx;
aspect-ratio: 1;
border-radius: 50%;
color: white;
font-weight: 700;
background: linear-gradient(rgba(29, 120, 116, 1), rgba(46, 139, 139, 1));
}
.t-1 {
font-weight: 700;
}
.t-2 {
font-size: 28rpx;
color: rgba(69, 70, 78, 1);
}
.t-3 {
font-size: 24rpx;
color: rgba(69, 70, 78, 1);
}
}
} }
} }

View File

@@ -14,21 +14,91 @@
<t-icon name="check-circle" /> <t-icon name="check-circle" />
可咨询次数 可咨询次数
</view> </view>
<view class="num">3</view> <view class="num">{{count}}</view>
</view> </view>
<view class="tip">本月剩余 3 次免费咨询机会</view> <view class="tip">本月剩余 {{count}} 次免费咨询机会</view>
</view> </view>
<t-button block <t-button block
theme="primary" theme="primary"
class="btn" class="btn"
icon="call-1"> icon="call-1"
bind:tap="onPlay">
一键拨打专家电话 一键拨打专家电话
</t-button> </t-button>
<view class="c-2 flex-align"> <view class="service-card">
<view class="icon"> <view class="header flex-align">
<t-icon name="time" /> <view class="icon">
<t-icon name="time" />
</view>
<view class="title">服务时间</view>
</view>
<view class="content">
<view class="row">
<view class="label">工作日</view>
<view class="time">09:00 - 21:00</view>
</view>
<view class="row">
<view class="label">周末及节假日</view>
<view class="time">10:00 - 18:00</view>
</view>
</view>
<view class="status">
<view class="dot"></view>
<view class="text">当前服务中</view>
</view>
</view>
<view class="emergency-card">
<view class="icon flex-center">
<t-icon name="error-triangle" />
</view>
<view style="flex: 1;">
<view class="t-1">紧急情况处理</view>
<view>如遇以下紧急情况,请立即拨打</view>
<view style="margin-bottom: 20rpx;"><text class="num">120</text> 或前往急诊</view>
<view class="list">
<view class="item">大量出血或伤口裂开</view>
<view class="item">高热不退体温≥39°C</view>
<view class="item">剧烈疼痛无法缓解</view>
<view class="item">呼吸困难或胸痛</view>
<view class="item">意识模糊或昏迷</view>
</view>
</view>
</view>
<view class="emergency-card service">
<view class="icon flex-center">
<t-icon name="info-circle" />
</view>
<view style="flex: 1;">
<view class="t-1">服务说明</view>
<view class="list">
<view class="item">每次通话时长不超过15分钟</view>
<view class="item">专家会根据您的情况提供专业建议</view>
<view class="item">复杂问题可能需要您到院面诊</view>
<view class="item">建议提前准备好相关检查报告</view>
<view class="item">咨询记录会同步到您的康复档案</view>
</view>
</view>
</view>
<view class="express">
<view style="font-weight: 700;">专家团队</view>
<view class="item flex-align"
wx:for="{{list}}"
wx:key="index">
<view class="icon flex-center">{{item.s}}</view>
<view>
<view class="t-1">{{item.name}}</view>
<view class="t-2">{{item.title}}</view>
<view class="t-3">{{item.desc}}</view>
</view>
</view> </view>
</view> </view>
</view> </view>

View File

@@ -1,7 +1,15 @@
import request from "@/utils/request"
Page({ Page({
data: { data: {
detail: {
overview: {},
tasks: [{}, {}, {}],
},
loading: true,
},
onLoad() {
this.init()
}, },
onShow() { onShow() {
@@ -10,5 +18,31 @@ Page({
selected: '首页', selected: '首页',
}) })
}) })
},
async init() {
let res = await request.get("/home")
this.setData({
detail: res,
loading: false,
})
},
//手动标记是否完成
handDone(e) {
const { data } = e.currentTarget.dataset;
const newTasks = this.data.detail.tasks.map(v => {
if (v.task_id == data.task_id) {
// 如果当前是 1 则变为 0如果是 0 则变为 1
return { ...v, is_completed: v.is_completed == 1 ? 0 : 1 };
}
return v;
});
request.post("/home/task-record", {
...data,
is_completed: data.is_completed == 1 ? 0 : 1
})
this.setData({
'detail.tasks': newTasks
});
} }
}) })

View File

@@ -1,5 +1,6 @@
page { page {
background-color: rgba(247, 249, 252, 1); background-color: rgba(247, 249, 252, 1);
padding-bottom: 160rpx;
} }
.header { .header {
@@ -78,11 +79,15 @@ page {
width: 90rpx; width: 90rpx;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 20rpx; border-radius: 20rpx;
background-color: var(--color); background-color: var(--back);
color: rgba(99, 63, 0, 1); color: rgba(99, 63, 0, 1);
} }
.content { .content {
flex: 1;
--td-tag-default-light-color: var(--back);
--td-tag-default-font-color: var(--color);
.time { .time {
margin-left: 20rpx; margin-left: 20rpx;
color: rgba(69, 70, 78, 1); color: rgba(69, 70, 78, 1);

View File

@@ -1,22 +1,25 @@
<wxs src="/utils/wxs/comment.wxs"
module="utils"></wxs>
<view class="header"> <view class="header">
<t-navbar class="fixed-nav" <t-navbar class="fixed-nav"
fixed="{{false}}" /> fixed="{{false}}" />
<view class="h-1"> <view class="h-1">
<view class="left"> <view class="left">
<view class="t-1">术后第 2 天</view> <view class="t-1">术后第 {{detail.overview.postoperative_day || 0}} 天</view>
<view class="t-2">康复进行中</view> <view class="t-2">{{detail.overview.status_text}}</view>
</view> </view>
<view class="right flex-column-center "> <view class="right flex-column-center ">
<view class="d">2</view> <view class="d">{{detail.overview.postoperative_day || 0}} </view>
<view class="c">DAYS</view> <view class="c">DAYS</view>
</view> </view>
</view> </view>
<view class="h-2"> <view class="h-2">
<view class="flex-between title"> <view class="flex-between title">
<view>今日任务完成度</view> <view>今日任务完成度</view>
<view class="num">0/5</view> <view class="num">{{taskUtils.getDoneCount(detail.tasks)}}/{{detail.overview.total_task_count}}</view>
</view> </view>
<t-progress percentage="80" <t-progress percentage="{{taskUtils.getDoneCount(detail.tasks) / detail.overview.total_task_count * 100}}"
color="{{ ['white', 'white'] }}" color="{{ ['white', 'white'] }}"
track-color="rgba(255, 255, 255, 0.2)" track-color="rgba(255, 255, 255, 0.2)"
label="" /> label="" />
@@ -25,20 +28,31 @@
<view class="list"> <view class="list">
<view class="item flex-align" <view class="item flex-align"
wx:for="{{5}}" style="--color:{{item.label_color}};--back:{{utils.getLightColor(item.label_color)}}"
wx:for="{{detail.tasks}}"
wx:key="index"> wx:key="index">
<view class="icon flex-center"> <t-skeleton loading="{{loading}}"
<t-icon name="apple" /> rowCol="{{[{ size: '90rpx' }]}}">
</view> <view class="icon flex-center">
<view class="content"> <t-icon name="{{item.pill}}" />
<view class="flex-align">
<t-tag variant="light"
theme="success">用药</t-tag>
<view class="time">08:00</view>
</view> </view>
<view class="title">服用抗生素</view> </t-skeleton>
<view class="desc">头孢类抗生素,饭后服用</view>
<button class="btn">标记完成</button> <view class="content">
<t-skeleton loading="{{loading}}"
theme="paragraph">
<view class="flex-align">
<t-tag variant="light">{{item.category_label}}</t-tag>
<view class="time">{{item.time_label}}</view>
</view>
<view class="title">{{item.title}}</view>
<view class="desc">{{item.content}}</view>
<button class="btn {{item.is_completed == 1 ? 'active' : ''}}"
bind:tap="handDone"
data-data="{{item}}">
{{item.is_completed == 0 ? '标记完成' : '已完成' }}
</button>
</t-skeleton>
</view> </view>
</view> </view>
</view> </view>
@@ -47,4 +61,22 @@
<view class="title">💡 温馨提示:</view> <view class="title">💡 温馨提示:</view>
<view class="text"> 请按时完成康复任务,如有任何不适请及时联系医护人员</view> <view class="text"> 请按时完成康复任务,如有任何不适请及时联系医护人员</view>
</view> </view>
<view style="height: 30rpx;"></view> <view style="height: 30rpx;"></view>
<wxs module="taskUtils">
var getDoneCount = function (tasks) {
if (!tasks || !tasks.length) return 0;
var count = 0;
for (var i = 0; i < tasks.length; i++) {
if (tasks[i].is_completed == 1 || tasks[i].is_completed === true) {
count++;
}
}
return count;
};
module.exports = {
getDoneCount: getDoneCount
};
</wxs>

View File

@@ -1,31 +0,0 @@
// pages/auth/access/index.js
Page({
data: {
},
handClick(e) {
let { type } = e.currentTarget.dataset
//拍照
if (type == 1) {
wx.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success(res) {
console.log(res);
},
fail(err) {
wx.showToast({
title: '扫码失败',
icon: 'none'
})
}
})
} else {
wx.navigateTo({
url: "/pages/joinFlow/manual/index",
})
}
}
})

View File

@@ -1,4 +0,0 @@
{
"usingComponents": {},
"navigationStyle": "custom"
}

View File

@@ -1,80 +0,0 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.banner {
height: 400rpx;
background-image: var(--background);
padding: 30rpx;
.text-1 {
color: white;
font-size: 60rpx;
font-weight: bold;
}
.text-2 {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
.content {
flex: 1;
background-color: white;
transform: translateY(-20rpx);
border-radius: 30rpx 30rpx 0 0;
padding: 80rpx 30rpx 30rpx;
.text-1 {
text-align: center;
font-weight: 700;
font-size: 45rpx;
}
.text-2 {
font-size: 28rpx;
text-align: center;
color: rgba(69, 70, 78, 1);
margin-top: 20rpx;
margin-bottom: 60rpx;
}
.item {
box-shadow: 0 4rpx 12rpx rgba(11, 27, 61, 0.14);
padding: 30rpx;
border-radius: 30rpx;
gap: 30rpx;
margin-bottom: 60rpx;
.icon {
width: 90rpx;
aspect-ratio: 1;
background-color: rgba(0, 106, 106, 0.1);
font-size: 50rpx;
border-radius: 20rpx;
color: rgba(46, 139, 87, 1);
}
.t-1 {
font-weight: bold;
}
.t-2 {
margin-top: 10rpx;
font-size: 24rpx;
color: rgba(69, 70, 78, 1);
}
}
.tip {
background-color: rgba(255, 221, 180, 1);
padding: 30rpx;
border-radius: 20rpx;
color: rgba(99, 63, 0, 1);
font-size: 28rpx;
line-height: 44rpx;
}
}

View File

@@ -1,40 +0,0 @@
<view class="banner">
<t-navbar t-class="fixed-nav"
fixed="{{false}}" />
<view class="info">
<view class="text-1">术极守护</view>
<view class="text-2">您的贴身康复管家</view>
</view>
</view>
<view class="content">
<view class="text-1">开始使用</view>
<view class="text-2">请选择康复计划接入方式</view>
<view class="item flex-align"
bind:tap="handClick"
data-type="1">
<view class="icon flex-center">
<t-icon name="qrcode" />
</view>
<view>
<view class="t-1">扫码接入</view>
<view class="t-2">扫描医护人员提供的二维码</view>
</view>
</view>
<view class="item flex-align"
bind:tap="handClick"
data-type="2">
<view class="icon flex-center">
<t-icon name="search" />
</view>
<view>
<view class="t-1">手动选择</view>
<view class="t-2">搜索医院、科室、手术名称</view>
</view>
</view>
<view class="tip">
<view>💡 温馨提示:</view>
<view>首次使用需要录入基本信息,请准备好您的手术相关资料</view>
</view>
</view>

View File

@@ -1,36 +0,0 @@
Page({
data: {
formData: {
hospital: "",
department: ""
}
},
onInputChange(e) {
console.log("dsds");
const {field} = e.currentTarget.dataset;
this.setData({
[`formData.${field}`]: e.detail.value,
});
},
onSubmit() {
let {formData} = this.data
let errorText = ""
if (!formData.hospital.trim()) {
errorText = "请填写医院名称"
}
if (!formData.department.trim()) {
errorText = "请填写科室名称"
}
if (errorText) {
wx.showToast({
title: errorText,
icon: "none"
})
return;
}
wx.navigateTo({
url: "/pages/joinFlow/person/index"
})
}
})

View File

@@ -1,4 +0,0 @@
{
"usingComponents": {},
"navigationStyle": "custom"
}

View File

@@ -1,8 +0,0 @@
.content {
margin-top: 60rpx;
.btn {
margin-top: 60rpx;
padding: 40rpx 30rpx;
}
}

View File

@@ -1,26 +0,0 @@
<t-navbar title="选择康复计划"
left-arrow
fixed="{{false}}" />
<view class="content">
<t-input value="{{formData.hospital}}"
borderless
label="医院"
placeholder="请输入医院名称"
bind:change="onInputChange"
data-field="hospital" />
<t-input value="{{formData.department}}"
borderless
label="科室"
clearable="{{false}}"
placeholder="请输入科室名称"
bind:change="onInputChange"
data-field="department" />
<view class="btn">
<t-button block
theme="primary" bind:tap="onSubmit">
确认
</t-button>
</view>
</view>

View File

@@ -1,21 +1,100 @@
import request from "@/utils/request"
import { getToken, setToken } from "@/utils/auth/manageToken"
import { formatDate } from "@/utils/common"
const app = getApp()
Page({ Page({
data: { data: {
loading: true,
parmas: {},
mood: "manual",
showPicker: false, showPicker: false,
date: new Date().getTime(), date: new Date().getTime(),
//手术 //手术
showSurgery: false, showSurgery: false,
surgeryList: [], surgeryList: [],
selectSurgery: [], selectSurgery: [],
//医院信息
hospital: "",
department: "",
//其他信息
age: "", age: "",
allergy: "", allergy: "",
comorbidity: "", comorbidity: "",
}, },
onLoad(e) {
if (Object.keys(e).length > 0) {
this.setData({
parmas: e,
mood: "scan"
})
}
this.init()
},
//初始化
async init() {
let token = getToken()
//如果存在
if (token) {
let info = await request.get("/my-info")
this.handUser(info)
} else {
wx.login({
success: async (res) => {
let response = await request.post("/login", {
"wx_code": res.code
})
setToken(response.accessToken)
this.handUser(response)
},
})
}
},
//处理信息
async handUser(data) {
app.globalData.userInfo = data.userInfo
if (data.needProfile == 0) {
wx.switchTab({
url: "/pages/home/index",
})
} else {
let res = await request.get("/profile/surgicals")
this.setData({
surgeryList: res.list,
})
}
this.setData({
loading: false,
})
},
onInputChange(e) { onInputChange(e) {
const { field } = e.currentTarget.dataset; const { field } = e.currentTarget.dataset;
this.setData({ this.setData({
[`${field}`]: e.detail.value, [`${field}`]: e.detail.value,
}); });
}, },
//选择手术
changeSurgeryShow() {
this.setData({
showSurgery: !this.data.showSurgery
})
},
handSelectSurgery(e) {
let { data } = e.currentTarget.dataset
let { selectSurgery } = this.data
let isHave = selectSurgery.find(item => item.id == data.id)
if (isHave) {
this.setData({
selectSurgery: selectSurgery.filter((item) => item.id != data.id)
})
} else {
this.setData({
selectSurgery: [...selectSurgery, data]
})
}
},
//选择时间 //选择时间
chaneTimeShow() { chaneTimeShow() {
this.setData({ this.setData({
@@ -29,22 +108,26 @@ Page({
}) })
}, },
//选择手术
changeSurgeryShow() {
this.setData({
showSurgery: !this.data.showSurgery
})
},
//提交 //提交
onSubmit() { async onSubmit() {
let { date, age, allergy, comorbidity } = this.data let { mood, hospital, department, date, age, allergy, comorbidity, parmas, selectSurgery } = this.data
let errorText = "" let errorText = ""
if (!date) { if (mood == 'manual' && !hospital.trim()) {
errorText = "请输入医院"
}
else if (mood == 'manual' && !department.trim()) {
errorText = "请输入科室"
}
else if (selectSurgery.length == 0) {
errorText = "请选择手术"
}
else if (!date) {
errorText = "请选择时间" errorText = "请选择时间"
} }
if (!age.trim()) { else if (!age.trim()) {
errorText = "请填写年龄" errorText = "请填写年龄"
} }
if (errorText) { if (errorText) {
@@ -54,8 +137,29 @@ Page({
}) })
return return
} }
wx.switchTab({ try {
url: '/pages/home/index', wx.showLoading({
}) title: '提交中',
mask: true
})
await request.post("/profile", {
entry_mode: mood,
...parmas,
hospital_name: hospital,
department_name: department,
surgical_ids: selectSurgery.map(item => item.id),
surgical_date: formatDate(date, 'YYYY-MM-DD'),
age: age,
allergy_history: allergy,
complication: comorbidity
})
wx.hideLoading()
wx.switchTab({
url: '/pages/home/index',
})
} catch (e) {
console.log(e);
wx.hideLoading()
}
} }
}) })

View File

@@ -1,4 +1,4 @@
page { .page {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -21,6 +21,7 @@ page {
} }
} }
.content { .content {
flex: 1; flex: 1;
height: 0; height: 0;
@@ -29,6 +30,10 @@ page {
border-radius: 30rpx 30rpx 0 0; border-radius: 30rpx 30rpx 0 0;
padding: 50rpx 0 0; padding: 50rpx 0 0;
.ce {
white-space: nowrap;
}
.text-1 { .text-1 {
font-weight: 700; font-weight: 700;
font-size: 45rpx; font-size: 45rpx;

View File

@@ -1,55 +1,84 @@
<wxs src="/utils/wxs/comment.wxs" <wxs src="/utils/wxs/comment.wxs"
module="utils"></wxs> module="utils"></wxs>
<view class="banner">
<t-navbar t-class="fixed-nav" <view class="page"
fixed="{{false}}" /> wx:if="{{!loading}}">
<view class="info"> <view class="banner">
<view class="text-1">术极守护</view> <t-navbar t-class="fixed-nav"
<view class="text-2">您的贴身康复管家</view> fixed="{{false}}" />
<view class="info">
<view class="text-1">术极守护</view>
<view class="text-2">您的贴身康复管家</view>
</view>
</view>
<scroll-view class="content"
scroll-y>
<view class="text-1">建立康复档案</view>
<t-cell title="医院"
wx:if="{{mood == 'manual'}}">
<input slot="note"
placeholder="请输入医院"
data-field="hospital"
bind:change="onInputChange" />
</t-cell>
<t-cell title="科室"
wx:if="{{mood == 'manual'}}">
<input slot="note"
placeholder="请输入科室"
data-field="department"
bind:change="onInputChange" />
</t-cell>
<t-cell title="选择手术"
arrow
t-class-title="ce"
bind:tap="changeSurgeryShow">
<view slot="note">
{{select.getNames(selectSurgery)}}
</view>
</t-cell>
<t-cell title="手术日期"
arrow
note="{{utils.formatDate(date, 'YYYY/MM/DD') || '请选择'}}"
bind:tap="chaneTimeShow">
</t-cell>
<t-cell title="年龄">
<input type="number"
slot="note"
maxlength="3"
placeholder="请输入年龄"
data-field="age"
bind:change="onInputChange" />
</t-cell>
<t-cell title="过敏史">
<textarea placeholder="请输入过敏史(如:青霉素过敏)"
slot="description"
data-field="allergy"
bind:change="onInputChange" />
</t-cell>
<t-cell title="合并症">
<textarea placeholder="请输入合并症(如:高血压、糖尿病)"
slot="description"
data-field="comorbidity"
bind:change="onInputChange" />
</t-cell>
</scroll-view>
<view class="btn">
<t-button block
theme="primary"
bind:tap="onSubmit">下一步
</t-button>
</view> </view>
</view> </view>
<scroll-view class="content" <view class="flex-center"
scroll-y> style="height: 100vh;"
<view class="text-1">建立康复档案</view> wx:else>
<t-cell title="选择手术" <t-loading theme="circular"
arrow size="40rpx" />
note="请选择"
bind:tap="changeSurgeryShow">
</t-cell>
<t-cell title="手术日期"
arrow
note="{{utils.formatDate(date, 'YYYY/MM/DD') || '请选择'}}"
bind:tap="chaneTimeShow">
</t-cell>
<t-cell title="年龄">
<input type="number"
slot="note"
maxlength="3"
placeholder="请输入年龄"
data-field="age"
bind:change="onInputChange" />
</t-cell>
<t-cell title="过敏史">
<textarea placeholder="请输入过敏史(如:青霉素过敏)"
slot="description"
data-field="allergy"
bind:change="onInputChange" />
</t-cell>
<t-cell title="合并症">
<textarea placeholder="请输入合并症(如:高血压、糖尿病)"
slot="description"
data-field="comorbidity"
bind:change="onInputChange" />
</t-cell>
</scroll-view>
<view class="btn">
<t-button block
theme="primary"
bind:tap="onSubmit">下一步
</t-button>
</view> </view>
<t-popup visible="{{showSurgery}}" <t-popup visible="{{showSurgery}}"
placement="bottom" placement="bottom"
bind:visible-change="changeSurgeryShow"> bind:visible-change="changeSurgeryShow">
@@ -57,9 +86,14 @@
<view class="title">手术列表</view> <view class="title">手术列表</view>
<scroll-view class="surgery-list" <scroll-view class="surgery-list"
scroll-y> scroll-y>
<view class="flex-between surgery-item " wx:for="{{6}}" wx:key="index"> <view class="flex-between surgery-item "
<view class="name">测试手术名字</view> wx:for="{{surgeryList}}"
<t-icon name="check" /> wx:key="index"
data-data="{{item}}"
bind:tap="handSelectSurgery">
<view class="name">{{item.name}}</view>
<t-icon name="check"
wx:if="{{select.isSelected(item.id, selectSurgery)}}" />
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -72,4 +106,28 @@
default-value="{{date}}" default-value="{{date}}"
format="YYYY-MM-DD" format="YYYY-MM-DD"
bindconfirm="onTimeConfirm" bindconfirm="onTimeConfirm"
bindclose="chaneTimeShow" /> bindclose="chaneTimeShow" />
<wxs module="select">
module.exports = {
isSelected: function (id, list) {
if (!list || !list.length) return false;
for (var i = 0; i < list.length; i++) {
if (list[i].id === id) return true;
}
return false;
},
getNames: function (list) {
if (!list || !list.length) return "请选择";
// 拼接名字,逗号分隔
var names = "";
for (var i = 0; i < list.length; i++) {
names += list[i].name;
if (i < list.length - 1) names += ", ";
}
return names;
}
};
</wxs>

View File

@@ -12,20 +12,19 @@ Page({
let state = wx.getStorageSync(this.data.key); let state = wx.getStorageSync(this.data.key);
if (state) { if (state) {
wx.redirectTo({ wx.redirectTo({
url: "/pages/joinFlow/access/index", url: "/pages/joinFlow/person/index",
}) })
} else { } else {
this.setData({ this.setData({
loading: false loading: false
}) })
} }
}, },
handNext() { handNext() {
wx.setStorageSync(this.data.key, true) wx.setStorageSync(this.data.key, true)
wx.redirectTo({ wx.redirectTo({
url: "/pages/home/index", url: "/pages/joinFlow/person/index"
}) })
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"compileType": "miniprogram", "compileType": "miniprogram",
"libVersion": "trial", "libVersion": "3.15.1",
"setting": { "setting": {
"coverView": true, "coverView": true,
"es6": true, "es6": true,

View File

@@ -3,7 +3,7 @@
"projectname": "wx_app_template", "projectname": "wx_app_template",
"setting": { "setting": {
"compileHotReLoad": true, "compileHotReLoad": true,
"urlCheck": true, "urlCheck": false,
"coverView": true, "coverView": true,
"lazyloadPlaceholderEnable": false, "lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false, "skylineRenderEnable": false,
@@ -20,17 +20,5 @@
"bigPackageSizeSupport": false "bigPackageSizeSupport": false
}, },
"libVersion": "3.15.1", "libVersion": "3.15.1",
"condition": { "condition": {}
"miniprogram": {
"list": [
{
"name": "pages/home/index",
"pathName": "pages/home/index",
"query": "",
"scene": null,
"launchMode": "default"
}
]
}
}
} }

View File

@@ -72,4 +72,20 @@ export function uploadQiuFile(path, file, is_public = 1) {
} }
}) })
}) })
}
/**
* 复制文字内容
* @param {文本} text
*/
export function copyText(text) {
wx.setClipboardData({
data: text,
success() {
wx.showToast({
title: '已复制到剪切板',
icon: 'none'
})
},
})
} }

View File

@@ -2,11 +2,11 @@ import httpEnv from "./auth/env"
import { getToken, removeToken } from "./auth/manageToken" import { getToken, removeToken } from "./auth/manageToken"
const baseApi = { const baseApi = {
//开发版 //开发版
develop: "https://curainwxapp.test.tuzuu.com/api", develop: "https://kairos-wx.test.tuzuu.com/api",
// 体验版 // 体验版
trial: "https://curainwxapp.test.tuzuu.com/api", trial: "https://kairos-wx.test.tuzuu.com/api",
// 正式版 // 正式版
release: "https://yidaojia.cells.org.cn/api" release: "https://kairos-wx.test.tuzuu.com/api",
} }
//获取当前环境的接口前缀 //获取当前环境的接口前缀
export const baseUrl = baseApi[httpEnv] export const baseUrl = baseApi[httpEnv]
@@ -82,4 +82,65 @@ request.post = function (url, options) {
}) })
} }
//流式
let lastChunk = '' //流可能会被截断,需要补充
export function streamRequest(url, data, onChunkReceived) {
return new Promise((resolve, reject) => {
const token = getToken()
let header = {}
if (token) {
header.Authorization = 'Bearer ' + token;
}
let requestTask = wx.request({
url: baseUrl + url,
data: data || {},
method: 'POST',
header,
timeout: 100000,
responseType: "arraybuffer",
enableChunked: true, //关键!开启流式传输模式
success: () => {
resolve()
},
fail: (e) => {
console.log(e);
reject()
}
})
//处理流
if (onChunkReceived) {
lastChunk = ''
requestTask.onChunkReceived((response) => {
let data = response.data;
// console.log(decodeStream(data));
onChunkReceived(decodeStream(data))
})
}
})
}
//流素具解码
function decodeStream(data) {
let uint8Array = new Uint8Array(data); // 将 ArrayBuffer 转换为 Uint8Array
let responseText = decodeURIComponent(escape(String.fromCharCode.apply(null, uint8Array))); // 使用 apply 扩展字节
// 处理数据,移除前缀等操作
if (lastChunk) {
responseText = "data: " + lastChunk + responseText
lastChunk = ""
}
// 处理数据,移除前缀等操作
responseText = responseText.replaceAll("data: ", '');
let jsonStrings = responseText.trim().split(/\n+/);
// 过滤掉解析失败的部分
let jsonArray = jsonStrings.map(jsonStr => {
try {
return JSON.parse(jsonStr);
} catch (e) {
lastChunk = jsonStr
console.error('JSON格式不正确(已做兼容处理):');
return null; // 如果解析失败返回null
}
}).filter(item => item !== null);
return jsonArray
}
export default request export default request

View File

@@ -1,29 +1,63 @@
module.exports = { module.exports = {
/** /**
* 日期格式化 * 日期格式化
* @param {Date | String | Number} date * @param {Date | String | Number} date
* @param {String} format 格式 * @param {String} format 格式
*/ */
formatDate: function (date, format) { formatDate: function (date, format) {
if (!format) { if (!format) {
format = 'YYYY/MM/DD hh:mm:ss'; format = 'YYYY/MM/DD hh:mm:ss';
} }
if (!date) { if (!date) {
return ''; return '';
} }
date = getDate(date) date = getDate(date)
var YYYY = date.getFullYear(); var YYYY = date.getFullYear();
var MM = ('0' + (date.getMonth() + 1)).slice(-2) var MM = ('0' + (date.getMonth() + 1)).slice(-2)
var DD =('0' + date.getDate()).slice(-2); var DD = ('0' + date.getDate()).slice(-2);
var hh = ("0" + date.getHours()).slice(-2); var hh = ("0" + date.getHours()).slice(-2);
var mm = ("0" + date.getMinutes()).slice(-2); var mm = ("0" + date.getMinutes()).slice(-2);
var ss = ("0" + date.getSeconds()).slice(-2); var ss = ("0" + date.getSeconds()).slice(-2);
var result = format.replace(getRegExp('YYYY'), YYYY) var result = format.replace(getRegExp('YYYY'), YYYY)
.replace(getRegExp('MM'), MM) .replace(getRegExp('MM'), MM)
.replace(getRegExp('DD'), DD) .replace(getRegExp('DD'), DD)
.replace(getRegExp('hh'), hh) .replace(getRegExp('hh'), hh)
.replace(getRegExp('mm'), mm) .replace(getRegExp('mm'), mm)
.replace(getRegExp('ss'), ss) .replace(getRegExp('ss'), ss)
return result; return result;
}, },
}; /**
* 获取浅色标签颜色
* @param {string} hexColor 如 "#e3f9e9"
*/
getLightColor: function (color, level) {
if (!color || color.indexOf('#') === -1) return color;
// 默认变浅程度
var weight = level !== undefined ? level : 0.9;
// 处理 #fff 这种简写
var hex = color.slice(1);
if (hex.length === 3) {
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
}
// 提取 RGB
var r = parseInt(hex.substring(0, 2), 16);
var g = parseInt(hex.substring(2, 4), 16);
var b = parseInt(hex.substring(4, 6), 16);
// 与白色(255)混合逻辑:新颜色 = 原颜色 * (1 - weight) + 255 * weight
r = Math.floor(r * (1 - weight) + 255 * weight);
g = Math.floor(g * (1 - weight) + 255 * weight);
b = Math.floor(b * (1 - weight) + 255 * weight);
// 转回 16 进制
var toString16 = function (n) {
var s = n.toString(16);
return s.length === 1 ? '0' + s : s;
};
return '#' + toString16(r) + toString16(g) + toString16(b);
}
};