Sfoglia il codice sorgente

健康档案、报告、调理

cc12458 1 anno fa
parent
commit
06bc951fb5
100 ha cambiato i file con 2896 aggiunte e 25 eliminazioni
  1. 2 0
      miniprogram/app.config.ts
  2. 46 4
      miniprogram/app.json
  3. 0 9
      miniprogram/app.scss
  4. 4 12
      miniprogram/app.ts
  5. BIN
      miniprogram/assets/bg/body-model-frame.bg.png
  6. BIN
      miniprogram/assets/bg/body-model.bg.png
  7. BIN
      miniprogram/assets/bg/button-block-1.bg.png
  8. BIN
      miniprogram/assets/bg/button-block-2.bg.png
  9. BIN
      miniprogram/assets/bg/button-line-1.bg.png
  10. BIN
      miniprogram/assets/bg/button-line-2.bg.png
  11. BIN
      miniprogram/assets/bg/home-banner.bg.png
  12. BIN
      miniprogram/assets/bg/user-info.bg.png
  13. BIN
      miniprogram/assets/icon/arrows-down.icon.png
  14. BIN
      miniprogram/assets/icon/fab.png
  15. BIN
      miniprogram/assets/icon/gender-0.icon.png
  16. BIN
      miniprogram/assets/icon/gender-1.icon.png
  17. 4 0
      miniprogram/components/button/button.json
  18. 67 0
      miniprogram/components/button/button.scss
  19. 22 0
      miniprogram/components/button/button.ts
  20. 9 0
      miniprogram/components/button/button.wxml
  21. 8 0
      miniprogram/components/form-picker/form-picker.json
  22. 81 0
      miniprogram/components/form-picker/form-picker.scss
  23. 122 0
      miniprogram/components/form-picker/form-picker.ts
  24. 21 0
      miniprogram/components/form-picker/form-picker.wxml
  25. 9 0
      miniprogram/components/form-picker/picker.wxs
  26. 5 0
      miniprogram/components/form-ruler/form-ruler.json
  27. 111 0
      miniprogram/components/form-ruler/form-ruler.scss
  28. 98 0
      miniprogram/components/form-ruler/form-ruler.ts
  29. 17 0
      miniprogram/components/form-ruler/form-ruler.wxml
  30. 6 0
      miniprogram/components/popup-privacy/popup-privacy.json
  31. 35 0
      miniprogram/components/popup-privacy/popup-privacy.scss
  32. 92 0
      miniprogram/components/popup-privacy/popup-privacy.ts
  33. 25 0
      miniprogram/components/popup-privacy/popup-privacy.wxml
  34. 6 0
      miniprogram/components/user-avatar/user-avatar.json
  35. 1 0
      miniprogram/components/user-avatar/user-avatar.scss
  36. 8 0
      miniprogram/components/user-avatar/user-avatar.ts
  37. 2 0
      miniprogram/components/user-avatar/user-avatar.wxml
  38. 43 0
      miniprogram/core/behavior/dictionaries.behavior.ts
  39. 16 0
      miniprogram/core/behavior/draggableSheet.behavior.ts
  40. 24 0
      miniprogram/core/behavior/page-container.behavior.ts
  41. 43 0
      miniprogram/core/behavior/page-loading.behavior.ts
  42. 25 0
      miniprogram/core/behavior/tickle.behavior.ts
  43. 23 0
      miniprogram/core/wxs/dictionary.wxs
  44. 9 0
      miniprogram/core/wxs/field-status.wxs
  45. 2 0
      miniprogram/global.scss
  46. 41 0
      miniprogram/lib/logic.ts
  47. 9 0
      miniprogram/lib/promise.ts
  48. 44 0
      miniprogram/lib/request/create.ts
  49. 19 0
      miniprogram/lib/request/method.ts
  50. 40 0
      miniprogram/lib/request/upload.ts
  51. 50 0
      miniprogram/lib/use/use-phone.ts
  52. 3 0
      miniprogram/lib/use/use-privacy.ts
  53. 37 0
      miniprogram/lib/wx/network.ts
  54. 19 0
      miniprogram/lib/wx/open-api.ts
  55. BIN
      miniprogram/module/health/assets/icon/health-index.icon.png
  56. BIN
      miniprogram/module/health/assets/icon/health-patient.icon.png
  57. BIN
      miniprogram/module/health/assets/icon/health-report.icon.png
  58. BIN
      miniprogram/module/health/assets/icon/health-status-1.icon.png
  59. BIN
      miniprogram/module/health/assets/icon/health-status-2.icon.png
  60. BIN
      miniprogram/module/health/assets/image/health-patient.png
  61. BIN
      miniprogram/module/health/assets/image/health-report.png
  62. BIN
      miniprogram/module/health/assets/image/health-scheme.png
  63. 7 0
      miniprogram/module/health/components/field-ruler/field-ruler.json
  64. 36 0
      miniprogram/module/health/components/field-ruler/field-ruler.scss
  65. 41 0
      miniprogram/module/health/components/field-ruler/field-ruler.ts
  66. 29 0
      miniprogram/module/health/components/field-ruler/field-ruler.wxml
  67. 10 0
      miniprogram/module/health/components/report-health-index/report-health-index.json
  68. 102 0
      miniprogram/module/health/components/report-health-index/report-health-index.scss
  69. 11 0
      miniprogram/module/health/components/report-health-index/report-health-index.ts
  70. 36 0
      miniprogram/module/health/components/report-health-index/report-health-index.wxml
  71. 12 0
      miniprogram/module/health/components/report-health-patient/report-health-patient.json
  72. 65 0
      miniprogram/module/health/components/report-health-patient/report-health-patient.scss
  73. 46 0
      miniprogram/module/health/components/report-health-patient/report-health-patient.ts
  74. 93 0
      miniprogram/module/health/components/report-health-patient/report-health-patient.wxml
  75. 6 0
      miniprogram/module/health/components/report-health-scheme/report-health-scheme.json
  76. 56 0
      miniprogram/module/health/components/report-health-scheme/report-health-scheme.scss
  77. 28 0
      miniprogram/module/health/components/report-health-scheme/report-health-scheme.ts
  78. 19 0
      miniprogram/module/health/components/report-health-scheme/report-health-scheme.wxml
  79. 11 0
      miniprogram/module/health/components/report-health-status/report-health-status.json
  80. 42 0
      miniprogram/module/health/components/report-health-status/report-health-status.scss
  81. 43 0
      miniprogram/module/health/components/report-health-status/report-health-status.ts
  82. 59 0
      miniprogram/module/health/components/report-health-status/report-health-status.wxml
  83. 13 0
      miniprogram/module/health/pages/home/home.json
  84. 32 0
      miniprogram/module/health/pages/home/home.scss
  85. 118 0
      miniprogram/module/health/pages/home/home.ts
  86. 45 0
      miniprogram/module/health/pages/home/home.wxml
  87. 12 0
      miniprogram/module/health/pages/report/report.json
  88. 88 0
      miniprogram/module/health/pages/report/report.scss
  89. 62 0
      miniprogram/module/health/pages/report/report.ts
  90. 111 0
      miniprogram/module/health/pages/report/report.wxml
  91. 12 0
      miniprogram/module/health/pages/scheme/scheme.json
  92. 44 0
      miniprogram/module/health/pages/scheme/scheme.scss
  93. 41 0
      miniprogram/module/health/pages/scheme/scheme.ts
  94. 32 0
      miniprogram/module/health/pages/scheme/scheme.wxml
  95. 9 0
      miniprogram/module/health/pages/status/status.json
  96. 16 0
      miniprogram/module/health/pages/status/status.scss
  97. 75 0
      miniprogram/module/health/pages/status/status.ts
  98. 15 0
      miniprogram/module/health/pages/status/status.wxml
  99. 57 0
      miniprogram/module/health/report-common.scss
  100. 114 0
      miniprogram/module/health/request.ts

+ 2 - 0
miniprogram/app.config.ts

@@ -0,0 +1,2 @@
+export const Base_URL = `https://wx.hzliuzhi.com/manager/fdhb-mobile` as const;
+export const Upload_URL = `https://wx.hzliuzhi.com/manager/file` as const;

+ 46 - 4
miniprogram/app.json

