feat(ai): 新增 AI 客服聊天功能 + 消息页重构为小华助手
- 新增 POST /ai/chat 端点,由 LLM(Ollama qwen3)担任 24h 健康客服"小华" - 新增 ai.chat.send 权限,绑定管理员/患者/医生/护士/健康管理师角色 - 消息页从咨询列表重构为单窗口 AI 对话(欢迎态 + 聊天态 + 快捷问诊) - 通知功能迁移到"我的"页面菜单项(带未读角标),独立通知列表页 - 修复气泡文字截断:改用百分比 max-width + block Text + pre-wrap 换行 - 修复权限绑定:迁移 SQL 角色名从英文改为中文(admin→管理员,patient→患者)
This commit is contained in:
@@ -1,270 +1,300 @@
|
||||
@import '../../styles/variables.scss';
|
||||
@import '../../styles/mixins.scss';
|
||||
|
||||
.messages-page {
|
||||
// PageShell 接管 min-height, background
|
||||
padding: var(--tk-section-gap) var(--tk-page-padding) var(--tk-tabbar-space);
|
||||
padding-bottom: calc(var(--tk-tabbar-space) + env(safe-area-inset-bottom));
|
||||
.ai-chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
/* ─── 页头 ─── */
|
||||
.messages-header {
|
||||
margin-bottom: var(--tk-section-gap);
|
||||
/* ─── 导航栏 ─── */
|
||||
.ai-chat-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 20px 12px;
|
||||
background: $card;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.messages-title {
|
||||
@include serif-number;
|
||||
font-size: var(--tk-font-h1);
|
||||
.ai-chat-nav__title-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ai-chat-nav__title {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
/* ─── 分段控件 Tab ─── */
|
||||
.msg-segment {
|
||||
.ai-chat-nav__online {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: $surface-alt;
|
||||
border-radius: $r-sm;
|
||||
padding: 3px;
|
||||
margin-bottom: var(--tk-gap-sm);
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.msg-segment-tab {
|
||||
.ai-chat-nav__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: $acc;
|
||||
}
|
||||
|
||||
.ai-chat-nav__online-text {
|
||||
font-size: 11px;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
/* ─── 欢迎状态 ─── */
|
||||
.ai-chat-welcome {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: $r-xs;
|
||||
@include flex-center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 36px;
|
||||
background: linear-gradient(135deg, $pri, $pri-d);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 24px rgba(196, 98, 58, 0.25);
|
||||
}
|
||||
|
||||
.ai-chat-welcome__avatar-char {
|
||||
color: $white;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__greeting {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__desc {
|
||||
font-size: 13px;
|
||||
color: $tx3;
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__divider {
|
||||
width: 32px;
|
||||
height: 1px;
|
||||
background: $bd;
|
||||
margin: 20px 0 16px;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__hint {
|
||||
font-size: 12px;
|
||||
color: $tx3;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.ai-chat-welcome__pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
border: 1px solid $bd-l;
|
||||
|
||||
&:active {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.msg-segment-active {
|
||||
background: $card;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
.msg-segment-text {
|
||||
color: $tx;
|
||||
}
|
||||
.ai-chat-welcome__pill-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.msg-segment-text {
|
||||
font-size: var(--tk-font-cap);
|
||||
font-weight: 600;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.msg-segment-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 12px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: $r-xs;
|
||||
background: $dan;
|
||||
@include flex-center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.msg-segment-badge-text {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: $white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 内容区 ─── */
|
||||
.msg-content {
|
||||
// wrapper
|
||||
}
|
||||
|
||||
.msg-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--tk-gap-xs);
|
||||
}
|
||||
|
||||
/* ─── 咨询卡片 ─── */
|
||||
.consult-card {
|
||||
display: flex;
|
||||
gap: var(--tk-gap-sm);
|
||||
align-items: center;
|
||||
// ContentCard 接管 background, border-radius, padding, box-shadow, active feedback
|
||||
}
|
||||
|
||||
.consult-card-muted {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.consult-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-pill;
|
||||
background: $surface-alt;
|
||||
@include flex-center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.consult-avatar-active {
|
||||
background: var(--tk-pri-l);
|
||||
}
|
||||
|
||||
.consult-avatar-char {
|
||||
@include serif-number;
|
||||
font-size: var(--tk-font-body-sm);
|
||||
font-weight: 700;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.consult-avatar-active .consult-avatar-char {
|
||||
color: var(--tk-pri);
|
||||
}
|
||||
|
||||
.consult-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.consult-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--tk-gap-2xs);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.consult-doctor {
|
||||
font-size: var(--tk-font-cap);
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.consult-type-tag {
|
||||
font-size: var(--tk-font-micro);
|
||||
font-weight: 400;
|
||||
color: $tx3;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.consult-time {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: var(--tk-text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.consult-preview {
|
||||
font-size: var(--tk-font-cap);
|
||||
.ai-chat-welcome__pill-text {
|
||||
font-size: 13px;
|
||||
color: $tx2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── 对话区域 ─── */
|
||||
.ai-chat-body {
|
||||
flex: 1;
|
||||
margin-right: var(--tk-gap-xs);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.consult-badge {
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: $r-pill;
|
||||
background: $dan;
|
||||
@include flex-center;
|
||||
padding: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.consult-badge-text {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: $white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 通知卡片 ─── */
|
||||
.notify-card {
|
||||
/* ─── 消息行 ─── */
|
||||
.ai-msg {
|
||||
display: flex;
|
||||
gap: var(--tk-gap-sm);
|
||||
align-items: flex-start;
|
||||
// ContentCard 接管 background, border-radius, padding, box-shadow
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
|
||||
&--self {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&--ai {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.notify-card-muted {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.notify-icon {
|
||||
/* ─── AI 头像 ─── */
|
||||
.ai-msg__avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: $r-sm;
|
||||
@include flex-center;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, $pri, $pri-d);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notify-icon-char {
|
||||
@include serif-number;
|
||||
font-size: var(--tk-font-body-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notify-type-appointment,
|
||||
.notify-type-points {
|
||||
background: var(--tk-pri-l);
|
||||
color: var(--tk-pri);
|
||||
}
|
||||
|
||||
.notify-type-alert {
|
||||
background: $wrn-l;
|
||||
color: $wrn;
|
||||
}
|
||||
|
||||
.notify-type-followup,
|
||||
.notify-type-report {
|
||||
background: $acc-l;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
.notify-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notify-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--tk-gap-2xs);
|
||||
}
|
||||
|
||||
.notify-title {
|
||||
font-size: var(--tk-font-cap);
|
||||
font-weight: 400;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.notify-title-bold {
|
||||
.ai-msg__avatar-char {
|
||||
color: $white;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notify-time {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: var(--tk-text-secondary);
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--tk-gap-xs);
|
||||
/* ─── 消息气泡 ─── */
|
||||
.ai-msg__bubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 14px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--ai {
|
||||
background: $card;
|
||||
border-radius: 4px 16px 16px 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&--self {
|
||||
background: $pri-l;
|
||||
border-radius: 16px 4px 16px 16px;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.notify-desc {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: $tx2;
|
||||
line-height: 1.5;
|
||||
.ai-msg__text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
color: $tx;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.notify-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: $r-xs;
|
||||
background: var(--tk-pri);
|
||||
flex-shrink: 0;
|
||||
margin-top: var(--tk-gap-2xs);
|
||||
/* ─── 打字指示器 ─── */
|
||||
.ai-msg__typing {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.ai-msg__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: $tx3;
|
||||
animation: ai-typing-pulse 1.4s infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ai-typing-pulse {
|
||||
0%, 80%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── 底部输入栏 ─── */
|
||||
.ai-chat-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
||||
background: $card;
|
||||
border-top: 1px solid $bd-l;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-chat-bar__input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
background: $surface-alt;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 0 14px;
|
||||
font-size: 14px;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.ai-chat-bar__placeholder {
|
||||
color: $tx3;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ai-chat-bar__send {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: $pri;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:active:not(&--disabled) {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-chat-bar__send-icon {
|
||||
color: $white;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user