tcm_exam.rb 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. # encoding:utf-8
  2. class TcmExam < ActiveRecord::Base
  3. has_paper_trail
  4. self.table_name = "tcm_exams"
  5. belongs_to :wx_user
  6. belongs_to :scale_user, :foreign_key => :scale_user_id
  7. STATUS_ENUM = [["待处理", "pending"], ["处理中", "processing"], ["已完成", "completed"], ["失败", "failed"]]
  8. def wx_user_contact
  9. return "-" if wx_user.blank?
  10. user = wx_user.user
  11. return "-" if user.blank?
  12. contact = user.tel
  13. contact = user.email if contact.blank?
  14. contact.blank? ? "-" : contact
  15. end
  16. def scale_user_name
  17. return "-" if scale_user.blank?
  18. scale_user.nick_name || "-"
  19. end
  20. # 解析 AI 报告内容
  21. def parsed_ai_summary
  22. return {} if ai_summary.blank?
  23. begin
  24. JSON.parse(ai_summary)
  25. rescue
  26. {}
  27. end
  28. end
  29. # 解析观察图片
  30. def parsed_observation_images
  31. return {} if observation_images.blank?
  32. begin
  33. JSON.parse(observation_images)
  34. rescue
  35. {}
  36. end
  37. end
  38. # 解析问诊答案
  39. def parsed_question_answers
  40. return {} if question_answers.blank?
  41. begin
  42. JSON.parse(question_answers)
  43. rescue
  44. {}
  45. end
  46. end
  47. # 解析最新体脂秤结果
  48. def parsed_latest_scale_result
  49. return {} if latest_scale_result.blank?
  50. begin
  51. JSON.parse(latest_scale_result)
  52. rescue
  53. {}
  54. end
  55. end
  56. # 状态标签
  57. def status_label
  58. case status
  59. when "pending" then "待处理"
  60. when "processing" then "处理中"
  61. when "completed" then "已完成"
  62. when "failed" then "失败"
  63. else status || "-"
  64. end
  65. end
  66. # 虚拟方法:调养与建议
  67. def advice_section
  68. parsed_ai_summary["advice"] || "-"
  69. end
  70. # 虚拟方法:望诊记录
  71. def observation_section
  72. parsed_ai_summary["observation"] || "-"
  73. end
  74. # 格式化内容,处理换行和 markdown 格式
  75. def format_content(content)
  76. return "-" if content.blank?
  77. # 处理换行符
  78. formatted = content.to_s
  79. .gsub(/\\n/, "<br>")
  80. .gsub(/\n/, "<br>")
  81. .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>') # **粗体**
  82. .gsub(/\*([^*]+)\*/, '<em>\1</em>') # *斜体*
  83. formatted
  84. end
  85. # 格式化调养与建议的嵌套 JSON 结构
  86. def format_advice(advice)
  87. return "-" if advice.blank?
  88. # 如果是字符串,尝试解析为 JSON
  89. if advice.is_a?(String)
  90. begin
  91. advice = JSON.parse(advice)
  92. rescue
  93. return format_content(advice)
  94. end
  95. end
  96. # 如果不是 Hash,直接格式化
  97. return format_content(advice.to_s) unless advice.is_a?(Hash)
  98. # 定义分类标签和图标
  99. category_info = {
  100. '饮食' => { icon: '🍵', color: '#059669' },
  101. '运动' => { icon: '🏃', color: '#2563eb' },
  102. '作息' => { icon: '🌙', color: '#7c3aed' },
  103. '情志' => { icon: '💆', color: '#db2777' }
  104. }
  105. html = ''
  106. advice.each do |category, items|
  107. info = category_info[category] || { icon: '📌', color: '#6b7280' }
  108. html += %Q{<div style="margin-bottom: 16px;">}
  109. html += %Q{<div style="font-weight: 600; color: #{info[:color]}; margin-bottom: 8px; font-size: 15px;">#{info[:icon]} #{category}</div>}
  110. html += %Q{<ul style="margin: 0; padding-left: 20px; list-style-type: disc;">}
  111. if items.is_a?(Array)
  112. items.each do |item|
  113. html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(item)}</li>}
  114. end
  115. else
  116. html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(items)}</li>}
  117. end
  118. html += '</ul></div>'
  119. end
  120. html
  121. end
  122. rails_admin do
  123. navigation_label '中医诊疗记录'
  124. parent ScaleDevice
  125. weight 3
  126. list do
  127. filters [:wx_user, :scale_user, :status, :created_at]
  128. field :id do
  129. label 'ID'
  130. end
  131. field :wx_user_id do
  132. label '微信用户ID'
  133. end
  134. field :wx_user do
  135. label '微信用户'
  136. pretty_value do
  137. bindings[:object].wx_user_contact
  138. end
  139. end
  140. field :scale_user_id do
  141. label '称用户ID'
  142. end
  143. field :scale_user do
  144. label '称用户名'
  145. pretty_value do
  146. bindings[:object].scale_user_name
  147. end
  148. end
  149. field :status do
  150. label '状态'
  151. pretty_value do
  152. bindings[:object].status_label
  153. end
  154. end
  155. field :ai_summary do
  156. label '报告详情'
  157. formatted_value do
  158. exam = bindings[:object]
  159. bindings[:view].link_to('查看报告', bindings[:view].rails_admin.show_path(model_name: 'tcm_exam', id: exam.id), class: 'btn btn-info btn-sm')
  160. end
  161. end
  162. field :created_at do
  163. label '创建时间'
  164. end
  165. end
  166. show do
  167. field :id
  168. field :wx_user_id do
  169. label '微信用户ID'
  170. end
  171. field :wx_user do
  172. label '微信用户'
  173. pretty_value do
  174. bindings[:object].wx_user_contact
  175. end
  176. end
  177. field :scale_user_id do
  178. label '称用户ID'
  179. end
  180. field :scale_user do
  181. label '称用户名'
  182. pretty_value do
  183. bindings[:object].scale_user_name
  184. end
  185. end
  186. field :status do
  187. label '状态'
  188. pretty_value do
  189. bindings[:object].status_label
  190. end
  191. end
  192. # 望诊图片
  193. field :observation_images do
  194. label '望诊图片'
  195. pretty_value do
  196. exam = bindings[:object]
  197. images = exam.parsed_observation_images
  198. return "-" if images.empty?
  199. image_labels = {
  200. 'tongue' => '舌面照',
  201. 'tongueBottom' => '舌底照',
  202. 'face' => '面部照',
  203. 'body' => '全身照'
  204. }
  205. html = '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 10px 0;">'
  206. ['tongue', 'tongueBottom', 'face', 'body'].each do |key|
  207. urls = images[key]
  208. next if urls.blank? || urls.empty?
  209. url = urls.first
  210. next if url.blank?
  211. html += %Q{
  212. <div style="text-align: center;">
  213. <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}')" />
  214. <div style="margin-top: 6px; font-size: 12px; color: #5d4037; font-weight: 600;">#{image_labels[key] || key}</div>
  215. </div>
  216. }
  217. end
  218. html += '</div>'
  219. html.html_safe
  220. end
  221. end
  222. # AI 报告内容 - 一般情况
  223. field :ai_summary do
  224. label '壹 · 一般情况'
  225. pretty_value do
  226. exam = bindings[:object]
  227. summary = exam.parsed_ai_summary
  228. content = summary['general'] || summary['summary'] || '-'
  229. %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
  230. end
  231. end
  232. # 问诊
  233. field :question_answers do
  234. label '贰 · 问诊 (十问歌)'
  235. pretty_value do
  236. exam = bindings[:object]
  237. summary = exam.parsed_ai_summary
  238. content = summary['inquiry'] || '-'
  239. %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
  240. end
  241. end
  242. # 望诊与闻诊 - 使用虚拟字段确保显示
  243. field :observation_section do
  244. label '叁 · 望诊与闻诊'
  245. pretty_value do
  246. exam = bindings[:object]
  247. summary = exam.parsed_ai_summary
  248. content = summary['observation'] || '-'
  249. %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
  250. end
  251. end
  252. # 总体评估与体质辨识
  253. field :latest_scale_result do
  254. label '肆 · 总体评估与体质辨识'
  255. pretty_value do
  256. exam = bindings[:object]
  257. summary = exam.parsed_ai_summary
  258. content = summary['constitution'] || '-'
  259. %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
  260. end
  261. end
  262. # 调养与建议 - 格式化嵌套 JSON
  263. field :advice_section do
  264. label '伍 · 调养与建议'
  265. pretty_value do
  266. exam = bindings[:object]
  267. summary = exam.parsed_ai_summary
  268. advice = summary['advice']
  269. return "-" if advice.blank?
  270. formatted = exam.format_advice(advice)
  271. %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
  272. end
  273. end
  274. # 总结
  275. field :model do
  276. label '陆 · 总结'
  277. pretty_value do
  278. exam = bindings[:object]
  279. summary = exam.parsed_ai_summary
  280. content = summary['summary']
  281. return "-" if content.blank?
  282. %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
  283. end
  284. end
  285. field :created_at do
  286. label '创建时间'
  287. end
  288. field :updated_at do
  289. label '更新时间'
  290. end
  291. end
  292. # 禁用编辑和删除,只允许查看
  293. edit do
  294. field :status, :enum do
  295. enum do
  296. STATUS_ENUM
  297. end
  298. end
  299. end
  300. end
  301. end