@@ -1,15 +1,52 @@
 {
   "pages": [
-    "pages/index/index",
-    "pages/logs/logs"
+    "pages/home/home"
   ],
+  "subpackages": [
+    {
+      "name": "chats",
+      "root": "module/chats",
+      "pages": [
+        "pages/index/index",
+        "pages/analysis/analysis"
+      ]
+    },
+    {
+      "name": "health",
+      "root": "module/health",
+      "pages": [
+        "pages/home/home",
+        "pages/report/report",
+        "pages/status/status",
+        "pages/scheme/scheme",
+        "pages/record-index/record-index"
+      ]
+    },
+    {
+      "name": "user",
+      "root": "module/user",
+      "pages": [
+        "pages/user-certification/user-certification",
+        "pages/user-edit/user-edit"
+      ]
+    }
+  ],
+  "preloadRule": {
+    "module/chats/pages/index/index": {
+      "packages": [
+        "chats"
+      ]
+    }
+  },
   "window": {
-    "navigationBarTextStyle": "black",
+    "navigationBarBackgroundColor": "#0F2226",
+    "navigationBarTextStyle": "white",
     "navigationStyle": "custom"
   },
   "rendererOptions": {
     "skyline": {
       "defaultDisplayBlock": true,
+      "defaultContentBox": true,
       "disableABTest": true,
       "sdkVersionBegin": "3.0.0",
       "sdkVersionEnd": "15.255.255"
@@ -17,5 +54,10 @@
   },
   "componentFramework": "glass-easel",
   "sitemapLocation": "sitemap.json",
-  "lazyCodeLoading": "requiredComponents"
+  "lazyCodeLoading": "requiredComponents",
+  "usingComponents": {
+    "t-navbar": "tdesign-miniprogram/navbar/navbar",
+    "t-message": "tdesign-miniprogram/message/message",
+    "popup-privacy": "./components/popup-privacy/popup-privacy"
+  }
 }

+ 0 - 9
miniprogram/app.scss

@@ -1,10 +1 @@
 /**app.wxss**/
-.container {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-between;
-  padding: 200rpx 0;
-  box-sizing: border-box;
-} 

+ 4 - 12
miniprogram/app.ts

@@ -1,18 +1,10 @@
+import { login } from "./lib/logic"
+// import { Get } from "./lib/request/method";
+
 // app.ts
 App<IAppOption>({
   globalData: {},
   onLaunch() {
-    // 展示本地存储能力
-    const logs = wx.getStorageSync('logs') || []
-    logs.unshift(Date.now())
-    wx.setStorageSync('logs', logs)
-
-    // 登录
-    wx.login({
-      success: res => {
-        console.log(res.code)
-        // 发送 res.code 到后台换取 openId, sessionKey, unionId
-      },
-    })
+    login();
   },
 })

BIN
miniprogram/assets/bg/body-model-frame.bg.png


BIN
miniprogram/assets/bg/body-model.bg.png


BIN
miniprogram/assets/bg/button-block-1.bg.png


BIN
miniprogram/assets/bg/button-block-2.bg.png


BIN
miniprogram/assets/bg/button-line-1.bg.png


BIN
miniprogram/assets/bg/button-line-2.bg.png


BIN
miniprogram/assets/bg/home-banner.bg.png


BIN
miniprogram/assets/bg/user-info.bg.png


BIN
miniprogram/assets/icon/arrows-down.icon.png


BIN
miniprogram/assets/icon/fab.png


BIN
miniprogram/assets/icon/gender-0.icon.png


BIN
miniprogram/assets/icon/gender-1.icon.png


+ 4 - 0
miniprogram/components/button/button.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 67 - 0
miniprogram/components/button/button.scss

@@ -0,0 +1,67 @@
+.button {
+  position: relative;
+  text-align: center;
+
+  &.block {
+    width: 100%;
+    height: 62px;
+
+    .button__bg {
+      height: 100%;
+      margin: auto;
+    }
+
+    .button__inner,
+    .button__text {
+      position: absolute;
+      top: 10px;
+      left: 0;
+      width: 100%;
+      height: 62px - 10px * 2;
+    }
+  }
+
+  &.line-1,
+  &.line-2 {
+    box-sizing: border-box;
+
+    .button__bg {
+      width: 100%;
+      height: 100%;
+    }
+
+    .button__inner,
+    .button__text {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+
+    .button__text {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      padding: 4px 12px;
+      box-sizing: border-box;
+    }
+  }
+
+  &.line-1 {
+    width: 85px;
+    height: 28px;
+    font-size: var(--button-line-1, 12px);
+  }
+
+  &.line-2 {
+    width: 90px;
+    height: 35px;
+    font-size: 10px;
+  }
+
+  .button__inner {
+    padding: 0;
+    opacity: 0;
+  }
+}

+ 22 - 0
miniprogram/components/button/button.ts

@@ -0,0 +1,22 @@
+// components/button/button.ts
+Component({
+  behaviors: ['wx://form-field-button'],
+  lifetimes: {
+    attached() {
+      const mode = this.data.block ? 'block' : 'line';
+      const index = this.data.index;
+      this.setData({ 
+        src: `../../assets/bg/button-${mode}-${index}.bg.png`,
+        className: this.data.block ? 'block' : `line-${index}`,
+      })
+    }
+  },
+  properties: {
+    block: { type: Boolean, value: false },
+    index: { type: Number, value: 1 },
+  },
+  data: {
+    src: '',
+  },
+  methods: {}
+})

+ 9 - 0
miniprogram/components/button/button.wxml

@@ -0,0 +1,9 @@
+<view class="button {{className}}">
+  <image wx:if="{{src}}" class="button__bg" src="{{src}}" mode="aspectFit" />
+  <view class="button__text">
+    <text max-lines="{{index}}" overflow="fade">
+      <slot></slot>
+    </text>
+  </view>
+  <button class="button__inner" form-type="submit"></button>
+</view>

+ 8 - 0
miniprogram/components/form-picker/form-picker.json

@@ -0,0 +1,8 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "usingComponents": {
+    "t-popup": "tdesign-miniprogram/popup/popup",
+    "t-icon": "tdesign-miniprogram/icon/icon"
+  }
+}

+ 81 - 0
miniprogram/components/form-picker/form-picker.scss

@@ -0,0 +1,81 @@
+/* components/form-picker/form-picker.wxss */
+.form-picker {
+  &__header {
+    display: flex;
+    align-items: center;
+    height: 116rpx;
+
+    .title {
+      flex: 1;
+      text-align: center;
+      font-weight: 600;
+      font-size: 36rpx;
+      color: var(--td-text-color-primary);
+    }
+
+    .btn {
+      font-size: 32rpx;
+      padding: 32rpx;
+
+      &--cancel {
+        color: var(--td-text-color-secondary);
+      }
+
+      &--confirm {
+        color: var(--primary-color);
+      }
+    }
+  }
+
+  &__content {
+    padding: 0 12px;
+    box-sizing: border-box;
+  }
+}
+
+
+.card {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 4px;
+  font-size: 28rpx;
+  text-align: center;
+  border-radius: 12rpx;
+  overflow: hidden;
+  box-sizing: border-box;
+  background-color: var(--td-bg-color-secondarycontainer);
+  border: 1px solid var(--td-bg-color-container, #fff);
+
+  &--active {
+    border-color: var(--td-bg-color-container, #9ea1a5);
+
+    &::after {
+      content: '';
+      display: block;
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 0;
+      height: 0;
+      border-width: 28px 28px 28px 0;
+      border-style: solid;
+      border-color: var(--td-bg-color-container) transparent transparent transparent;
+      border: 14px solid var(--td-bg-color-container, #0052d9);
+      border-bottom-color: transparent;
+      border-right-color: transparent;
+    }
+  }
+
+  &--disabled {
+    opacity: 0.5;
+  }
+
+  &__icon {
+    color: var(--primary-color, #fff);
+    position: absolute;
+    left: 1.5px;
+    top: 1.5px;
+    z-index: 1;
+  }
+}

+ 122 - 0
miniprogram/components/form-picker/form-picker.ts

@@ -0,0 +1,122 @@
+// components/form-picker/form-picker.ts
+interface Option {
+  label: string;
+  value: any;
+  checked?: boolean;
+  disabled?: boolean;
+  mutex?: boolean;
+};
+
+
+const filtration = (selected: string[] | Set<string>, options: Option[], option?: Option): string[] => {
+  const _selected = new Set(selected);
+  if (option) {
+    if (_selected.has(option.value)) {
+      _selected.delete(option.value);
+    } else {
+      if (option.mutex) {
+        _selected.clear();
+      } else {
+        for (const option of options) {
+          if (option.mutex) _selected.delete(option.value);
+        }
+      }
+      _selected.add(option.value);
+    }
+  }
+
+  return [..._selected];
+}
+
+const reset = (values: Option[] | Option | string, options: Option[]) => {
+  const selected = new Set<string>();
+  const _values = Array.isArray(values) ? values : values ? [values] : [];
+  for (const item of _values) {
+    const value = typeof item === 'object' ? item?.value : item;
+    if (value && options.find(item => item.value === value)) selected.add(value);
+  }
+  for (const option of options) {
+    if (option.checked) selected.add(option.value);
+  }
+  return filtration(selected, options);
+}
+
+Component({
+  options: {
+    multipleSlots: true,
+  },
+  lifetimes: {
+    attached() {
+      console.log('attached');
+
+    }
+  },
+  /**
+   * 组件的属性列表
+   */
+  properties: {
+    visible: { type: Boolean, value: false },
+    title: { type: String, value: '' },
+    value: { type: Array, value: [] },
+    options: { type: Array, value: [] },
+    optionsColumns: { type: Number, value: 1 },
+    itemHeight: { type: Number, value: 64 },
+    multiple: { type: Boolean, value: true },
+    closeOnOverlayClick: { type: Boolean, value: true },
+  },
+
+  /**
+   * 组件的初始数据
+   */
+  data: {
+    containerHeight: 350,
+    gap: 8,
+    selected: [] as any[],
+  },
+  observers: {
+    'options,optionsColumns,itemHeight'(options, columns, height) {
+      const rows = Math.ceil(options.length / columns);
+      this.setData({ containerHeight: Math.min(rows * height + (rows - 1) * this.data.gap, 350) })
+    },
+    'value, options'(values: Option[] | Option | string, options: Option[]) {
+      this.setData({ selected: reset(values, options) });
+    }
+  },
+  /**
+   * 组件的方法列表
+   */
+  methods: {
+    handle(event: WechatMiniprogram.TouchEvent) {
+      const index = event.currentTarget.dataset.index;
+      const option = this.data.options[index];
+      if (option.disabled) return;
+      if (this.data.multiple) {
+        this.setData({ selected: filtration(this.data.selected, this.data.options, option) });
+      } else {
+        this.setData({ selected: this.data.selected.includes(option.value) ? [] : [option.value] });
+      }
+    },
+    onConfirm() {
+      const get = (option: Option) => option ? ({ label: option.label, value: option.value }) : null;
+      this.setData({ visible: false })
+      this.triggerEvent('confirm', {
+        selected: this.data.selected,
+        options: this.data.selected
+          .map(value => get(this.data.options.find(item => item.value === value)))
+          .filter(Boolean),
+      })
+      this.triggerEvent('close', { trigger: 'confirm-btn' });
+    },
+    onCancel() {
+      this.setData({ visible: false });
+      this.triggerEvent('cancel');
+      this.triggerEvent('close', { trigger: 'cancel-btn' });
+    },
+    onVisibleChange(event: any) {
+      if (!event.detail.visible) {
+        this.triggerEvent('close', { trigger: event.detail.trigger })
+        setTimeout(() => { this.setData({ selected: reset(this.data.value, this.data.options) }); }, 100)
+      };
+    }
+  }
+})

+ 21 - 0
miniprogram/components/form-picker/form-picker.wxml

@@ -0,0 +1,21 @@
+<!--components/form-picker/form-picker.wxml-->
+<wxs src="./picker.wxs" module="_" />
+<view class="form-picker">
+  <t-popup t-class="form-picker__inner" visible="{{ visible }}" bind:visible-change="onCancel" placement="bottom" close-on-overlay-click="{{closeOnOverlayClick}}" bind:visible-change="onVisibleChange">
+    <view class="form-picker__header">
+      <view class="btn btn--cancel" aria-role="button" bind:tap="onCancel">取消</view>
+      <view class="title">{{title}}</view>
+      <view class="btn btn--confirm" aria-role="button" bind:tap="onConfirm">确定</view>
+    </view>
+    <view class="form-picker__content">
+      <scroll-view type="custom" scroll-y style="height: {{containerHeight}}px;">
+        <grid-builder list="{{options}}" cross-axis-count="{{optionsColumns}}" cross-axis-gap="{{gap}}" main-axis-gap="{{gap}}">
+          <view slot:item slot:index class="card {{_.getClassName(selected, item)}}" style="height: {{itemHeight}}px;" data-index="{{index}}" bind:tap="handle">
+            <t-icon wx:if="{{_.contain(selected, item.value)}}" name="check" t-class="card__icon" ariaHidden="{{true}}" />
+            <text overflow="ellipsis" max-lines="3">{{item.label}}</text>
+          </view>
+        </grid-builder>
+      </scroll-view>
+    </view>
+  </t-popup>
+</view>

+ 9 - 0
miniprogram/components/form-picker/picker.wxs

@@ -0,0 +1,9 @@
+module.exports.contain = function contain(selected, value) {
+  return selected.indexOf(value) > -1;
+}
+
+module.exports.getClassName = function (selected, option) {
+  if (!option) return '';
+  if (option.disabled) return 'card--disabled';
+  return selected.indexOf(option.value) > -1 ? 'card--active' : '';
+}

+ 5 - 0
miniprogram/components/form-ruler/form-ruler.json

@@ -0,0 +1,5 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "usingComponents": {}
+}

+ 111 - 0
miniprogram/components/form-ruler/form-ruler.scss

@@ -0,0 +1,111 @@
+/* components/form-ruler/form-ruler.wxss */
+
+// 定义一个混合,用于生成三角形
+@mixin triangle($size, $color, $direction) {
+  height: 0;
+  width: 0;
+
+  border-color: transparent;
+  border-style: solid;
+  border-width: $size * 0.5;
+
+  @if $direction=='up' {
+    border-bottom-color: $color;
+  }
+
+  @else if $direction=='right' {
+    border-left-color: $color;
+  }
+
+  @else if $direction=='down' {
+    border-top-color: $color;
+  }
+
+  @else if $direction=='left' {
+    border-right-color: $color;
+  }
+
+  @else {
+    @error "Unknown direction #{$direction}.";
+  }
+}
+.show-data {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: flex-end;
+  margin: 12px 0;
+  font-size: 12px;
+  .value {
+    margin: 0 4px;
+    font-size: 18px;
+  }
+}
+.form-ruler {
+  --height: 56px;
+  --width: 100%;
+  --active-color: #34A76B;
+  --tick-width: 5px;
+  position: relative;
+  width: calc(var(--width) + 2px);
+  height: calc(var(--height) + 2px);
+  border: 1px solid #D3D4D5;
+  border-radius: var(--height);
+  box-sizing: border-box;
+  overflow: hidden;
+
+  &::before,
+  &::after {
+    content: '';
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+
+  &::before {
+    top: 0;
+    @include triangle(14px, var(--active-color), 'down');
+  }
+
+  &::after {
+    bottom: 0;
+    @include triangle(14px, var(--active-color), 'up');
+  }
+
+  &__inner {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 100%;
+  }
+
+  &__axis {
+    position: relative;
+    height: 32px;
+    transform: translateY(calc(var(--height) * 0.25));
+    font-size: 12px;
+    width: 5px;
+    box-sizing: border-box;
+
+    text {
+      position: absolute;
+      left: 2px;
+      bottom: -10px;
+    }
+
+    .tick {
+      height: calc(var(--height) * 0.2);
+      border-left: 1px solid #D3D4D5;
+    }
+
+    .median {
+      height: calc(var(--height) * 0.32);
+      border-left: 1px solid #D3D4D5;
+    }
+
+    .integer {
+      height: calc(var(--height) * 0.5);
+      border-left: 1px solid #D3D4D5;
+    }
+  }
+}

+ 98 - 0
miniprogram/components/form-ruler/form-ruler.ts

@@ -0,0 +1,98 @@
+// components/form-ruler/form-ruler.ts
+type Axis = {
+  value: number;
+  type?: 'integer' | 'median' | 'tick';
+}[]
+const prefix = 't-' as const;
+const createAxis = (min = 0, max = 10, precision = 0.1) => {
+  // const length = precision.toString().match(/\d+(?:\.(\d*))/)?.[1]?.length ?? 0
+  // 计算精度的倒数(即1除以精度),然后乘以10的幂来得到一个整数
+  const scale = Math.pow(10, Math.ceil(Math.log10(1 / precision)));
+  const _min = min * scale;
+  const _max = Math.floor(max * scale);
+  const _precision = precision * scale;
+
+  const ticks: Axis = [];
+  let current = _min;
+  while (current < _max + _precision) {
+    ticks.push({
+      value: current / scale,
+      type: Math.floor(current) % 10 ? Math.floor(current) % 5 ? 'tick' : 'median' : 'integer',
+    });
+    current += _precision;
+  }
+  return ticks;
+}
+
+Component({
+  options: {
+    multipleSlots: true,
+  },
+  properties: {
+    value: { type: Number, value: Number.NaN },
+    min: { type: Number, value: 0 },
+    max: { type: Number, value: 1 },
+    precision: { type: Number, value: 0.1 },
+    defaultValue: { type: Number, value: Number.NaN }
+  },
+  observers: {
+    'min, max, precision'(...args: [number, number, number]) {
+      this._updateAxis(args);
+    },
+    'defaultValue,min, max'(value, min, max) {
+      if (!value || Number.isNaN(value)) {
+        value = Math.floor((max - min) / 2 + min)
+        // this.setData({ value });
+      }
+      this._scrollValue(value);
+    }
+  },
+  lifetimes: {
+    attached() {
+      this._updateRect();
+    },
+  },
+  data: {
+    prefix,
+    initialValue: '',
+    axis: [] as Axis,
+    rect: { width: 0, height: 0, gap: 5 }
+  },
+
+  /**
+   * 组件的方法列表
+   */
+  methods: {
+    _updateAxis(params: [number, number, number]) {
+      const axis = createAxis.apply(null, params);
+      this.setData({ axis })
+    },
+    _updateRect() {
+      this.createSelectorQuery()
+        .select('.form-ruler')
+        .boundingClientRect()
+        .exec(res => {
+          const rect = res[0] as WechatMiniprogram.BoundingClientRectResult;
+          const width = rect.width - 2;
+          const height = rect.height - 2;
+          this.setData({ 'rect.width': width, 'rect.height': height, });
+        })
+    },
+    _updateValue(index?: number) {
+      const value = this.data.axis[index ?? 0]?.value
+      this.setData({ value })
+    },
+    _scrollValue(value: number) {
+      const index = this.data.axis.findIndex(item => item.value === value);
+      clearTimeout((this as any).lock);
+      (this as any).lock = setTimeout(() => { this.setData({ initialValue: `${prefix}${index}` }); }, 300);
+    },
+    onScrollUpdate(event: WechatMiniprogram.ScrollViewScroll) {
+      const left = event.detail.scrollLeft;
+      if (left >= 0) { this._updateValue(Math.floor(left / this.data.rect.gap)); }
+    },
+    onScrollEnd() {
+      this._scrollValue(this.data.value);
+    }
+  }
+})

+ 17 - 0
miniprogram/components/form-ruler/form-ruler.wxml

@@ -0,0 +1,17 @@
+<!--components/form-ruler/form-ruler.wxml-->
+<view class="show-data">
+  <slot name="before"></slot>
+  <text class="value">{{value}}</text>
+  <slot name="after"></slot>
+</view>
+<view class="form-ruler" style="--tick-width:{{rect.gap}}px;">
+  <scroll-view class="form-ruler__inner scrollable" type="list" scroll-x enhanced enable-flex show-scrollbar="{{false}}" scroll-into-view="{{initialValue}}" scroll-into-view-alignment="center" scroll-into-view-offset="{{rect.gap / -2}}" 	scroll-into-view-within-extent="{{false}}" bindscroll="onScrollUpdate" 	bind:scrollend="onScrollEnd">
+    <view id="min" class="form-ruler__offset-left" style="width:{{rect.width / 2}}px;"></view>
+    <view wx:for="{{axis}}" wx:key="{{item.id}}" class="form-ruler__axis" id="{{prefix + index}}">
+      <view class=" {{item.type}}">
+        <text wx:if="{{item.type === 'integer'}}">{{item.value}}</text>
+      </view>
+    </view>
+    <view id="max" class="form-ruler__offset-right" style="width:{{(rect.width - rect.gap) / 2}}px;"></view>
+  </scroll-view>
+</view>

+ 6 - 0
miniprogram/components/popup-privacy/popup-privacy.json

@@ -0,0 +1,6 @@
+{
+  "component": true,
+  "usingComponents": {
+    "t-popup": "tdesign-miniprogram/popup/popup"
+  }
+}

+ 35 - 0
miniprogram/components/popup-privacy/popup-privacy.scss

@@ -0,0 +1,35 @@
+/* components/popup-privacy/popup-privacy.wxss */
+@import "../../themes/t.scss";
+@import "../../themes/button.scss";
+
+.popup {
+  &__header {
+    padding: 12px;
+    font-size: 18px;
+    text-align: center;
+  }
+  &__content{
+    padding: 0 12px;
+  }
+}
+
+.row {
+  display: flex;
+  flex-direction: row;
+  flex-flow: wrap;
+  margin: 8px 0;
+}
+
+.name {
+  color: var(--primary-color, #38FF6E);
+}
+
+.button {
+  margin: 12px 0;
+
+  &.text {
+    font-size: 12px;
+    text-align: center;
+    color: var(--td-text-color-secondary, #929292);
+  }
+}

+ 92 - 0
miniprogram/components/popup-privacy/popup-privacy.ts

@@ -0,0 +1,92 @@
+// components/popup-privacy/popup-privacy.ts
+import { getPrivacySetting, openPrivacyContract } from "../../lib/wx/open-api";
+let privacyHandler: (resolve: WechatMiniprogram.GeneralCallbackResult) => void;
+let privacyResolves = new Set<WechatMiniprogram.GeneralCallbackResult>();
+let closeOtherPagePopUpHooks = new Set<() => void>();
+
+if (wx.onNeedPrivacyAuthorization) {
+  wx.onNeedPrivacyAuthorization(resolve => {
+    console.log('[log: popup-privacy] 触发 onNeedPrivacyAuthorization');
+    if (typeof privacyHandler === 'function') privacyHandler(resolve);
+  })
+}
+
+Component({
+  lifetimes: {
+    attached() {
+      const hide = () => this.onHide();
+      privacyHandler = resolve => {
+        privacyResolves.add(resolve);
+        this.onShow();
+        closeOtherPagePopUpHooks.forEach(hook => { if (hide !== hook) hook(); })
+      }
+      closeOtherPagePopUpHooks.add(hide);
+      (<any>this).hide = hide;
+    },
+    detached() {
+      closeOtherPagePopUpHooks.delete((<any>this).hide);
+    }
+  },
+  pageLifetimes: {
+    show() {
+      getPrivacySetting().then(({ needAuthorization, privacyContractName }) => {
+        const [app, contract] = privacyContractName.split('小程序');
+        this.triggerEvent('setting', { needAuthorization, privacyContractName })
+        this.setData({
+          appName: app.slice(1),
+          privacyContractName: `《${contract}`
+        });
+        if (this.data.pre && needAuthorization) this.onShow();
+      })
+    }
+  },
+  properties: {
+    pre: { type: Boolean, value: false },
+    show: { type: Boolean, value: false },
+  },
+  data: {
+    visible: false,
+    title: '用户隐私保护提示',
+    appName: '本小程序',
+    privacyContractName: '《隐私政策》'
+  },
+  observers: {
+    'show'(visible) {
+      this.setData({ visible })
+    }
+  },
+  methods: {
+    onShow() {
+      if (!this.data.visible) this.setData({ visible: true });
+    },
+    onHide() {
+      if (this.data.visible) this.setData({ visible: false });
+    },
+    onOpenPrivacyContract() {
+      openPrivacyContract().then();
+    },
+    handleAgree() {
+      console.log('[log: popup-privacy] 触发 handleAgree');
+      privacyResolves.forEach(resolve => {
+        resolve({
+          event: 'agree',
+          buttonId: 'agree-btn'
+        })
+      })
+      privacyResolves.clear();
+      this.onHide();
+      this.triggerEvent('agree')
+    },
+    handleDisagree() {
+      console.log('[log: popup-privacy] 触发 handleDisagree');
+      privacyResolves.forEach(resolve => {
+        resolve({
+          event: 'disagree',
+        })
+      })
+      privacyResolves.clear();
+      this.onHide();
+      this.triggerEvent('disagree')
+    }
+  }
+})

+ 25 - 0
miniprogram/components/popup-privacy/popup-privacy.wxml

@@ -0,0 +1,25 @@
+<!--components/popup-privacy/popup-privacy.wxml-->
+<t-popup placement="bottom" show-overlay="{{true}}" visible="{{visible}}">
+  <view class="popup">
+    <view class="popup__header">{{title}}</view>
+    <view class="popup__content">
+      <view class="row">感谢您对{{appName}}一直以来的信任!</view>
+      <text class="row">
+        <text>为更好地保护您的个人信息安全,请您仔细阅读并理解我们最新更新的</text>
+        <text class="name" bind:tap="onOpenPrivacyContract">{{privacyContractName}}</text>
+        <text>。</text>
+      </text>
+      <text class="row">
+        <text>当您点击同意并开始时用产品服务时,即表示您已阅读并同意以上条款,{{appName}}将严格按照</text>
+        <text class="name" bind:tap="onOpenPrivacyContract">{{privacyContractName}}</text>
+        <text>的各项条款使用和保护您的信息安全,如您点击"不同意",将无法使用本小程序。</text>
+      </text>
+    </view>
+    <view class="button button__line-1">
+      <view class="button__text">同意并继续</view>
+      <image class="button__bg" src="../../assets/bg/button-1.bg.png" mode="aspectFit"></image>
+      <button class="button__inner" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgree">同意并继续</button>
+    </view>
+    <view class="button text" bind:tap="handleDisagree">不同意并退出</view>
+  </view>
+</t-popup>

+ 6 - 0
miniprogram/components/user-avatar/user-avatar.json

@@ -0,0 +1,6 @@
+{
+  "component": true,
+  "usingComponents": {
+    "t-avatar": "tdesign-miniprogram/avatar/avatar"
+  }
+}

+ 1 - 0
miniprogram/components/user-avatar/user-avatar.scss

@@ -0,0 +1 @@
+/* components/user-avatar/user-avatar.wxss */

+ 8 - 0
miniprogram/components/user-avatar/user-avatar.ts

@@ -0,0 +1,8 @@
+// components/user-avatar/user-avatar.ts
+Component({
+  properties: {
+    size: { type: String, value: '42px' },
+  },
+  data: {},
+  methods: {}
+})

+ 2 - 0
miniprogram/components/user-avatar/user-avatar.wxml

@@ -0,0 +1,2 @@
+<!--components/user-avatar/user-avatar.wxml-->
+<t-avatar shape="circle" image="" icon="user" size="{{size}}" />

+ 43 - 0
miniprogram/core/behavior/dictionaries.behavior.ts

@@ -0,0 +1,43 @@
+import { login } from "../../lib/logic";
+import { Get } from "../../lib/request/method";
+interface Dictionary {
+  dictName: string;
+  dictType: string;
+  items: { dictLabel: string; dictValue: string }[]
+}
+export default Behavior({
+  data: {
+    $dictionaries: [] as App.Dictionary[],
+  },
+  lifetimes: {
+    attached() {
+      login()
+        .then(() => Get<App.Dictionary[], Dictionary[]>(`/dict/getDicts`, {
+          shareRequest: true,
+          transform({ data }) {
+            return data.map(item => {
+              return {
+                key: item.dictType,
+                name: item.dictName,
+                options: item.items.map(item => ({
+                  label: item.dictLabel,
+                  value: item.dictValue,
+                  mutex: item.dictLabel === '无' && item.dictValue === '0'
+                }))
+              }
+            })
+          }
+        }))
+        .then(dictionaries => { this.setData({ $dictionaries: dictionaries }); })
+    }
+  },
+  methods: {
+    getDictionaryOptions(key: string) {
+      const { options } = this.data.$dictionaries.find(item => item.key === key) ?? { options: [] };
+      return options
+    },
+    getDictionaryLabel(key: string, value: string) {
+      return this.getDictionaryOptions(key).find(item => item.value === value)?.label ?? ''
+    }
+  }
+})

+ 16 - 0
miniprogram/core/behavior/draggableSheet.behavior.ts

@@ -0,0 +1,16 @@
+const KEY = 'draggableSheetContext' as const;
+export function DraggableSheetBehavior(selector: string) {
+  return Behavior({
+    lifetimes: {
+      created() {
+        this.createSelectorQuery().select(selector).node().exec(res => {
+          (<any>this)[KEY] = res[0].node;
+        })
+      }
+    }
+  })
+}
+
+export function getDraggableSheetContext(this: any) {
+  return this?.[KEY] as WechatMiniprogram.DraggableSheetContext;
+}

+ 24 - 0
miniprogram/core/behavior/page-container.behavior.ts

@@ -0,0 +1,24 @@
+export default Behavior({
+  data: {},
+  lifetimes: {
+    attached() {
+      const { windowWidth, windowHeight, safeArea } = wx.getWindowInfo();
+      const { top, bottom, right } = wx.getMenuButtonBoundingClientRect();
+      const offsetTop = Math.max(0, top - safeArea.top);
+      const bleeding = windowWidth - right;
+      const container = {
+        width: windowWidth,
+        height: windowHeight - bottom - offsetTop,
+        maxHeight: windowHeight - bottom,
+        safeWidth: safeArea.right - safeArea.left,
+        safeHeight: safeArea.bottom - bottom - offsetTop,
+        safeBottomOffset: windowHeight - safeArea.bottom,
+        bleeding,
+      }
+      this.setData({
+        container,
+        containerStyle: Object.entries(container).map(([key, value]) => `--page-container-${key}:${value}px;`).join('')
+      })
+    }
+  }
+})

+ 43 - 0
miniprogram/core/behavior/page-loading.behavior.ts

@@ -0,0 +1,43 @@
+interface Instance<T> {
+  data$?: Promise<T>
+}
+export default PageLoadBehavior();
+
+export function PageLoadBehavior<T extends Record<string, any>>(method?: () => Promise<T>) {
+  return Behavior({
+    data: {
+      loading: false,
+      model: null as T | null,
+    },
+    lifetimes: {
+      created() {
+        if (method) {
+          const context = this as Instance<T>;
+          context.data$ = method();
+        }
+      },
+      attached() {
+        const context = this as Instance<T>;
+        if (context.data$ && typeof context.data$.then === 'function') {
+          let timer = setTimeout(() => this.showLoading(), 300);
+          context.data$
+            .then(model => { this.setData({ model }); })
+            .finally(() => {
+              clearTimeout(timer);
+              if (this.data.loading) this.hideLoading();
+            })
+        }
+      }
+    },
+    methods: {
+      showLoading(title = '加载中', mask?: boolean) {
+        wx.showLoading({ title, mask });
+        this.setData({ loading: true });
+      },
+      hideLoading() {
+        wx.hideLoading();
+        this.setData({ loading: false });
+      }
+    }
+  })
+}

+ 25 - 0
miniprogram/core/behavior/tickle.behavior.ts

@@ -0,0 +1,25 @@
+
+import Message from '../../miniprogram_npm/tdesign-miniprogram/message/index';
+export default Behavior({
+  data: {
+    $messageId: 't-message',
+  },
+  lifetimes: {},
+  methods: {
+    showMessage(type: 'info' | 'success' | 'warning' | 'error', content: string, duration = 3000) {
+      (<any>Message)[type]({
+        selector: `#${this.data.$messageId}`,
+        offset: [90, 32],
+        duration, content,
+      });
+    },
+    showInfoMessage(content: string, duration?: number) { this.showMessage('info', content, duration); },
+    showSuccessMessage(content: string, duration?: number) { this.showMessage('success', content, duration); },
+    showWarnMessage(content: string, duration?: number) { this.showMessage('warning', content, duration); },
+    showErrorMessage(content: string, duration?: number) { this.showMessage('error', content, duration); },
+  }
+});
+
+export function getTickleContext(this: any) {
+  return this as Tickle;
+}

+ 23 - 0
miniprogram/core/wxs/dictionary.wxs

@@ -0,0 +1,23 @@
+function getDictionaryOptions() {
+  if (arguments.length !== 2) return [];
+
+  var keys = arguments[0].map(function (item) { return item.key; });
+  var index = keys.indexOf(arguments[1]);
+
+  return index !== -1 ? arguments[0][index].options : []
+};
+
+function getDictionaryLabel() {
+  if (arguments.length !== 3) return '';
+  var options = getDictionaryOptions(arguments[0], arguments[1]);
+  var keys = options.map(function (item) { return item.value; });
+  var index = keys.indexOf(arguments[2]);
+
+  return index !== -1 ? options[index].label : ''
+};
+
+module.exports = {
+  options: getDictionaryOptions,
+  label: getDictionaryLabel
+};
+

+ 9 - 0
miniprogram/core/wxs/field-status.wxs

@@ -0,0 +1,9 @@
+function getFieldStatusClassName(dirty, model, key) {
+  if (!dirty) return '';
+  return model[key] ? '' : 'has-error'
+};
+
+module.exports = {
+  className: getFieldStatusClassName
+};
+

+ 2 - 0
miniprogram/global.scss

@@ -0,0 +1,2 @@
+$primary-color: #38FF6E;
+$bg-color-container: #0F2226;

+ 41 - 0
miniprogram/lib/logic.ts

@@ -0,0 +1,41 @@
+import { login as _login } from './wx/open-api'
+import { Get } from './request/method';
+
+const TOKEN: Pick<WechatMiniprogram.SetStorageOption, 'key' | 'data' | 'encrypt'> = {
+  key: 'token',
+  encrypt: true,
+  data: '',
+};
+
+export function token() {
+  return TOKEN.data || wx.getStorageSync(TOKEN.key)
+}
+
+export async function login(force?: boolean) {
+  const _token = force ? '' : await wx.checkSession().then(() => token(), () => '');
+  if (_token) return _token;
+  const show = setTimeout(() => wx.showLoading({ title: '登录中' }), 300);
+  try {
+    const { code } = await _login();
+
+    TOKEN.data = await Get<string, { access_token: string }>(`/authManage/login`, {
+      params: { code },
+      transform({ data }) {
+        return data.access_token
+      },
+      shareRequest: !force,
+      meta: { ignoreToken: true }
+    });
+
+    wx.setStorageSync(TOKEN.key, TOKEN.data);
+    return TOKEN.data;
+  } catch (error) {
+    throw error
+  } finally {
+    clearTimeout(show);
+    wx.hideLoading();
+  }
+}
+
+
+

+ 9 - 0
miniprogram/lib/promise.ts

@@ -0,0 +1,9 @@
+export function withResolvers<T, P extends PromiseLike<T> = PromiseLike<T>>() {
+  let resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void;
+  const promise = new Promise<T>((res, rej) => {
+    resolve = res;
+    reject = rej;
+  }) as unknown as P;
+  // @ts-ignore
+  return { promise, resolve, reject };
+}

+ 44 - 0
miniprogram/lib/request/create.ts

@@ -0,0 +1,44 @@
+import { request as _request } from "../wx/network";
+
+const shareRequestCache = new Map<string, IRequestData<any>>();
+
+export function createRequest(option: IRequestCreateConfig) {
+  const { baseURL } = option;
+
+  return async <R, T>(config: IRequestConfig<R, T>) => {
+    let { url, data, params, header, meta, transform = (params: any) => params, shareRequest, ..._config } = config;
+
+    if (config.method === 'GET') {
+      data = params;
+    } else if (params) {
+      const query = Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
+      url += `?${query.join('&')}`
+    }
+
+    const key = url;
+    if (shareRequest && shareRequestCache.has(key)) {
+      return shareRequestCache.get(key) as unknown as PromiseLike<IRequestData<T>>;
+    }
+
+    header ??= {};
+    header['Authorization'] = meta?.ignoreToken ? '' : await option.token?.() ?? '';
+    header['patientId'] = wx.getStorageSync('patientId') ?? '';
+    header['doctorId'] = wx.getStorageSync('doctorId') ?? '';
+
+    const promise = _request<IRequestData<T>>({
+      url: /https?\:\/\//.test(url) ? url : `${baseURL}${url}`,
+      header, data, ..._config,
+    }).then(response => {
+      if (response.statusCode === 200) {
+        if (response.data.code === 200 && response.data.success !== false)
+          return transform({ data: response.data.data, header: response.header });
+        throw { errMsg: response.data.msg || `app:${response.data.code}`, errno: `060202${response.data.code}` }
+      }
+      throw { errMsg: `${response.errMsg}:${response.statusCode}`, errno: `060201${response.statusCode}` }
+    });
+
+    if (shareRequest) shareRequestCache.set(key, <any>promise);
+
+    return promise;
+  }
+}

+ 19 - 0
miniprogram/lib/request/method.ts

@@ -0,0 +1,19 @@
+import { Base_URL } from "../../app.config";
+import { createRequest } from "./create";
+import { token } from "../logic";
+
+
+const Instance = createRequest({
+  baseURL: Base_URL,
+  token: token
+})
+
+export default Instance;
+
+export function Get<R, T>(url: string, config?: Omit<IRequestConfig<R, T>, 'url' | 'method' | 'data'>) {
+  return Instance<R, T>({ ...config, url, method: 'GET' })
+}
+
+export function Post<R, T>(url: string, data?: IRequestConfig<R, T>['data'], config?: Omit<IRequestConfig<R, T>, 'url' | 'method' | 'data'>) {
+  return Instance<R, T>({ ...config, url, method: 'POST', data })
+}

+ 40 - 0
miniprogram/lib/request/upload.ts

@@ -0,0 +1,40 @@
+import { Upload_URL } from "../../app.config";
+import { upload as _upload } from "../wx/network";
+
+import { token } from "../logic";
+
+const Instance = (function createUploadRequest(option: IRequestCreateConfig) {
+  const { baseURL } = option;
+
+  return <R, T>(config: IUploadConfig<R, T>) => {
+    let { url, data, params, header, meta, transform = (params: any) => params, ..._config } = config;
+
+    url ??= `/upload`;
+    header ??= {};
+
+
+    header['Authorization'] = meta?.ignoreToken ? '' : option.token?.() ?? '';
+
+    // header['patientId'] = wx.getStorageSync('patientId') ?? '';
+    // header['doctorId'] = wx.getStorageSync('doctorId') ?? '';
+
+    return _upload({
+      url: /https?\:\/\//.test(url) ? url : `${baseURL}${url}`,
+      header, formData: data, filePath: params.file, name: params.name, ..._config,
+    }, (response => {
+      const data = JSON.parse(response);
+      if (data.code === 200 && data.success !== false)
+        return transform({ data: data.data, header: {} });
+      throw { errMsg: data.msg || `app:${data.code}`, errno: `060402${data.code}` }
+    }));
+  }
+})({
+  baseURL: Upload_URL,
+  token: token
+})
+
+export function upload<R, T>(config: IUploadConfig<R, T>) {
+  return Instance<R, T>(config)
+}
+
+

+ 50 - 0
miniprogram/lib/use/use-phone.ts

@@ -0,0 +1,50 @@
+import { login } from "../logic";
+import request from "../request/method";
+
+type Status = 'loading' | 'pending' | 'success' | 'fail'
+type UpdataStatusCallback = (status: Status) => void
+type UpdataValueCallback = (value: string) => void
+
+export function usePhoneNumber() {
+  const _updataValue: UpdataValueCallback[] = [];
+  const _updateStatus: UpdataStatusCallback[] = [];
+
+  let value = '';
+  let status: Status = 'loading';
+
+  login(false).then(token => token ? 'pending' : 'fail' as const, () => 'fail' as const).then(_status => {
+    status = _status;
+    _updateStatus.forEach(cb => cb(status));
+  })
+
+  return {
+    updateValue(callback: UpdataValueCallback) {
+      _updataValue.push(callback);
+      if (value) callback(value);
+    },
+    updateStatus(callback: UpdataStatusCallback) {
+      _updateStatus.push(callback);
+      callback(status)
+    },
+    getPhoneNumber(event: WechatMiniprogram.ButtonGetPhoneNumber) {
+      const code = event.detail.code;
+      if (!code) return;
+      _updateStatus.forEach(cb => cb('loading'))
+      request({
+        url: `/mobileAccountManage/getAccountPhone`,
+        method: 'GET',
+        params: { code },
+        transform({ data }) { return data; }
+      }).then(phone => {
+        value = phone;
+        status = 'success';
+
+        _updataValue.forEach(cb => cb(value))
+        _updateStatus.forEach(cb => cb(status))
+      }, (error) => {
+        _updateStatus.forEach(cb => cb('pending'))
+        wx.showModal({ title: '获取失败', content: error.errMsg || error.message, showCancel: false })
+      })
+    }
+  }
+}

+ 3 - 0
miniprogram/lib/use/use-privacy.ts

@@ -0,0 +1,3 @@
+export function usePrivacy() {
+  
+}

+ 37 - 0
miniprogram/lib/wx/network.ts

@@ -0,0 +1,37 @@
+import { withResolvers } from "../promise";
+
+type Data = string | Record<string, any> | ArrayBuffer;
+type Option<T extends Data = Data> = Omit<WechatMiniprogram.RequestOption<T>, 'success' | 'fail' | 'complete'>;
+export type Request<T> = PromiseLike<T> & {
+  abort: () => void;
+  onProgressUpdate: (listener: WechatMiniprogram.UploadTaskOnProgressUpdateCallback) => () => void;
+};
+
+export function request<T extends Data>(option: Option<T>) {
+  type Result = WechatMiniprogram.RequestSuccessCallbackResult<T>
+  const { promise, resolve, reject } = withResolvers<Result, Request<Result>>();
+
+  const task = wx.request({ ...option, success: resolve, fail: reject, });
+  promise.abort = () => {
+    task.abort();
+    task.offHeadersReceived();
+    task.offChunkReceived();
+  }
+  return promise
+}
+
+type UploadOption = Omit<WechatMiniprogram.UploadFileOption, 'success' | 'fail' | 'complete'>;
+export function upload<T extends string>(option: UploadOption, fn: (data: string) => T): Request<T> {
+  const { promise, resolve, reject } = withResolvers<T, Request<T>>();
+  const task = wx.uploadFile({ ...option, success(res) { resolve(fn(res.data)); }, fail: reject, });
+  promise.abort = () => {
+    task.abort();
+    task.offHeadersReceived();
+    task.offProgressUpdate();
+  }
+  promise.onProgressUpdate = (listener: WechatMiniprogram.UploadTaskOnProgressUpdateCallback) => {
+    task.onProgressUpdate(listener);
+    return () => task.offProgressUpdate(listener);
+  }
+  return promise;
+}

+ 19 - 0
miniprogram/lib/wx/open-api.ts

@@ -0,0 +1,19 @@
+import { withResolvers } from "../promise"
+
+export function login(timeout?: number) {
+  const { promise, resolve, reject } = withResolvers<WechatMiniprogram.LoginSuccessCallbackResult>();
+  wx.login({ timeout, success: resolve, fail: reject, });
+  return promise;
+}
+
+export function getPrivacySetting() {
+  const { promise, resolve, reject } = withResolvers<WechatMiniprogram.GetPrivacySettingSuccessCallbackResult>();
+  wx.getPrivacySetting({ success: resolve, fail: reject });
+  return promise;
+}
+
+export function openPrivacyContract() {
+  const { promise, resolve, reject } = withResolvers<WechatMiniprogram.GeneralCallbackResult>();
+  wx.openPrivacyContract({ success: resolve, fail: reject });
+  return promise;
+}

BIN
miniprogram/module/health/assets/icon/health-index.icon.png


BIN
miniprogram/module/health/assets/icon/health-patient.icon.png


BIN
miniprogram/module/health/assets/icon/health-report.icon.png


BIN
miniprogram/module/health/assets/icon/health-status-1.icon.png


BIN
miniprogram/module/health/assets/icon/health-status-2.icon.png


BIN
miniprogram/module/health/assets/image/health-patient.png


BIN
miniprogram/module/health/assets/image/health-report.png


BIN
miniprogram/module/health/assets/image/health-scheme.png


+ 7 - 0
miniprogram/module/health/components/field-ruler/field-ruler.json

@@ -0,0 +1,7 @@
+{
+  "component": true,
+  "usingComponents": {
+    "t-popup": "tdesign-miniprogram/popup/popup",
+    "form-ruler": "../../../../components/form-ruler/form-ruler"
+  }
+}

+ 36 - 0
miniprogram/module/health/components/field-ruler/field-ruler.scss

@@ -0,0 +1,36 @@
+@import "../../../../themes/t.popup.scss";
+
+/* module/health/components/field-ruler/field-ruler.wxss */
+.field-picker {
+  width: 100%;
+  height: 100%;
+
+  &__inner {
+    padding: var(--td-cell-vertical-padding, 32rpx) 0;
+    padding-left: var(--padding-left, 0);
+    height: 100%;
+    box-sizing: border-box;
+  }
+
+  .scrollbar {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-end;
+    text-align: right;
+  }
+
+  .placeholder {
+    color: var(--td-text-color-secondary, #939393);
+  }
+}
+
+.scrollbar {
+  .item {
+    display: flex;
+    flex-direction: row;
+  }
+  .item+.item::before {
+    content: '/';
+    margin: 0 4px;
+  }
+}

+ 41 - 0
miniprogram/module/health/components/field-ruler/field-ruler.ts

@@ -0,0 +1,41 @@
+// module/health/components/field-ruler/field-ruler.ts
+Component({
+  behaviors: ['wx://form-field-group'],
+  lifetimes: {
+    attached() { }
+  },
+  properties: {
+    title: { type: String, value: '' },
+    options: { type: Array, value: [] },
+    closeOnOverlayClick: { type: Boolean, value: true },
+  },
+
+  /**
+   * 组件的初始数据
+   */
+  data: {
+    visible: false,
+    selected: [] as (number | undefined)[],
+    scrollValue: [] as number[],
+  },
+  observers: {
+    'options'(options: { value?: number }[]) {
+      const selected = options.map(option => option.value)
+      this.setData({ selected })
+    }
+  },
+  methods: {
+    onShow() {
+      this.setData({ visible: true });
+    },
+    onConfirm() {
+      this.setData({
+        visible: false,
+        selected: [...this.data.scrollValue],
+      })
+    },
+    onCancel() {
+      this.setData({ visible: false })
+    }
+  }
+})

+ 29 - 0
miniprogram/module/health/components/field-ruler/field-ruler.wxml

@@ -0,0 +1,29 @@
+<!--module/health/components/field-ruler/field-ruler.wxml-->
+<view class="field-picker" bind:tap="onShow">
+  <view class="field-picker__inner">
+    <view wx:if="{{selected.length}}" class="scrollbar">
+      <view class="item" wx:for="{{options}}" wx:key="*this">
+        <text name="{{item.name}}">{{selected[index]}}</text>
+        <text style="margin-left: 2px;">{{item.unit}}</text>
+      </view>
+    </view>
+    <view wx:else class=" placeholder">请选择</view>
+  </view>
+</view>
+
+<input style="opacity: 0;" type="digit" wx:for="{{options}}" wx:key="id" name="{{item.id}}" value="{{selected[index]}}" />
+
+
+<t-popup visible="{{visible}}" bind:visible-change="onCancel" placement="bottom" close-on-overlay-click="{{closeOnOverlayClick}}">
+  <view class="popup__header">
+    <view class="btn btn--cancel" aria-role="button" bind:tap="onCancel">取消</view>
+    <view class="title">{{title}}</view>
+    <view class="btn btn--confirm" aria-role="button" bind:tap="onConfirm">确定</view>
+  </view>
+  <view class="popup__content">
+    <form-ruler wx:for="{{options}}" min="{{item.min}}" max="{{item.max}}" precision="{{item.precision}}" model:value="{{scrollValue[index]}}" default-value="{{visible ? selected[index] : 0}}">
+      <view slot="before">{{item.label}}</view>
+      <view slot="after">{{item.unit}}</view>
+    </form-ruler>
+  </view>
+</t-popup>

+ 10 - 0
miniprogram/module/health/components/report-health-index/report-health-index.json

@@ -0,0 +1,10 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "pureDataPattern": "^_",
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-loading": "tdesign-miniprogram/loading/loading",
+    "t-empty": "tdesign-miniprogram/empty/empty"
+  }
+}

+ 102 - 0
miniprogram/module/health/components/report-health-index/report-health-index.scss

@@ -0,0 +1,102 @@
+@import "../../../../themes//t.cell.scss";
+@import "../../report-common.scss";
+/* module/health/components/report-health-index/report-health-index.wxss */
+
+.health-index {
+  $size: 10px;
+  $gap: 12px;
+  background-color: transparent;
+
+
+  &__title {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+
+  &__slider {
+    position: relative;
+    height: $size * 6;
+    font-size: 12px;
+
+    .value {
+      position: absolute;
+      top: $size - 4px;
+      display: flex;
+
+      &.normal {
+        color: #38FF6E;
+        transform: translateX(-50%);
+      }
+
+      &.big {
+        flex-direction: row-reverse;
+        color: #FF611B;
+        transform: translateX(-98%);
+
+        .description {
+          margin-right: 4px;
+        }
+      }
+
+      &.small {
+        flex-direction: row;
+        color: #FF611B;
+        transform: translateX(-2%);
+
+        .description {
+          margin-left: 4px;
+        }
+      }
+    }
+
+    .track {
+      position: absolute;
+      top: $size * 3;
+      height: $size;
+      border-radius: $size;
+
+      &__bg {
+        left: 0;
+        right: 0;
+        background-color: #143731;
+      }
+
+      &__value {
+        background-color: #FF611B;
+      }
+
+      &__scope {
+        background-color: #38FF6E;
+      }
+
+      &__point {
+        width: $size;
+        height: $size;
+        border: 1px solid #38FF6E;
+        border-radius: 50%;
+        box-sizing: content-box;
+        transform: translate(-50%, -1px);
+        background-color: #fff;
+
+        &.small,
+        &.big {
+          border-color: #FF611B;
+        }
+      }
+    }
+
+    .point {
+      position: absolute;
+      top: $size * 4 + 4px;
+
+      &__left {
+        transform: translateX(-50%);
+      }
+
+      &__right {
+        transform: translateX(50%);
+      }
+    }
+  }
+}

+ 11 - 0
miniprogram/module/health/components/report-health-index/report-health-index.ts

@@ -0,0 +1,11 @@
+// module/health/components/report-health-index/report-health-index.ts
+Component({
+  options: {
+    multipleSlots: true,
+  },
+  properties: {
+    dataset: { type: Array, value: [] },
+    loading: { type: Boolean, value: false },
+    message: { type: String, value: '' },
+  },
+})

+ 36 - 0
miniprogram/module/health/components/report-health-index/report-health-index.wxml

@@ -0,0 +1,36 @@
+<!--module/health/components/report-health-index/report-health-index.wxml-->
+<view class="card-wrapper">
+  <t-cell t-class="card-header cell-border-gradient" t-class-title="card-header__title">
+    <block slot="title">
+      <text>指标信息</text>
+      <t-loading wx:if="{{loading}}" theme="spinner" size="20px" class="loading" />
+      <image wx:else class="icon" src="../../assets/icon/health-index.icon.png" mode="heightFix"></image>
+    </block>
+    <block slot="right-icon">
+      <slot name="extra"></slot>
+    </block>
+
+    <view slot="description" wx:if="{{!loading && !dataset.length}}">暂无数据</view>
+  </t-cell>
+
+  <t-cell wx:for="{{dataset}}" t-class="card-body health-index cell-border-gradient" t-class-title="health-index__title" t-class-description="health-index__slider">
+    <block slot="title">
+      <text>{{item.name}}</text>
+      <text style="font-size: 12px;">({{item.unit}})</text>
+    </block>
+    <block slot="description">
+      <view class="value {{item.valueType}}" style="left: {{item.valueOffset}};">
+        <text>{{item.value}}</text>
+        <text class="description">{{item.description}}</text>
+      </view>
+      <view class="track track__bg"></view>
+      <view class="track track__value" style="left: {{item.valueOffsetLeft}};right: {{item.valueOffsetRight}};"></view>
+      <view class="track track__scope" style="left: {{item.scopeOffsetLeft}};right: {{item.scopeOffsetRight}};"></view>
+      <view class="track track__point {{item.valueType}}" style="left: {{item.valueOffset}};"></view>
+      <view class="point point__left" style="left: {{item.scopeOffsetLeft}};">{{item.min}}</view>
+      <view class="point point__right" style="right: {{item.scopeOffsetRight}};">{{item.max}}</view>
+    </block>
+  </t-cell>
+
+  <t-empty wx:if="{{message}}" t-class="empty-wrapper error" icon="info-circle" description="{{message}}" />
+</view>

+ 12 - 0
miniprogram/module/health/components/report-health-patient/report-health-patient.json

@@ -0,0 +1,12 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "pureDataPattern": "^_",
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-tag": "tdesign-miniprogram/tag/tag",
+    "t-loading": "tdesign-miniprogram/loading/loading",
+    "t-empty": "tdesign-miniprogram/empty/empty",
+    "user-avatar": "../../../../components/user-avatar/user-avatar"
+  }
+}

+ 65 - 0
miniprogram/module/health/components/report-health-patient/report-health-patient.scss

@@ -0,0 +1,65 @@
+@import "../../../../themes//t.cell.scss";
+@import "../../report-common.scss";
+/* module/health/components/report-health-patient/report-health-patient.wxss */
+
+.card-body {
+  .row+.row {
+    margin-top: 8px;
+  }
+}
+
+.patient-info-content {
+  --td-cell-description-color: #fff;
+  --td-cell-description-font-size: 12px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  .avatar {
+    flex: none;
+    margin-right: 8px;
+  }
+
+  .content {
+    flex: auto;
+  }
+
+  .title {
+    font-size: 14px;
+  }
+}
+
+.patient-data-wrapper {
+  .card-body {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .content-wrapper {
+      flex: auto;
+    }
+
+    .image-wrapper {
+      flex: none;
+      width: 100px;
+      height: 100px;
+      margin-right: 8px;
+    }
+  }
+
+  .row {
+    display: flex;
+
+    .col {
+      flex: 1 1 50%;
+    }
+  }
+}
+
+.text-item+.text-item::before {
+  content: ',';
+}
+
+.primary {
+  color: #34A76B;
+}

+ 46 - 0
miniprogram/module/health/components/report-health-patient/report-health-patient.ts

@@ -0,0 +1,46 @@
+import DictionariesBehavior from "../../../../core/behavior/dictionaries.behavior";
+
+// module/health/components/report-health-patient/report-health-patient.ts
+Component({
+  behaviors: [DictionariesBehavior],
+  options: {
+    multipleSlots: true,
+  },
+  properties: {
+    dataset: { type: Object },
+    loading: { type: Boolean, value: false },
+    message: { type: String, value: '' },
+  },
+  data: {
+    hobbyFlavor: [] as string[],
+    foodAllergy: [] as string[],
+    specialPeriod: [] as string[],
+    job: [] as string[],
+
+    phone: '',
+  },
+  observers: {
+    'dataset.phone'(value: string) {
+      this.setData({ phone: value?.slice(0, 3).padEnd(12, '*') ?? '' })
+    },
+    'dataset.cardno'(value: string) {
+      this.setData({ cardno: value?.slice(0, 3).padEnd(18, '*') ?? '' })
+    },
+    'dataset.womenSpecialPeriod'(value: string) {
+      const values = value?.split(',') ?? []
+      this.setData({ specialPeriod: values.filter(Boolean) })
+    },
+    'dataset.job'(value: string) {
+      const values = value?.split(',') ?? []
+      this.setData({ job: values.filter(Boolean) })
+    },
+    'dataset.hobbyFlavor'(value: string) {
+      const values = value?.split(',') ?? []
+      this.setData({ hobbyFlavor: values.filter(Boolean) })
+    },
+    'dataset.foodAllergy'(value: string) {
+      const values = value?.split(',') ?? []
+      this.setData({ foodAllergy: values.filter(Boolean) })
+    }
+  }
+})

+ 93 - 0
miniprogram/module/health/components/report-health-patient/report-health-patient.wxml

@@ -0,0 +1,93 @@
+<wxs src="../../../../core/wxs/dictionary.wxs" module="dictionary" />
+
+<!--module/health/components/report-health-patient/report-health-patient.wxml-->
+<view class="card-wrapper">
+  <t-cell t-class="card-header cell-border-gradient" t-class-description="patient-info-content">
+    <block slot="description">
+      <user-avatar class="avatar" size="60px"></user-avatar>
+      <view class="content">
+        <view class="row primary title">{{dataset.name}}</view>
+        <view class="row">
+          <span class="col">
+            <text>个人简介:</text>
+            <text class="text-item">{{dictionary.label($dictionaries, 'sys_user_sex', dataset.sex)}}</text>
+            <text class="text-item">{{dataset.age}}岁</text>
+            <text class="text-item" wx:for="{{specialPeriod}}" wx:key="*this">
+              {{dictionary.label($dictionaries, 'women_special_period', item)}}
+            </text>
+            <text class="text-item" wx:for="{{job}}" wx:key="*this">
+              {{dictionary.label($dictionaries, 'job', item)}}
+            </text>
+            <text class="text-item">手机号:{{phone}}</text>
+            <text class="text-item">身份证号:{{cardno}}</text>
+          </span>
+        </view>
+      </view>
+    </block>
+  </t-cell>
+</view>
+
+<view class="card-wrapper patient-data-wrapper">
+  <t-cell t-class="card-header cell-border-gradient" t-class-title="card-header__title">
+    <block slot="title">
+      <text>基础信息</text>
+      <t-loading wx:if="{{loading}}" theme="spinner" size="20px" class="loading" />
+      <image wx:else class="icon" src="../../assets/icon/health-patient.icon.png" mode="heightFix"></image>
+    </block>
+    <block slot="right-icon">
+      <slot name="extra"></slot>
+    </block>
+
+    <view slot="description" wx:if="{{!loading && !dataset}}">暂无数据</view>
+  </t-cell>
+
+  <view class="card-body">
+    <image class="image-wrapper" src="../../assets/image/health-patient.png" mode="aspectFill" />
+    <view class="content-wrapper">
+      <view class="row title primary">
+        <view>{{dataset.willillStateName}}</view>
+      </view>
+      <view class="row">
+        <span class="col">
+          <text>身高:</text>
+          <text>{{dataset.height}}cm</text>
+        </span>
+        <span class="col">
+          <text>体重:</text>
+          <text>{{dataset.height}}kg</text>
+        </span>
+      </view>
+      <view class="row">
+        <span class="col">
+          <text>饮酒:</text>
+          <text>{{dictionary.label($dictionaries, 'sys_yes_no', dataset.drinkState)}}</text>
+        </span>
+        <span class="col">
+          <text>抽烟:</text>
+          <text>{{dictionary.label($dictionaries, 'sys_yes_no', dataset.smokeState)}}</text>
+        </span>
+      </view>
+      <span class="row">
+        <text>喜好口味:</text>
+        <t-tag variant="outline" wx:for="{{hobbyFlavor}}" wx:key="*this">
+          {{dictionary.label($dictionaries, 'hobby_flavor', item)}}
+        </t-tag>
+        <t-tag variant="outline" wx:if="{{!hobbyFlavor.length}}">无</t-tag>
+      </span>
+      <span class="row">
+        <text>食物过敏:</text>
+        <t-tag variant="outline" wx:for="{{foodAllergy}}" wx:key="*this">
+          {{dictionary.label($dictionaries, 'food_allergy', item)}}
+        </t-tag>
+        <t-tag variant="outline" wx:if="{{!foodAllergy.length}}">无</t-tag>
+      </span>
+      <span class="row">
+        <text class="text-item" wx:if="{{dataset.willillDegreeName}}">{{dataset.willillDegreeName}}</text>
+        <text class="text-item" wx:if="{{dataset.willillSocialName}}">{{dataset.willillSocialName}}</text>
+        <text class="text-item" wx:if="{{dataset.willillFunctionName}}">{{dataset.willillFunctionName}}</text>
+      </span>
+    </view>
+  </view>
+
+  <t-empty wx:if="{{message}}" t-class="empty-wrapper error" icon="info-circle" description="{{message}}" />
+</view>

+ 6 - 0
miniprogram/module/health/components/report-health-scheme/report-health-scheme.json

@@ -0,0 +1,6 @@
+{
+  "renderer": "skyline",
+  "pureDataPattern": "^_",
+  "component": true,
+  "usingComponents": {}
+}

+ 56 - 0
miniprogram/module/health/components/report-health-scheme/report-health-scheme.scss

@@ -0,0 +1,56 @@
+/* module/health/components/report-health-scheme/report-health-scheme.wxss */
+.wrapper {
+  position: relative;
+  margin: 12px calc(var(--page-container-bleeding, 0) + 4px);
+  border-radius: 12px;
+  box-sizing: border-box;
+
+  &__bg {
+    width: 100%;
+  }
+
+  &__content {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    flex-direction: column;
+    padding: 24px 0 12px;
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    overflow: hidden;
+  }
+
+  .scrollable {
+    height: 100%;
+  }
+
+  .row {
+    display: flex;
+    flex-direction: row;
+    padding: 12px;
+
+    &-1 {
+      flex: none;
+      padding: 12px;
+    }
+
+    &-2 {
+      flex: auto;
+      padding-right: 0;
+      overflow: hidden;
+
+      .value {
+        padding-right: 12px;
+        padding-bottom: 6px;
+      }
+    }
+
+    .label {
+      flex: none;
+      width: 100px;
+      color: #37B473;
+    }
+  }
+}

+ 28 - 0
miniprogram/module/health/components/report-health-scheme/report-health-scheme.ts

@@ -0,0 +1,28 @@
+// module/health/components/report-health-scheme/report-health-scheme.ts
+Component({
+  lifetimes: {},
+  properties: {
+    dataset: { type: Object },
+  },
+  data: {
+    wrapper: { width: 'auto', height: 'auto' },
+    show: false,
+  },
+  observers: {
+    'dataset'(data) {
+      const show = !!data;
+      this.setData({ show })
+      if (show) setTimeout(() => this._getContainerRect(), 300);
+    }
+  },
+  methods: {
+    _getContainerRect() {
+      this.createSelectorQuery().select('.wrapper__bg').boundingClientRect().exec(res => {
+        this.setData({
+          'wrapper.width': `${res[0]?.width}px`,
+          'wrapper.height': `${res[0]?.height}px`,
+        })
+      })
+    }
+  }
+})

+ 19 - 0
miniprogram/module/health/components/report-health-scheme/report-health-scheme.wxml

@@ -0,0 +1,19 @@
+<!--module/health/components/report-health-scheme/report-health-scheme.wxml-->
+<view wx:if="{{show}}" class="wrapper" style="width: {{wrapper.width}}; height: {{wrapper.height}};">
+  <image class="wrapper__bg" src="../../assets/image/health-scheme.png" mode="widthFix" />
+  <view class="wrapper__content">
+    <view class="row row-1">
+      <view class="label">调理进程</view>
+      <view>{{dataset.process}}</view>
+    </view>
+    <view class="row row-2">
+      <view class="label">方案内容</view>
+      <scroll-view class="scrollable" type="list" scroll-y bounces="{{false}}">
+        <span class="value" wx:for="{{dataset.types}}" wx:key="*this">
+          <text wx:if="{{item.type}}">{{item.type}}</text>
+          <text wx:if="{{item.summary}}">{{item.summary}}</text>
+        </span>
+      </scroll-view>
+    </view>
+  </view>
+</view>

+ 11 - 0
miniprogram/module/health/components/report-health-status/report-health-status.json

@@ -0,0 +1,11 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "pureDataPattern": "^_",
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-icon": "tdesign-miniprogram/icon/icon",
+    "t-loading": "tdesign-miniprogram/loading/loading",
+    "t-empty": "tdesign-miniprogram/empty/empty"
+  }
+}

+ 42 - 0
miniprogram/module/health/components/report-health-status/report-health-status.scss

@@ -0,0 +1,42 @@
+@import "../../../../themes/t.cell.scss";
+@import "../../report-common.scss";
+
+/* module/health/components/report-health-status/report-health-status.wxss */
+
+
+.status-data-wrapper {
+  font-size: 12px;
+}
+
+.report-data-wrapper {
+  font-size: 14px;
+
+  .card-body {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .content-wrapper {
+      flex: auto;
+    }
+
+    .image-wrapper {
+      flex: none;
+      width: 128px;
+      height: 128px;
+      margin-left: 8px;
+    }
+  }
+
+  .title {
+    text-align: center;
+  }
+
+  .primary {
+    color: #34A76B;
+  }
+}
+
+.text-item+.text-item::before {
+  content: ',';
+}

+ 43 - 0
miniprogram/module/health/components/report-health-status/report-health-status.ts

@@ -0,0 +1,43 @@
+// module/health/components/report-health-status/report-health-status.ts
+function getDraggableSheetContext(this: any) {
+  return this as { scroll: WechatMiniprogram.DraggableSheetContext }
+}
+const { windowHeight } = wx.getSystemInfoSync()
+const menuRect = wx.getMenuButtonBoundingClientRect()
+const menuBottom = menuRect.bottom + 1
+Component({
+  options: {
+    multipleSlots: true,
+  },
+  properties: {
+    dataset: { type: Object },
+    loading: { type: Boolean, value: false },
+    message: { type: String, value: '' },
+  },
+  data: {
+    sheetHeight: windowHeight - menuBottom,
+    initialSize: 0,
+    minSize: 0,
+    maxSize: 1,
+  },
+  lifetimes: {
+    created() {
+      this.createSelectorQuery().select('.draggable-sheet-wrapper').node().exec(res => {
+        getDraggableSheetContext.call(this).scroll = res[0].node;
+      })
+    }
+  },
+  methods: {
+    showList() {
+      console.log('showList', this);
+      
+      getDraggableSheetContext.call(this).scroll.scrollTo({
+        size: 0.5,
+        pixels: 600,
+        animated: true,
+        duration: 300,
+        easingFunction: 'ease'
+      })
+    }
+  }
+})

+ 59 - 0
miniprogram/module/health/components/report-health-status/report-health-status.wxml

@@ -0,0 +1,59 @@
+<!--module/health/components/report-health-status/report-health-status.wxml-->
+<view class="card-wrapper">
+  <t-cell t-class="card-header cell-border-gradient" t-class-title="card-header__title">
+    <block slot="title">
+      <text>健康状况</text>
+      <t-loading wx:if="{{loading}}" theme="spinner" size="20px" class="loading" />
+      <image wx:else class="icon" src="../../assets/icon/health-status-1.icon.png" mode="heightFix"></image>
+    </block>
+    <block slot="right-icon">
+      <slot name="extra-status"></slot>
+    </block>
+
+    <view slot="description" wx:if="{{!loading && !dataset}}">暂无数据</view>
+  </t-cell>
+
+  <view class="card-body status-data-wrapper">
+    <span class="row">
+      <text>症状信息:</text>
+      <text>{{dataset.pickedSymptom}}</text>
+    </span>
+  </view>
+
+  <t-empty wx:if="{{message}}" t-class="empty-wrapper error" icon="info-circle" description="{{message}}" />
+</view>
+
+<view class="card-wrapper report-data-wrapper">
+  <t-cell t-class="card-header cell-border-gradient" t-class-title="card-header__title">
+    <block slot="title">
+      <text>健康分析报告</text>
+      <t-loading wx:if="{{loading}}" theme="spinner" size="20px" class="loading" />
+      <image wx:else class="icon" src="../../assets/icon/health-status-2.icon.png" mode="heightFix"></image>
+    </block>
+    <block slot="right-icon">
+      <slot name="extra-report"></slot>
+    </block>
+
+    <view slot="description" wx:if="{{!loading && !dataset}}">暂无数据</view>
+  </t-cell>
+
+  <view class="card-body">
+    <view class="content-wrapper">
+      <view class="row title primary">
+        <view>{{dataset.willillStateName}}</view>
+      </view>
+      <span class="row">
+        <text class="text-item" wx:if="{{dataset.willillDegreeName}}">{{dataset.willillDegreeName}}</text>
+        <text class="text-item" wx:if="{{dataset.willillSocialName}}">{{dataset.willillSocialName}}</text>
+        <text class="text-item" wx:if="{{dataset.willillFunctionName}}">{{dataset.willillFunctionName}}</text>
+      </span>
+      <span class="row primary">
+        <text>体质:</text>
+        <text>{{dataset.constitutionGroupName}}</text>
+      </span>
+    </view>
+    <image class="image-wrapper" src="../../assets/image/health-report.png" mode="aspectFill" />
+  </view>
+
+  <t-empty wx:if="{{message}}" t-class="empty-wrapper error" icon="info-circle" description="{{message}}" />
+</view>

+ 13 - 0
miniprogram/module/health/pages/home/home.json

@@ -0,0 +1,13 @@
+{
+  "renderer": "skyline",
+  "pureDataPattern": "^_",
+  "component": true,
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-icon": "tdesign-miniprogram/icon/icon",
+    "report-health-index": "../../components/report-health-index/report-health-index",
+    "report-health-status": "../../components/report-health-status/report-health-status",
+    "report-health-patient": "../../components/report-health-patient/report-health-patient",
+    "report-health-scheme": "../../components/report-health-scheme/report-health-scheme"
+  }
+}

+ 32 - 0
miniprogram/module/health/pages/home/home.scss

@@ -0,0 +1,32 @@
+@import '../../../../themes/page.scss';
+@import '../../../../themes/draggable-sheet.scss';
+@import "../../report-common.scss";
+/* module/health/pages/home/home.wxss */
+
+
+
+
+
+.bar {
+  height: 60px;
+  box-sizing: border-box;
+  background-color: #fff;
+  border-top-left-radius: 25px;
+  border-top-right-radius: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding-bottom: 10px;
+  box-shadow: 0px -3px 0 1px rgba(0, 0, 0, 0.12);
+}
+
+.bar-shadow-placeholder {
+  height: 30px;
+}
+
+.indicator {
+  background-color: rgb(190, 186, 186);
+  border-radius: 3px;
+  height: 4px;
+  width: 80px;
+}

+ 118 - 0
miniprogram/module/health/pages/home/home.ts

@@ -0,0 +1,118 @@
+import PageContainerBehavior from "../../../../core/behavior/page-container.behavior";
+import { DraggableSheetBehavior, getDraggableSheetContext } from "../../../../core/behavior/draggableSheet.behavior";
+
+// module/health/pages/home/home.ts
+import { toReportPage, toSchemePage } from "../../router";
+import { healthIndexMethod, healthPatientMethod, healthReportListMethod, healthReportMethod } from "../../request";
+import { healthIndex2Progress } from "../../tools/health-index";
+import { getTickleContext } from "../../../../core/behavior/tickle.behavior";
+
+Component({
+  behaviors: [
+    PageContainerBehavior,
+    DraggableSheetBehavior('.health-report-list'),
+  ],
+  lifetimes: {
+    attached() {
+      this.getHealthPatient();
+      this.getHealthReport();
+      this.getHealthIndex();
+    }
+  },
+  properties: {},
+  observers: {
+    'healthReport.data'(data) {
+      this.setData({ healthId: data?.healthAnalysisReportId })
+    }
+  },
+  data: {
+    healthId: '',
+    healthPatient: { data: null, loading: false, message: '' },
+    healthIndex: { data: [], loading: false, message: '' },
+    healthReport: { data: null, loading: false, message: '' },
+    healthReportList: { data: [], loaded: false },
+  },
+  methods: {
+    async getHealthPatient() {
+      this.setData({ 'healthPatient.loading': true, })
+      try {
+        const data = await healthPatientMethod();
+        this.setData({
+          'healthPatient.data': data,
+          'healthPatient.loading': false,
+        });
+      } catch (error) {
+        this.setData({
+          'healthPatient.data': [],
+          'healthPatient.loading': false,
+          'healthPatient.message': error.errMsg,
+        });
+      }
+    },
+    async getHealthReport(id?: string) {
+      this.setData({ 'healthReport.loading': true, })
+      try {
+        const data = await healthReportMethod(id);
+        this.setData({
+          'healthReport.data': data,
+          'healthReport.loading': false,
+        });
+      } catch (error) {
+        this.setData({
+          'healthReport.data': null,
+          'healthReport.loading': false,
+          'healthReport.message': error.errMsg,
+        });
+      }
+    },
+    async getHealthIndex(id?: string) {
+      this.setData({ 'healthIndex.loading': true, })
+      try {
+        const data = await healthIndexMethod(id);
+        this.setData({
+          'healthIndex.data': healthIndex2Progress(data),
+          'healthIndex.loading': false,
+        });
+      } catch (error) {
+        this.setData({
+          'healthIndex.data': [],
+          'healthIndex.loading': false,
+          'healthIndex.message': error.errMsg,
+        });
+      }
+    },
+
+    async showReportList() {
+      if (!this.data.healthReportList.loaded) {
+        wx.showLoading({ title: '加载中' })
+        try {
+          const data = await healthReportListMethod();
+          this.setData({
+            'healthReportList.data': data,
+            'healthReportList.loaded': true,
+          })
+        } catch (error) {
+          getTickleContext.call(this).showErrorMessage(error.errMsg);
+        }
+        wx.hideLoading();
+      }
+      if (this.data.healthReportList.data.length) {
+        getDraggableSheetContext.call(this).scrollTo({
+          size: 0.5,
+          pixels: 600,
+          animated: true,
+          duration: 300,
+          easingFunction: 'ease'
+        })
+      } else {
+        wx.showToast({ title: '暂无报告记录' })
+      }
+    },
+
+    toSchemePage(event: WechatMiniprogram.TouchEvent) { toSchemePage(event.mark?.id); },
+    toReportPage(event: WechatMiniprogram.TouchEvent) { toReportPage(event.mark?.id); },
+    toHealthIndexListPage() {
+      wx.navigateTo({ url: `/module/health/pages/record-index/record-index` })
+    },
+  }
+})

+ 45 - 0
miniprogram/module/health/pages/home/home.wxml

@@ -0,0 +1,45 @@
+<!--module/health/pages/home/home.wxml-->
+<t-navbar title="健康档案" left-arrow />
+<scroll-view class="page-scroll__container" type="list" scroll-y style="{{containerStyle}}">
+  <report-health-patient dataset="{{healthPatient.data}}" loading="{{healthPatient.loading}}" message="{{healthPatient.message}}"></report-health-patient>
+  <report-health-status dataset="{{healthReport.data}}" loading="{{healthReport.loading}}" message="{{healthReport.message}}" mark:id="{{healthId}}" bind:tap="toReportPage">
+    <view class="extra-warapper" slot="extra-status" catch:tap="">
+      <text>更新记录</text>
+      <t-icon t-class="icon" name="chevron-right-double-s" size="24px" />
+    </view>
+    <view class="extra-warapper" slot="extra-report" catch:tap="showReportList">
+      <text>更新记录</text>
+      <t-icon t-class="icon" name="chevron-right-double-s" size="24px" />
+    </view>
+  </report-health-status>
+  <report-health-scheme dataset="{{healthReport.data.conditProgram}}" mark:id="{{healthId}}" bind:tap="toSchemePage"></report-health-scheme>
+  <report-health-index dataset="{{healthIndex.data}}" loading="{{healthIndex.loading}}" message="{{healthIndex.message}}">
+    <view class="extra-warapper" slot="extra" catch:tap="toHealthIndexListPage">
+      <text>更新记录</text>
+      <t-icon t-class="icon" name="chevron-right-double-s" size="24px" />
+    </view>
+
+  </report-health-index>
+</scroll-view>
+
+<t-message id="{{$messageId}}"></t-message>
+
+<draggable-sheet class="draggable-sheet-wrapper health-report-list" style="height: {{container.height}}px;" initial-child-size="0" min-child-size="0" max-child-size="0.8" snap="{{true}}" snap-sizes="{{[0.5]}}">
+  <scroll-view class="scrollable draggable-sheet-container" type="custom" scroll-y associative-container="draggable-sheet" show-scrollbar="{{false}}" bounces="{{false}}">
+    <sticky-section>
+      <sticky-header>
+        <view class="draggable-sheet-bar">
+          <view class="indicator" />
+          <view class="title">健康分析报告记录</view>
+        </view>
+      </sticky-header>
+      <list-builder list="{{healthReportList.data}}" child-height="90">
+        <block slot:item slot:index>
+          <t-cell t-class="cell-border-gradient" title="{{item.reportTime}}" description="{{item.description}}" arrow mark:id="{{item.id}}" bind:tap="toReportPage">
+            <image slot="image" src="../../assets/image/health-report.png" style="width: 53px;height: 53px;"></image>
+          </t-cell>
+        </block>
+      </list-builder>
+    </sticky-section>
+  </scroll-view>
+</draggable-sheet>

+ 12 - 0
miniprogram/module/health/pages/report/report.json

@@ -0,0 +1,12 @@
+{
+  "renderer": "skyline",
+  "pureDataPattern": "^_",
+  "component": true,
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-icon": "tdesign-miniprogram/icon/icon",
+    "report-health-index": "../../components/report-health-index/report-health-index",
+    "report-health-status": "../../components/report-health-status/report-health-status",
+    "form-button": "../../../../components/button/button"
+  }
+}

+ 88 - 0
miniprogram/module/health/pages/report/report.scss

@@ -0,0 +1,88 @@
+@import '../../../../themes/page.scss';
+@import "../../../../themes/t.cell.scss";
+@import "../../report-common.scss";
+/* module/health/pages/report/report.wxss */
+
+.report-data-wrapper {
+  font-size: 14px;
+
+  .card-body {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .content-wrapper {
+      flex: auto;
+    }
+
+    .image-wrapper {
+      flex: none;
+      width: 128px;
+      height: 128px;
+      margin-left: 8px;
+    }
+  }
+
+  .title {
+    text-align: center;
+  }
+
+  .primary {
+    color: #34A76B;
+  }
+
+  .talbel-wrapper {
+    $border: 1px solid#999;
+    margin-top: 12px;
+    border: $border;
+    border-radius: 8px;
+
+    .talbel-row {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      &+.talbel-row {
+        border-top: $border;
+      }
+
+      .label {
+        flex: none;
+        padding: 4px;
+        width: 100px;
+      }
+
+      .value {
+        float: auto;
+        padding: 4px;
+        border-left: $border;
+      }
+    }
+  }
+}
+
+.picture-wrapper {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+
+  image {
+    margin: 12px;
+    width: 128px;
+    height: 128px;
+  }
+}
+
+.notification-wrapper {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  color: #34A76B;
+  font-weight: 700;
+
+  .icon {
+    color: #FF611B;
+    margin-right: 8px;
+  }
+}

+ 62 - 0
miniprogram/module/health/pages/report/report.ts

@@ -0,0 +1,62 @@
+import PageContainerBehavior from "../../../../core/behavior/page-container.behavior";
+import TickleBehavior, { getTickleContext } from "../../../../core/behavior/tickle.behavior";
+
+// module/health/pages/report/report.ts
+import { toSchemePage } from '../../router';
+import { healthIndexMethod, healthReportMethod } from "../../request";
+import { healthIndex2Progress } from "../../tools/health-index";
+Component({
+  behaviors: [
+    PageContainerBehavior,
+    TickleBehavior
+  ],
+  lifetimes: {
+    attached() {
+      this.getHealthReport(this.data.id);
+      this.getHealthIndex(this.data.id);
+    }
+  },
+  properties: {
+    id: { type: String, value: '' },
+  },
+  data: {
+    dataset: null,
+    schemeId: '',
+    healthIndex: { data: [], loading: false, message: '' },
+  },
+  observers: {
+    'dataset'(data) {
+      const has = data?.isHaveConditioningProgram === 'Y' && data?.isConfirmConditioningProgram === 'Y';
+      this.setData({ schemeId: has ? this.data.id : '' })
+    }
+  },
+  methods: {
+    async getHealthReport(id?: string) {
+      wx.showLoading({ title: '加载中' });
+      try {
+        const dataset = await healthReportMethod(id);
+        this.setData({ dataset });
+      } catch (error) {
+        getTickleContext.call(this).showErrorMessage(error.errMsg, 0);
+      }
+      wx.hideLoading();
+    },
+    async getHealthIndex(id?: string) {
+      this.setData({ 'healthIndex.loading': true, })
+      try {
+        const data = await healthIndexMethod(id);
+        this.setData({
+          'healthIndex.data': healthIndex2Progress(data),
+          'healthIndex.loading': false,
+        });
+      } catch (error) {
+        this.setData({
+          'healthIndex.data': [],
+          'healthIndex.loading': false,
+          'healthIndex.message': error.errMsg,
+        });
+      }
+    },
+    toSchemePage() { toSchemePage(this.data.schemeId); }
+  }
+})

+ 111 - 0
miniprogram/module/health/pages/report/report.wxml

@@ -0,0 +1,111 @@
+<!--module/health/pages/report/report.wxml-->
+<t-navbar title="健康分析报告" left-arrow />
+
+<scroll-view class="page-scroll__container" type="list" scroll-y style="{{containerStyle}}">
+  <view class="card-wrapper">
+    <t-cell t-class="card-header {{schemeId ? '' : 'cell-border-gradient'}}" bordered="{{!schemeId}}">
+      <view slot="title">报告日期:{{dataset.reportTime}}</view>
+      <form-button slot="right-icon" index="1" bind:tap="toSchemePage">调理方案</form-button>
+    </t-cell>
+
+    <view wx:if="{{!schemeId && dataset.isHaveConditioningProgram === 'Y'}}" class="card-body notification-wrapper">
+      <t-icon t-class="icon" name="chat-bubble-error" size="24px" />
+      <text>请找医生获取中医调理方案</text>
+    </view>
+  </view>
+  <view class="card-wrapper report-data-wrapper">
+    <view class="card-body">
+      <view class="content-wrapper">
+        <block wx:if="dataset">
+          <span class="row">
+            <text>结果显示您是:</text>
+            <text>{{dataset.willillStateName}}</text>
+          </span>
+          <span class="row">
+            <text>程度:</text>
+            <text>{{dataset.willillDegreeName}}</text>
+          </span>
+          <span class="row">
+            <text>类型:</text>
+            <text>{{dataset.willillSocialName}}</text>
+          </span>
+          <span class="row">
+            <text>表现:</text>
+            <text>{{dataset.willillFunctionName}}</text>
+          </span>
+          <span class="row">
+            <text>体质:</text>
+            <text>{{dataset.constitutionGroupName}}</text>
+          </span>
+        </block>
+      </view>
+      <image class="image-wrapper" src="../../assets/image/health-report.png" mode="aspectFill" />
+    </view>
+  </view>
+  <view class="card-wrapper report-data-wrapper">
+    <view class="card-body">
+      <view class="content-wrapper">
+        <block wx:if="dataset">
+          <span class="row">
+            <text>体质:</text>
+            <text>{{dataset.constitutionGroupName}}是</text>
+            <text>{{dataset.constitutionGroupDefinition}}</text>
+          </span>
+        </block>
+        <view class="talbel-wrapper">
+          <view class="talbel-row">
+            <view class="label">总体特征</view>
+            <view class="value">{{dataset.constitutionGroupGeneralCharacteristics}}</view>
+          </view>
+          <view class="talbel-row">
+            <view class="label">形体特征</view>
+            <view class="value">{{dataset.constitutionGroupPhysicalCharacteristics}}</view>
+          </view>
+          <view class="talbel-row">
+            <view class="label">常见表现</view>
+            <view class="value">{{dataset.constitutionGroupCommonManifestations}}</view>
+          </view>
+          <view class="talbel-row">
+            <view class="label">发病倾向</view>
+            <view class="value">{{dataset.constitutionGroupDiseaseTendency}}</view>
+          </view>
+          <view class="talbel-row">
+            <view class="label">外界适应能力</view>
+            <view class="value">{{dataset.constitutionGroupPsychicCharacteristics}}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <view class="card-wrapper">
+    <t-cell t-class="card-header cell-border-gradient" title="舌象分析" description="{{dataset.tongueAnalysisResult}}" />
+    <view class="picture-wrapper">
+      <image src="{{dataset.upImg}}" mode="aspectFit" />
+      <image src="{{dataset.downImg}}" mode="aspectFit" />
+    </view>
+  </view>
+  <view class="card-wrapper">
+    <t-cell t-class="card-header cell-border-gradient" title="面象分析" description="{{dataset.faceAnalysisResult}}" />
+    <view class="picture-wrapper">
+      <image src="{{dataset.faceImg}}" mode="aspectFit" />
+    </view>
+  </view>
+  <view class="card-wrapper">
+    <t-cell t-class="card-header cell-border-gradient" title="中医证素" />
+    <view class="card-body">
+      {{dataset.factorItemSummary}}
+    </view>
+  </view>
+  <view class="card-wrapper">
+    <t-cell t-class="card-header cell-border-gradient" title="中医证型" />
+    <view class="card-body">
+      {{dataset.diagnoseSyndromeSummary}}
+    </view>
+  </view>
+  
+  <report-health-index dataset="{{healthIndex.data}}" loading="{{healthIndex.loading}}" message="{{healthIndex.message}}">
+  </report-health-index>
+</scroll-view>
+
+<t-message id="{{$messageId}}"></t-message>

+ 12 - 0
miniprogram/module/health/pages/scheme/scheme.json

@@ -0,0 +1,12 @@
+{
+  "renderer": "skyline",
+  "pureDataPattern": "^_",
+  "component": true,
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "t-icon": "tdesign-miniprogram/icon/icon",
+    "report-health-index": "../../components/report-health-index/report-health-index",
+    "report-health-status": "../../components/report-health-status/report-health-status",
+    "form-button": "../../../../components/button/button"
+  }
+}

