|
|
@@ -7,6 +7,14 @@ class TcmExam < ActiveRecord::Base
|
|
|
|
|
|
STATUS_ENUM = [["待处理", "pending"], ["处理中", "processing"], ["已完成", "completed"], ["失败", "failed"]]
|
|
|
|
|
|
+ # 大模型名称映射(技术名 → 展示名)
|
|
|
+ AI_MODEL_LABELS = {
|
|
|
+ 'GLM-5.1' => 'GLM-5.1',
|
|
|
+ 'glm-5.1' => 'GLM-5.1',
|
|
|
+ 'gemini-3.1-flash-lite-preview' => 'GLM-5.1',
|
|
|
+ 'gemini-3-pro-preview' => 'GLM-5.1',
|
|
|
+ }.freeze
|
|
|
+
|
|
|
def wx_user_contact
|
|
|
return "-" if wx_user.blank?
|
|
|
user = wx_user.user
|
|
|
@@ -21,6 +29,11 @@ class TcmExam < ActiveRecord::Base
|
|
|
scale_user.nick_name || "-"
|
|
|
end
|
|
|
|
|
|
+ # 诊疗大模型展示名称,默认 GLM-5.1
|
|
|
+ def ai_model_display
|
|
|
+ AI_MODEL_LABELS[model.to_s] || model.presence || 'GLM-5.1'
|
|
|
+ end
|
|
|
+
|
|
|
# 解析 AI 报告内容
|
|
|
def parsed_ai_summary
|
|
|
return {} if ai_summary.blank?
|
|
|
@@ -82,23 +95,25 @@ class TcmExam < ActiveRecord::Base
|
|
|
parsed_ai_summary["observation"] || "-"
|
|
|
end
|
|
|
|
|
|
+ # 虚拟方法:AI 总结段落
|
|
|
+ def summary_section
|
|
|
+ parsed_ai_summary["summary"] || "-"
|
|
|
+ end
|
|
|
+
|
|
|
# 格式化内容,处理换行和 markdown 格式
|
|
|
def format_content(content)
|
|
|
return "-" if content.blank?
|
|
|
- # 处理换行符
|
|
|
formatted = content.to_s
|
|
|
.gsub(/\\n/, "<br>")
|
|
|
.gsub(/\n/, "<br>")
|
|
|
- .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>') # **粗体**
|
|
|
- .gsub(/\*([^*]+)\*/, '<em>\1</em>') # *斜体*
|
|
|
+ .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')
|
|
|
+ .gsub(/\*([^*]+)\*/, '<em>\1</em>')
|
|
|
formatted
|
|
|
end
|
|
|
|
|
|
# 格式化调养与建议的嵌套 JSON 结构
|
|
|
def format_advice(advice)
|
|
|
return "-" if advice.blank?
|
|
|
-
|
|
|
- # 如果是字符串,尝试解析为 JSON
|
|
|
if advice.is_a?(String)
|
|
|
begin
|
|
|
advice = JSON.parse(advice)
|
|
|
@@ -106,71 +121,69 @@ class TcmExam < ActiveRecord::Base
|
|
|
return format_content(advice)
|
|
|
end
|
|
|
end
|
|
|
-
|
|
|
- # 如果不是 Hash,直接格式化
|
|
|
return format_content(advice.to_s) unless advice.is_a?(Hash)
|
|
|
-
|
|
|
- # 定义分类标签和图标
|
|
|
category_info = {
|
|
|
'饮食' => { icon: '🍵', color: '#059669' },
|
|
|
'运动' => { icon: '🏃', color: '#2563eb' },
|
|
|
'作息' => { icon: '🌙', color: '#7c3aed' },
|
|
|
'情志' => { icon: '💆', color: '#db2777' }
|
|
|
}
|
|
|
-
|
|
|
html = ''
|
|
|
advice.each do |category, items|
|
|
|
info = category_info[category] || { icon: '📌', color: '#6b7280' }
|
|
|
- html += %Q{<div style="margin-bottom: 16px;">}
|
|
|
- html += %Q{<div style="font-weight: 600; color: #{info[:color]}; margin-bottom: 8px; font-size: 15px;">#{info[:icon]} #{category}</div>}
|
|
|
- html += %Q{<ul style="margin: 0; padding-left: 20px; list-style-type: disc;">}
|
|
|
-
|
|
|
+ html += %Q{<div style="margin-bottom:12px;">}
|
|
|
+ html += %Q{<div style="font-weight:600;color:#{info[:color]};margin-bottom:6px;font-size:14px;">#{info[:icon]} #{category}</div>}
|
|
|
+ html += %Q{<ul style="margin:0;padding-left:18px;list-style-type:disc;">}
|
|
|
if items.is_a?(Array)
|
|
|
- items.each do |item|
|
|
|
- html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(item)}</li>}
|
|
|
- end
|
|
|
+ items.each { |item| html += %Q{<li style="margin-bottom:4px;line-height:1.5;font-size:13px;">#{format_content(item)}</li>} }
|
|
|
else
|
|
|
- html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(items)}</li>}
|
|
|
+ html += %Q{<li style="margin-bottom:4px;line-height:1.5;font-size:13px;">#{format_content(items)}</li>}
|
|
|
end
|
|
|
-
|
|
|
html += '</ul></div>'
|
|
|
end
|
|
|
-
|
|
|
html
|
|
|
end
|
|
|
|
|
|
rails_admin do
|
|
|
- navigation_label '中医诊疗记录'
|
|
|
+ navigation_label 'AI诊疗记录'
|
|
|
parent ScaleDevice
|
|
|
weight 3
|
|
|
|
|
|
list do
|
|
|
filters [:wx_user, :scale_user, :status, :created_at]
|
|
|
+
|
|
|
field :id do
|
|
|
label 'ID'
|
|
|
- end
|
|
|
- field :wx_user_id do
|
|
|
- label '微信用户ID'
|
|
|
+ column_width 60
|
|
|
end
|
|
|
field :wx_user do
|
|
|
label '微信用户'
|
|
|
- pretty_value do
|
|
|
- bindings[:object].wx_user_contact
|
|
|
- end
|
|
|
- end
|
|
|
- field :scale_user_id do
|
|
|
- label '称用户ID'
|
|
|
+ pretty_value { bindings[:object].wx_user_contact }
|
|
|
end
|
|
|
field :scale_user do
|
|
|
- label '称用户名'
|
|
|
+ label '用户名'
|
|
|
+ pretty_value { bindings[:object].scale_user_name }
|
|
|
+ end
|
|
|
+ field :model do
|
|
|
+ label '诊疗大模型'
|
|
|
+ column_width 130
|
|
|
pretty_value do
|
|
|
- bindings[:object].scale_user_name
|
|
|
+ name = bindings[:object].ai_model_display
|
|
|
+ %Q{<span style="display:inline-flex;align-items:center;gap:5px;background:linear-gradient(135deg,#1a56db,#7e3af2);color:#fff;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:600;letter-spacing:0.3px;">🤖 #{name}</span>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
field :status do
|
|
|
label '状态'
|
|
|
+ column_width 80
|
|
|
pretty_value do
|
|
|
- bindings[:object].status_label
|
|
|
+ label = bindings[:object].status_label
|
|
|
+ color = case bindings[:object].status
|
|
|
+ when 'completed' then '#059669'
|
|
|
+ when 'processing' then '#d97706'
|
|
|
+ when 'failed' then '#dc2626'
|
|
|
+ else '#6b7280'
|
|
|
+ end
|
|
|
+ %Q{<span style="color:#{color};font-weight:600;">#{label}</span>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
field :ai_summary do
|
|
|
@@ -182,61 +195,79 @@ class TcmExam < ActiveRecord::Base
|
|
|
end
|
|
|
field :created_at do
|
|
|
label '创建时间'
|
|
|
+ column_width 150
|
|
|
+ strftime_format '%Y-%m-%d %H:%M'
|
|
|
end
|
|
|
end
|
|
|
|
|
|
show do
|
|
|
- field :id
|
|
|
- field :wx_user_id do
|
|
|
- label '微信用户ID'
|
|
|
- end
|
|
|
- field :wx_user do
|
|
|
- label '微信用户'
|
|
|
- pretty_value do
|
|
|
- bindings[:object].wx_user_contact
|
|
|
- end
|
|
|
- end
|
|
|
- field :scale_user_id do
|
|
|
- label '称用户ID'
|
|
|
- end
|
|
|
- field :scale_user do
|
|
|
- label '称用户名'
|
|
|
- pretty_value do
|
|
|
- bindings[:object].scale_user_name
|
|
|
- end
|
|
|
- end
|
|
|
- field :status do
|
|
|
- label '状态'
|
|
|
+ # ── 顶部信息卡:大模型徽章 + 基础信息 ──────────────────────────────
|
|
|
+ field :id do
|
|
|
+ label '诊疗概览'
|
|
|
pretty_value do
|
|
|
- bindings[:object].status_label
|
|
|
+ exam = bindings[:object]
|
|
|
+ model_name = exam.ai_model_display
|
|
|
+ status_color = case exam.status
|
|
|
+ when 'completed' then '#059669'
|
|
|
+ when 'processing' then '#d97706'
|
|
|
+ when 'failed' then '#dc2626'
|
|
|
+ else '#6b7280'
|
|
|
+ end
|
|
|
+ status_text = exam.status_label
|
|
|
+
|
|
|
+ %Q{
|
|
|
+ <div style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 60%,#1a3c6e 100%);border-radius:14px;padding:18px 22px;margin:4px 0 16px;color:#fff;">
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
|
|
|
+ <div style="background:linear-gradient(135deg,#2563eb,#7c3aed);border-radius:10px;padding:8px 16px;display:inline-flex;align-items:center;gap:6px;">
|
|
|
+ <span style="font-size:16px;">🤖</span>
|
|
|
+ <span style="font-size:18px;font-weight:700;letter-spacing:1px;color:#fff;">#{model_name}</span>
|
|
|
+ </div>
|
|
|
+ <div style="background:rgba(255,255,255,0.12);border-radius:8px;padding:4px 12px;font-size:13px;color:#e2e8f0;">诊疗大模型</div>
|
|
|
+ </div>
|
|
|
+ <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:13px;">
|
|
|
+ <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
|
|
|
+ <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">记录 ID</div>
|
|
|
+ <div style="font-weight:600;">##{exam.id}</div>
|
|
|
+ </div>
|
|
|
+ <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
|
|
|
+ <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">微信用户</div>
|
|
|
+ <div style="font-weight:600;">#{exam.wx_user_contact}</div>
|
|
|
+ </div>
|
|
|
+ <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
|
|
|
+ <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">称用户名</div>
|
|
|
+ <div style="font-weight:600;">#{exam.scale_user_name}</div>
|
|
|
+ </div>
|
|
|
+ <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
|
|
|
+ <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">状态</div>
|
|
|
+ <div style="font-weight:700;color:#{status_color};">#{status_text}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style="margin-top:10px;font-size:12px;color:#64748b;">
|
|
|
+ 创建:#{exam.created_at&.strftime('%Y-%m-%d %H:%M:%S')} | 更新:#{exam.updated_at&.strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ }.html_safe
|
|
|
end
|
|
|
end
|
|
|
-
|
|
|
+
|
|
|
# 望诊图片
|
|
|
field :observation_images do
|
|
|
- label '望诊图片'
|
|
|
+ label '📷 望诊图片'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
images = exam.parsed_observation_images
|
|
|
- return "-" if images.empty?
|
|
|
-
|
|
|
- image_labels = {
|
|
|
- 'tongue' => '舌面照',
|
|
|
- 'tongueBottom' => '舌底照',
|
|
|
- 'face' => '面部照',
|
|
|
- 'body' => '全身照'
|
|
|
- }
|
|
|
-
|
|
|
- html = '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 10px 0;">'
|
|
|
+ return "-".html_safe if images.empty?
|
|
|
+ image_labels = { 'tongue' => '舌面照', 'tongueBottom' => '舌底照', 'face' => '面部照', 'body' => '全身照' }
|
|
|
+ html = '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:6px 0;">'
|
|
|
['tongue', 'tongueBottom', 'face', 'body'].each do |key|
|
|
|
urls = images[key]
|
|
|
next if urls.blank? || urls.empty?
|
|
|
url = urls.first
|
|
|
next if url.blank?
|
|
|
html += %Q{
|
|
|
- <div style="text-align: center;">
|
|
|
- <img src="#{url}" style="width: 100%; max-width: 200px; height: 150px; object-fit: cover; border-radius: 8px; border: 1px solid #e5e7eb; cursor: pointer;" onclick="window.open('#{url}')" />
|
|
|
- <div style="margin-top: 6px; font-size: 12px; color: #5d4037; font-weight: 600;">#{image_labels[key] || key}</div>
|
|
|
+ <div style="text-align:center;">
|
|
|
+ <img src="#{url}" style="width:100%;max-width:180px;height:130px;object-fit:cover;border-radius:8px;border:1px solid #e5e7eb;cursor:pointer;" onclick="window.open('#{url}')" />
|
|
|
+ <div style="margin-top:4px;font-size:11px;color:#5d4037;font-weight:600;">#{image_labels[key]}</div>
|
|
|
</div>
|
|
|
}
|
|
|
end
|
|
|
@@ -245,89 +276,68 @@ class TcmExam < ActiveRecord::Base
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # AI 报告内容 - 一般情况
|
|
|
+ # ── 诊断内容各节(紧凑样式)─────────────────────────────────────────
|
|
|
field :ai_summary do
|
|
|
label '壹 · 一般情况'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- content = summary['general'] || summary['summary'] || '-'
|
|
|
- %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
+ content = exam.parsed_ai_summary['general'] || exam.parsed_ai_summary['summary'] || '-'
|
|
|
+ %Q{<div style="background:#f8fafc;border-left:3px solid #2563eb;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # 问诊
|
|
|
field :question_answers do
|
|
|
label '贰 · 问诊 (十问歌)'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- content = summary['inquiry'] || '-'
|
|
|
- %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
+ content = exam.parsed_ai_summary['inquiry'] || '-'
|
|
|
+ %Q{<div style="background:#f8fafc;border-left:3px solid #7c3aed;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # 望诊与闻诊 - 使用虚拟字段确保显示
|
|
|
field :observation_section do
|
|
|
label '叁 · 望诊与闻诊'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- content = summary['observation'] || '-'
|
|
|
- %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
+ content = exam.parsed_ai_summary['observation'] || '-'
|
|
|
+ %Q{<div style="background:#f8fafc;border-left:3px solid #059669;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # 总体评估与体质辨识
|
|
|
field :latest_scale_result do
|
|
|
label '肆 · 总体评估与体质辨识'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- content = summary['constitution'] || '-'
|
|
|
- %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
+ content = exam.parsed_ai_summary['constitution'] || '-'
|
|
|
+ %Q{<div style="background:#f8fafc;border-left:3px solid #d97706;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # 调养与建议 - 格式化嵌套 JSON
|
|
|
field :advice_section do
|
|
|
label '伍 · 调养与建议'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- advice = summary['advice']
|
|
|
- return "-" if advice.blank?
|
|
|
+ advice = exam.parsed_ai_summary['advice']
|
|
|
+ return "-".html_safe if advice.blank?
|
|
|
formatted = exam.format_advice(advice)
|
|
|
- %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{formatted}</div>}.html_safe
|
|
|
+ %Q{<div style="background:#f8fafc;border-left:3px solid #db2777;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{formatted}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- # 总结
|
|
|
- field :model do
|
|
|
+ field :summary_section do
|
|
|
label '陆 · 总结'
|
|
|
pretty_value do
|
|
|
exam = bindings[:object]
|
|
|
- summary = exam.parsed_ai_summary
|
|
|
- content = summary['summary']
|
|
|
- return "-" if content.blank?
|
|
|
- %Q{<div style="background: #fff7ed; border: 1px solid #fed7aa; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7; color: #9a3412;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
+ content = exam.parsed_ai_summary['summary']
|
|
|
+ return "-".html_safe if content.blank?
|
|
|
+ %Q{<div style="background:#fff7ed;border-left:3px solid #ea580c;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;color:#9a3412;">#{exam.format_content(content)}</div>}.html_safe
|
|
|
end
|
|
|
end
|
|
|
-
|
|
|
- field :created_at do
|
|
|
- label '创建时间'
|
|
|
- end
|
|
|
- field :updated_at do
|
|
|
- label '更新时间'
|
|
|
- end
|
|
|
end
|
|
|
|
|
|
- # 禁用编辑和删除,只允许查看
|
|
|
edit do
|
|
|
field :status, :enum do
|
|
|
- enum do
|
|
|
- STATUS_ENUM
|
|
|
- end
|
|
|
+ enum { STATUS_ENUM }
|
|
|
end
|
|
|
end
|
|
|
end
|