+ 44 - 0
miniprogram/module/health/pages/scheme/scheme.scss

@@ -0,0 +1,44 @@
+@import '../../../../themes/page.scss';
+@import "../../../../themes/t.cell.scss";
+@import "../../report-common.scss";
+/* module/health/pages/scheme/scheme.wxss */
+.card-wrapper {
+  --button-line-1: 10px;
+}
+.scheme-wrapper {
+  &+.scheme-wrapper {
+    margin-top: 12px;
+  }
+
+  color: #D7D9DA;
+
+  .name {
+    text-align: center;
+    font-size: 15px;
+    color: #34A76B;
+  }
+
+  .description {
+    margin: 4px 0;
+    font-size: 14px;
+  }
+
+  .media-container {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    margin: 0 -1%;
+
+    .media {
+      flex: none;
+      width: 31%;
+      margin: 0 1%;
+
+      image,
+      video {
+        width: 100%;
+        height: 100px;
+      }
+    }
+  }
+}

+ 41 - 0
miniprogram/module/health/pages/scheme/scheme.ts

@@ -0,0 +1,41 @@
+import PageContainerBehavior from "../../../../core/behavior/page-container.behavior";
+import TickleBehavior, { getTickleContext } from "../../../../core/behavior/tickle.behavior";
+
+// module/health/pages/scheme/scheme.ts
+import { healthSchemeMethod } from "../../request";
+import { toReportPage } from "../../router";
+Component({
+  behaviors: [
+    PageContainerBehavior,
+    TickleBehavior
+  ],
+  lifetimes: {
+    attached() {
+      this.getHealthScheme(this.data.id);
+    }
+  },
+  properties: {
+    id: { type: String, value: '' },
+  },
+  data: {
+    dataset: null,
+    schemeId: '',
+    healthIndex: { data: [], loading: false, message: '' },
+  },
+  observers: {},
+  methods: {
+    async getHealthScheme(id: string) {
+      wx.showLoading({ title: '加载中' });
+      try {
+        const dataset = await healthSchemeMethod(id);
+        this.setData({ dataset });
+      } catch (error) {
+        console.log(error);
+
+        getTickleContext.call(this).showErrorMessage(error.errMsg || error.message, 0);
+      }
+      wx.hideLoading();
+    },
+    toReportPage() { toReportPage(this.data.id); }
+  }
+})

+ 32 - 0
miniprogram/module/health/pages/scheme/scheme.wxml

@@ -0,0 +1,32 @@
+<!--module/health/pages/scheme/scheme.wxml-->
+<t-navbar title="调理方案" left-arrow />
+
+<scroll-view class="page-scroll__container" type="list" scroll-y style="{{containerStyle}}">
+  <view class="card-wrapper">
+    <t-cell t-class="card-header" bordered="{{false}}">
+      <view slot="title">方案日期:{{dataset.reportTime}}</view>
+      <form-button slot="right-icon" index="1" bind:tap="toReportPage">健康分析报告</form-button>
+    </t-cell>
+  </view>
+
+
+  <view class="card-wrapper" wx:for="{{dataset.types}}" wx:key="*this">
+    <t-cell wx:if="{{item.type}}" t-class="card-header cell-border-gradient" title="{{item.type}}" />
+    <view class="card-body">
+      <view class="scheme-wrapper" wx:for="{{item.groups}}" wx:key="*this">
+        <view class="name">{{item.name}}</view>
+        <view class="media-container">
+          <view class="media" wx:for="{{item.media}}" wx:key="*this">
+            <view class="name">{{item.name}}</view>
+            <image wx:if="{{item.type==='picture'}}" src="{{item.mediaUrl}}" mode="aspectFit"></image>
+            <video wx:elif="{{item.type==='video'}}" src="{{item.mediaUrl}}" poster="{{item.imgUrl}}"></video>
+            <view class="description">{{item.description}}</view>
+          </view>
+        </view>
+        <view class="description" wx:for="{{item.descriptions}}" wx:key="*this">{{item}}</view>
+      </view>
+    </view>
+  </view>
+</scroll-view>
+
+<t-message id="{{$messageId}}"></t-message>

+ 9 - 0
miniprogram/module/health/pages/status/status.json

@@ -0,0 +1,9 @@
+{
+  "renderer": "skyline",
+  "component": true,
+  "usingComponents": {
+    "t-cell": "tdesign-miniprogram/cell/cell",
+    "form-button": "../../../../components/button/button",
+    "field-ruler": "../../components/field-ruler/field-ruler"
+  }
+}

+ 16 - 0
miniprogram/module/health/pages/status/status.scss

@@ -0,0 +1,16 @@
+@import '../../../../themes/t.cell.scss';
+@import '../../../../themes/page.scss';
+
+/* module/health/pages/status/status.wxss */
+.page-scroll__container {
+  flex: 0 1 auto;
+  height: var(--page-container-safeHeight, 100vh);
+
+  padding-top: 12px;
+}
+
+.tips {
+  font-size: 12px;
+  color: #ff611b;
+  padding: 12px var(--td-cell-horizontal-padding, 32rpx) 0;
+}

+ 75 - 0
miniprogram/module/health/pages/status/status.ts

@@ -0,0 +1,75 @@
+import PageContainerBehavior from "../../../../core/behavior/page-container.behavior";
+import TickleBehavior, { getTickleContext } from "../../../../core/behavior/tickle.behavior";
+
+import { Post } from "../../../../lib/request/method";
+import { transformHealthIndex2Ruler } from "../../tools/health-index";
+
+// module/health/pages/status/status.ts
+type ResponseData = { id: string, name: string, options: App.Health.Index.Ruler[] }[];
+
+Component({
+  behaviors: [PageContainerBehavior, TickleBehavior],
+  lifetimes: {
+    attached() {
+      this.getData()
+    }
+  },
+  properties: {},
+  data: {
+    health: [] as ResponseData,
+  },
+  methods: {
+    async getData() {
+      wx.showLoading({ title: '加载中' });
+      try {
+        const health = await Post<ResponseData, App.Health.Index.Data[]>(`/patientQuota/getCurQuoval`, {}, {
+          transform({ data }) { return transformHealthIndex2Ruler(data) }
+        });
+        this.setData({ health })
+      } catch (error) {
+        getTickleContext.call(this).showErrorMessage(error.errMsg, 0)
+      }
+      wx.hideLoading()
+    },
+
+    async onSubmit(event: WechatMiniprogram.FormSubmit) {
+      const values = event.detail.value;
+      const model = Object
+        .entries(values)
+        .filter(([, value]) => value)
+        .map(([quotaId, quotaVal]) => ({ quotaId: quotaId, quotaVal: +quotaVal }))
+
+      if (model.length) {
+        wx.showLoading({ title: `提交中` })
+        try {
+          const health = this.data.health
+          const result = await Post(`/patientQuota/updateCurQuoval`, model, {
+            transform() {
+              return health
+                .map(h => {
+                  const value = h.options.map(option => {
+                    const value = values[option.id];
+                    return value && value != option.value ? `${value}${option.unit}` : ''
+                  }).filter(Boolean).join(' / ')
+                  return value ? `${h.name}: ${value}` : ''
+                })
+                .filter(Boolean)
+            }
+          })
+          console.log('[TODO]: result-->', result);
+          this.getOpenerEventChannel().emit('update', result);
+          wx.navigateBack()
+        } catch (error) {
+          getTickleContext.call(this).showErrorMessage(error.errMsg)
+        }
+        wx.hideLoading()
+      } else {
+        wx.showToast({ title: `请至少填写一项`, icon: 'error' })
+      }
+
+      // console.log('[log]:-->', event.detail.value, data);
+
+
+    }
+  }
+})

+ 15 - 0
miniprogram/module/health/pages/status/status.wxml

@@ -0,0 +1,15 @@
+<!--module/health/pages/status/status.wxml-->
+<t-navbar title="指标信息" left-arrow />
+<scroll-view class="page-scroll__container" type="list" scroll-y style="{{containerStyle}}">
+  <form bind:submit="onSubmit">
+    <t-cell wx:for="{{health}}" wx:key="{{item.id}}" t-class="cell-border-gradient cell-field" t-class-note="cell-field__wrapper" title="{{item.name}}" bind:tap="onPicker">
+      <field-ruler class="cell-field__inner full" slot="note" title="{{item.name}}" options="{{item.options}}"></field-ruler>
+    </t-cell>
+    <block wx:if="{{health.length}}">
+      <view class="tips">温馨提示:指标信息越完善,我们分析的越精</view>
+      <form-button block index="2"></form-button>
+    </block>
+  </form>
+</scroll-view>
+
+<t-message id="{{$messageId}}"></t-message>

+ 57 - 0
miniprogram/module/health/report-common.scss

@@ -0,0 +1,57 @@
+.card-wrapper {
+  $gap: 12px;
+  margin: $gap var(--page-container-bleeding, 0);
+  border-radius: $gap;
+  box-shadow: inset 0px 0px $gap * 3 0px rgba(52, 167, 107, 0.38);
+
+  .card-header {
+    --td-cell-title-color: var(--primary-color);
+    --td-loading-color: var(--primary-color);
+    background-color: transparent;
+
+    &__title {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .icon {
+        margin-left: 8px;
+        margin-top: 2px;
+        height: 20px;
+      }
+    }
+
+    .loading {
+      margin-left: 8px;
+    }
+  }
+
+  .card-body {
+    padding: var(--td-cell-vertical-padding, 32rpx) var(--td-cell-horizontal-padding, 32rpx);
+    background-color: transparent;
+
+    .row+.row {
+      margin-top: 8px;
+    }
+  }
+}
+
+.empty-wrapper {
+  padding: 12px;
+  --td-empty-description-color: #939393;
+  --td-empty-description-line-height: var(--td-empty-description-font-size);
+  &.error {
+    --td-empty-icon-color: #FF611B;
+  }
+}
+
+.extra-warapper {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  font-size: 14px;
+  color: var(--primary-color);
+  .icon {
+    margin-top: 2px;
+  }
+}

+ 114 - 0
miniprogram/module/health/request.ts

@@ -0,0 +1,114 @@
+import { Get, Post } from "../../lib/request/method";
+import { createHealthIndex, transformHealthIndex2Chart } from "./tools/health-index";
+import dayjs from "dayjs";
+
+export function healthPatientMethod() {
+  return Post(`/patientInfoManage/getPatientInfoDetail`, {}, { transform({ data }) { return data } });
+}
+
+export function healthReportListMethod() {
+  const transform = ({ data }: AnyObject) => {
+    return Array.isArray(data) ? data.map(item => {
+      return {
+        id: item.healthAnalysisReportId,
+        reportTime: item.time3,
+        description: [item.willillStateName, item.willillDegreeName, item.willillSocialName, item.willillFunctionName].filter(Boolean).join(',')
+      }
+    }) : []
+  }
+  return Get(`/analysisManage/getHarsTid`, { transform })
+}
+
+export function healthReportMethod(id?: string) {
+  const transform = ({ data }: AnyObject) => {
+    if (Array.isArray(data?.conditProgram?.types)) {
+      data.conditProgram.types = data.conditProgram.types.map((item: AnyObject) => (item.summary = item.summary?.replace(/null/g, '') || '', item))
+    }
+    return data;
+  };
+
+  return id
+    ? Get(`/analysisManage/getHealRepDetailById`, { params: { healthAnalysisReportId: id }, transform })
+    : Post(`/analysisManage/getLastHealRepDetail`, {}, { transform })
+}
+
+/**
+ * 获取指标信息
+ * @param id 健康分析报告
+ */
+export function healthIndexMethod(id?: string) {
+  const transform = ({ data }: AnyObject) => Array.isArray(data) ? data.map(item => createHealthIndex(item)) : [];
+
+  return id
+    ? Get(`/analysisManage/getQuovalByHarId`, { params: { healthAnalysisReportId: id }, transform })
+    : Post(`/patientQuota/getCurQuoval`, {}, { transform })
+}
+
+export function healthIndexReportMethod() {
+  return Post(`/patientQuota/getQuovalRecord`, {}, {
+    transform({ data }) { return transformHealthIndex2Chart(<any[]>data) }
+  })
+}
+
+export function healthSchemeMethod(id: string) {
+  const transform = ({ data }: AnyObject) => {
+    return {
+      reportTime: dayjs(data?.time).format('YYYY年MM月DD日'),
+      types: Array.isArray(data?.types) ? data.types.map((item: AnyObject) => {
+        return {
+          type: item.type || '',
+          summary: item.summary?.replace(/null/g, '') || '',
+          groups: item.groups?.map((group: AnyObject) => {
+            const descriptions = group.description?.split('\n').filter(Boolean) ?? [];
+            const media = [] as any[];
+            const items = group.items;
+            if (Array.isArray(items)) {
+              let medicine = [] as any[];
+              const fn = () => {
+                const str = medicine.map(item => [item.name, [item.doase, item.unit].filter(Boolean).join('')].filter(Boolean).join(' ')).join('; ')
+                descriptions.push(`${str}`)
+                medicine = [];
+              }
+              for (const item of items) {
+                const { type, imgUrl, mediaUrl, name, description } = item;
+                if (type !== 'medicine' && medicine.length) fn();
+                switch (type) {
+                  case 'img':
+                    if (mediaUrl || imgUrl) media.push({
+                      type: 'picture',
+                      mediaUrl: mediaUrl ?? imgUrl,
+                      imgUrl: imgUrl ?? mediaUrl,
+                      name: name ?? '',
+                      description: description ?? ''
+                    })
+                    break;
+                  case 'video':
+                    if (mediaUrl) media.push({
+                      type: 'video',
+                      mediaUrl: mediaUrl,
+                      imgUrl: imgUrl,
+                      name: name ?? '',
+                      description: description ?? ''
+                    });
+                    break;
+                  case 'text':
+                    descriptions.push(`${name}: ${description}`)
+                    break;
+                  case 'medicine':
+                    medicine.push(item);
+                    break;
+                }
+              }
+              if (medicine.length) fn();
+            }
+            return {
+              name: group.name,
+              descriptions, media,
+            }
+          })
+        }
+      }) : []
+    }
+  };
+  return Get(`/analysisManage/getCondProgDetailById`, { params: { healthAnalysisReportId: id }, transform })
+}

Some files were not shown because too many files changed in this diff