张田田 8 месяцев назад
Родитель
Сommit
ad82de731c
100 измененных файлов с 7215 добавлено и 29 удалено
  1. 6 0
      .idea/vcs.xml
  2. 79 0
      .idea/workspace.xml
  3. 14 0
      miniprogram/app.config.ts
  4. 96 6
      miniprogram/app.json
  5. 5 9
      miniprogram/app.scss
  6. 17 14
      miniprogram/app.ts
  7. BIN
      miniprogram/assets/bg/bg_dialog@2x.png
  8. BIN
      miniprogram/assets/bg/bg_home.png
  9. BIN
      miniprogram/assets/bg/bg_interview.png
  10. BIN
      miniprogram/assets/bg/bg_records.png
  11. BIN
      miniprogram/assets/bg/bg_user@2x.png
  12. BIN
      miniprogram/assets/bg/bg_wave@3x.png
  13. BIN
      miniprogram/assets/bg/health-file.bg.png
  14. BIN
      miniprogram/assets/bg/icon_diet@3x.png
  15. BIN
      miniprogram/assets/bg/icon_file@3x.png
  16. BIN
      miniprogram/assets/bg/icon_notice@3x.png
  17. BIN
      miniprogram/assets/bg/icon_tea@3x.png
  18. BIN
      miniprogram/assets/bg/icon_warning@3x.png
  19. BIN
      miniprogram/assets/bg/pic_body@2x.png
  20. BIN
      miniprogram/assets/icon/all@3x.png
  21. BIN
      miniprogram/assets/icon/arrows-down.icon.png
  22. BIN
      miniprogram/assets/icon/deal@3x.png
  23. BIN
      miniprogram/assets/icon/delivery@3x.png
  24. BIN
      miniprogram/assets/icon/icon_announce@3x.png
  25. BIN
      miniprogram/assets/icon/icon_consult@3x.png
  26. BIN
      miniprogram/assets/icon/icon_footprint@3x.png
  27. BIN
      miniprogram/assets/icon/icon_location@3x.png
  28. BIN
      miniprogram/assets/icon/icon_plan@3x.png
  29. BIN
      miniprogram/assets/icon/obligation@3x.png
  30. BIN
      miniprogram/assets/icon/weather.icon.png
  31. 4 0
      miniprogram/components/button/button.json
  32. 80 0
      miniprogram/components/button/button.scss
  33. 73 0
      miniprogram/components/button/button.ts
  34. 16 0
      miniprogram/components/button/button.wxml
  35. 33 0
      miniprogram/components/calendar/func/config.js
  36. 998 0
      miniprogram/components/calendar/func/convertSolarLunar.js
  37. 543 0
      miniprogram/components/calendar/func/day.js
  38. 375 0
      miniprogram/components/calendar/func/render.js
  39. 182 0
      miniprogram/components/calendar/func/todo.js
  40. 367 0
      miniprogram/components/calendar/func/utils.js
  41. 601 0
      miniprogram/components/calendar/func/week.js
  42. 26 0
      miniprogram/components/calendar/func/wxData.js
  43. 6 0
      miniprogram/components/calendar/index.json
  44. 342 0
      miniprogram/components/calendar/index.scss
  45. 502 0
      miniprogram/components/calendar/index.ts
  46. 98 0
      miniprogram/components/calendar/index.wxml
  47. 878 0
      miniprogram/components/calendar/main.js
  48. 2 0
      miniprogram/components/calendar/theme/iconfont.wxss
  49. 52 0
      miniprogram/components/calendar/theme/theme-default.wxss
  50. 49 0
      miniprogram/components/calendar/theme/theme-elegant.wxss
  51. 8 0
      miniprogram/components/form-picker/form-picker.json
  52. 95 0
      miniprogram/components/form-picker/form-picker.scss
  53. 171 0
      miniprogram/components/form-picker/form-picker.ts
  54. 34 0
      miniprogram/components/form-picker/form-picker.wxml
  55. 9 0
      miniprogram/components/form-picker/picker.wxs
  56. 5 0
      miniprogram/components/form-ruler/form-ruler.json
  57. 112 0
      miniprogram/components/form-ruler/form-ruler.scss
  58. 107 0
      miniprogram/components/form-ruler/form-ruler.ts
  59. 17 0
      miniprogram/components/form-ruler/form-ruler.wxml
  60. 5 0
      miniprogram/components/form/form.json
  61. 22 0
      miniprogram/components/form/form.ts
  62. 3 0
      miniprogram/components/form/form.wxml
  63. 6 0
      miniprogram/components/media-carousel/media-carousel.json
  64. 103 0
      miniprogram/components/media-carousel/media-carousel.scss
  65. 120 0
      miniprogram/components/media-carousel/media-carousel.ts
  66. 39 0
      miniprogram/components/media-carousel/media-carousel.wxml
  67. 6 0
      miniprogram/components/popup-privacy/popup-privacy.json
  68. 36 0
      miniprogram/components/popup-privacy/popup-privacy.scss
  69. 92 0
      miniprogram/components/popup-privacy/popup-privacy.ts
  70. 25 0
      miniprogram/components/popup-privacy/popup-privacy.wxml
  71. 54 0
      miniprogram/components/record-index/record-index.js
  72. 6 0
      miniprogram/components/record-index/record-index.json
  73. 22 0
      miniprogram/components/record-index/record-index.wxml
  74. 21 0
      miniprogram/components/record-index/record-index.wxss
  75. 7 0
      miniprogram/components/tabbar/tabbar.json
  76. 0 0
      miniprogram/components/tabbar/tabbar.scss
  77. 95 0
      miniprogram/components/tabbar/tabbar.ts
  78. 5 0
      miniprogram/components/tabbar/tabbar.wxml
  79. 6 0
      miniprogram/components/user-avatar/user-avatar.json
  80. 1 0
      miniprogram/components/user-avatar/user-avatar.scss
  81. 8 0
      miniprogram/components/user-avatar/user-avatar.ts
  82. 2 0
      miniprogram/components/user-avatar/user-avatar.wxml
  83. 43 0
      miniprogram/core/behavior/dictionaries.behavior.ts
  84. 16 0
      miniprogram/core/behavior/draggableSheet.behavior.ts
  85. 24 0
      miniprogram/core/behavior/page-container.behavior.ts
  86. 43 0
      miniprogram/core/behavior/page-loading.behavior.ts
  87. 25 0
      miniprogram/core/behavior/tickle.behavior.ts
  88. 24 0
      miniprogram/core/wxs/dictionary.wxs
  89. 9 0
      miniprogram/core/wxs/field-status.wxs
  90. 2 0
      miniprogram/global.scss
  91. 42 0
      miniprogram/lib/logic.ts
  92. 9 0
      miniprogram/lib/promise.ts
  93. 94 0
      miniprogram/lib/request/create.ts
  94. 19 0
      miniprogram/lib/request/method.ts
  95. 40 0
      miniprogram/lib/request/upload.ts
  96. 32 0
      miniprogram/lib/use/use-location.ts
  97. 53 0
      miniprogram/lib/use/use-phone.ts
  98. 3 0
      miniprogram/lib/use/use-privacy.ts
  99. 9 0
      miniprogram/lib/wx/location.ts
  100. 42 0
      miniprogram/lib/wx/network.ts

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 79 - 0
.idea/workspace.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AutoImportSettings">
+    <option name="autoReloadType" value="SELECTIVE" />
+  </component>
+  <component name="ChangeListManager">
+    <list default="true" id="ade0a8c5-0639-4f05-956a-aae1181b5d18" name="更改" comment="">
+      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/miniprogram/app.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/miniprogram/app.config.ts" afterDir="false" />
+    </list>
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="Git.Settings">
+    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+  </component>
+  <component name="ProjectColorInfo">{
+  &quot;associatedIndex&quot;: 6
+}</component>
+  <component name="ProjectId" id="2y83hfQWdk0KL1ry1ltAyuUZcIM" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent"><![CDATA[{
+  "keyToString": {
+    "ModuleVcsDetector.initialDetectionPerformed": "true",
+    "RunOnceActivity.ShowReadmeOnStart": "true",
+    "RunOnceActivity.git.unshallow": "true",
+    "git-widget-placeholder": "release/2.0.1",
+    "junie.onboarding.icon.badge.shown": "true",
+    "last_opened_file_path": "/Users/zhangtiantian/Desktop/six-health.applet",
+    "node.js.detected.package.eslint": "true",
+    "node.js.detected.package.tslint": "true",
+    "node.js.selected.package.eslint": "(autodetect)",
+    "node.js.selected.package.tslint": "(autodetect)",
+    "nodejs_package_manager_path": "npm",
+    "ts.external.directory.path": "/Applications/WebStorm.app/Contents/plugins/javascript-plugin/jsLanguageServicesImpl/external",
+    "vue.rearranger.settings.migration": "true"
+  }
+}]]></component>
+  <component name="SharedIndexes">
+    <attachedChunks>
+      <set>
+        <option value="bundled-js-predefined-d6986cc7102b-6a121458b545-JavaScript-WS-251.25410.117" />
+      </set>
+    </attachedChunks>
+  </component>
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="默认任务">
+      <changelist id="ade0a8c5-0639-4f05-956a-aae1181b5d18" name="更改" comment="" />
+      <created>1749202493163</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1749202493163</updated>
+      <workItem from="1749202494220" duration="720000" />
+      <workItem from="1749523832830" duration="2478000" />
+      <workItem from="1754882475936" duration="7606000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+    <option name="exactExcludedFiles">
+      <list>
+        <option value="$PROJECT_DIR$/miniprogram/module/care/pages/careDetail/careDetail.js" />
+        <option value="$PROJECT_DIR$/miniprogram/module/charts/compoments/record-index/record-index.js" />
+        <option value="$PROJECT_DIR$/miniprogram/module/charts/compoments/record-index/chalk.theme.js" />
+        <option value="$PROJECT_DIR$/miniprogram/module/charts/record-care/record-care.js" />
+        <option value="$PROJECT_DIR$/miniprogram/module/charts/record-care/record-care.js" />
+      </list>
+    </option>
+  </component>
+  <component name="UnknownFeatures">
+    <option featureType="com.intellij.fileTypeFactory" implementationName="*.wxml" />
+  </component>
+</project>

+ 14 - 0
miniprogram/app.config.ts

@@ -0,0 +1,14 @@
+import { getAccountInfoSync } from "./lib/wx/open-api";
+
+const miniProgram = getAccountInfoSync();
+const env = miniProgram.envVersion;
+let host = "wx2.hzliuzhi.com";
+if (env === "trial") {
+  host = "test.hzliuzhi.com";
+} else if (env === "develop") {
+  host = "wx.hzliuzhi.com:4433";
+}
+export const Base_URL = `https://${host}/manager/fdhb-mobile`;
+export const Upload_URL = `https://${host}/manager/file`;
+
+

+ 96 - 6
miniprogram/app.json

@@ -1,15 +1,94 @@
 {
-  "pages": [
-    "pages/index/index",
-    "pages/logs/logs"
+  "pages": ["pages/home/home", "pages/mine/mine"],
+  "subpackages": [
+    {
+      "name": "chats",
+      "root": "module/chats",
+      "pages": ["pages/index/index", "pages/analysis/analysis"]
+    },
+    {
+      "name": "health",
+      "root": "module/health",
+      "pages": [
+        "pages/home/home",
+        "pages/analysis/analysis",
+        "pages/report/report",
+        "pages/status/status",
+        "pages/scheme/scheme",
+        "pages/status-record/status-record",
+        "pages/tongue-analysis/tongue-analysis"
+      ]
+    },
+    {
+      "name": "article",
+      "root": "module/article",
+      "pages": [
+        "pages/diet-list/diet-list",
+        "pages/diet-info/diet-info",
+        "pages/science-list/science-list",
+        "pages/science-info/science-info",
+        "pages/manage-address/manage-address",
+        "pages/add-address/add-address",
+        "pages/order-list/order-list",
+        "pages/order-detail/order-detail",
+        "pages/foot-print/foot-print",
+        "pages/evangelism-notice/evangelism-notice",
+        "pages/evangelism-detail/evangelism-detail",
+        "pages/see-logistics/see-logistics",
+        "pages/confirm-receiving/confirm-receiving",
+        "pages/success-page/success-page",
+        "pages/punch-card/punch-card"
+      ]
+    },
+    {
+      "name": "user",
+      "root": "module/user",
+      "pages": [
+        "pages/user-certification/user-certification",
+        "pages/user-edit/user-edit",
+        "pages/user-record/user-record"
+      ]
+    },
+    {
+      "name": "charts",
+      "root": "module/charts",
+      "pages": ["record-index/record-index"]
+    },
+    {
+      "name": "follow",
+      "root": "module/follow",
+      "pages": ["pages/evaluation/report"]
+    },
+    {
+      "name": "care",
+      "root": "module/care",
+      "pages": [
+        "pages/care/verifyRecord",
+        "pages/offlineTreatment/offlineTreatment",
+        "pages/careDetail/careDetail",
+        "pages/reportRecord/reportRecord"
+      ]
+    }
   ],
+  "preloadRule": {
+    "pages/home/home": {
+      "packages": ["chats"]
+    }
+  },
   "window": {
+    "navigationStyle": "custom",
     "navigationBarTextStyle": "black",
-    "navigationStyle": "custom"
+    "navigationBarBackgroundColor": "#ffffff",
+    "backgroundColor": "#ffffff",
+    "backgroundColorContent": "#ffffff",
+    "backgroundColorTop": "#ffffff",
+    "backgroundColorBottom": "#ffffff"
   },
+
   "rendererOptions": {
     "skyline": {
       "defaultDisplayBlock": true,
+      "defaultContentBox": true,
       "disableABTest": true,
       "sdkVersionBegin": "3.0.0",
       "sdkVersionEnd": "15.255.255"
@@ -17,5 +96,16 @@
   },
   "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"
+  },
+  "requiredPrivateInfos": ["getFuzzyLocation", "chooseAddress"],
+  "permission": {
+    "scope.userFuzzyLocation": {
+      "desc": "你的位置信息将用于小程序推荐"
+    }
+  }
+}

+ 5 - 9
miniprogram/app.scss

@@ -1,10 +1,6 @@
 /**app.wxss**/
-.container {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-between;
-  padding: 200rpx 0;
-  box-sizing: border-box;
-} 
+
+page {
+  --td-cell-title-color: #111;
+  --td-navbar-bg-color: transparent;
+}

+ 17 - 14
miniprogram/app.ts

@@ -1,18 +1,21 @@
+import { login } from "./lib/logic"
+import { appUpdate } from "./lib/wx/update";
+import { useRouteQuery } from "./utils/route-query";
+// 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
-      },
-    })
+  globalData: {
+    doctorId: '',
+    patientId: '',
+    dictionaries: [],
+  },
+  onLaunch(options: WechatMiniprogram.App.LaunchShowOption) {
+    appUpdate(true);
+    const query = useRouteQuery(options.query.scene);
+    const doctorId = query.ys;
+    this.globalData.doctorId = doctorId;
+    wx.setStorageSync('doctorId', doctorId);
+    login();
   },
 })

BIN
miniprogram/assets/bg/bg_dialog@2x.png


BIN
miniprogram/assets/bg/bg_home.png


BIN
miniprogram/assets/bg/bg_interview.png


BIN
miniprogram/assets/bg/bg_records.png


BIN
miniprogram/assets/bg/bg_user@2x.png


BIN
miniprogram/assets/bg/bg_wave@3x.png


BIN
miniprogram/assets/bg/health-file.bg.png


BIN
miniprogram/assets/bg/icon_diet@3x.png


BIN
miniprogram/assets/bg/icon_file@3x.png


BIN
miniprogram/assets/bg/icon_notice@3x.png


BIN
miniprogram/assets/bg/icon_tea@3x.png


BIN
miniprogram/assets/bg/icon_warning@3x.png


BIN
miniprogram/assets/bg/pic_body@2x.png


BIN
miniprogram/assets/icon/all@3x.png


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


BIN
miniprogram/assets/icon/deal@3x.png


BIN
miniprogram/assets/icon/delivery@3x.png


BIN
miniprogram/assets/icon/icon_announce@3x.png


BIN
miniprogram/assets/icon/icon_consult@3x.png


BIN
miniprogram/assets/icon/icon_footprint@3x.png


BIN
miniprogram/assets/icon/icon_location@3x.png


BIN
miniprogram/assets/icon/icon_plan@3x.png


BIN
miniprogram/assets/icon/obligation@3x.png


BIN
miniprogram/assets/icon/weather.icon.png


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

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

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

@@ -0,0 +1,80 @@
+.button {
+  position: relative;
+  text-align: center;
+
+  &.disabled {
+    opacity: 0.5;
+    pointer-events: none;
+  }
+
+  &.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;
+      position: absolute;
+      top: 10px;
+      left: 0;
+      width: 96%;
+      background: #1d6ff6;
+      color: white;
+      border-radius: 5px;
+      left: 10px;
+    }
+  }
+
+  &.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;
+    transition: opacity 0.3s ease;
+  }
+}

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

@@ -0,0 +1,73 @@
+// components/button/button.ts
+Component({
+  behaviors: ["wx://form-field-button"],
+  observers: {
+    loading(this: any, val: boolean) {
+      // 当外部声明 loading 时,同步禁用状态
+      this.setData({ isDisabled: !!val });
+    },
+  },
+  lifetimes: {
+    attached(this: any) {
+      console.log("[Button] attached, initializing...");
+      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}`,
+        isDisabled: false,
+      });
+      console.log(
+        "[Button] initialized with isDisabled:",
+        this.data.isDisabled
+      );
+    },
+  },
+  properties: {
+    block: { type: Boolean, value: false },
+    index: { type: Number, value: 1 },
+    loading: { type: Boolean, value: false },
+    text: { type: String, value: "" },
+    // 是否在点击后禁用按钮,避免重复提交
+    disableOnClick: { type: Boolean, value: true },
+  },
+  data: {
+    list: [
+      {
+        name: "保存",
+        value: "save",
+      },
+      {
+        name: "提交",
+        value: "submit",
+      },
+      {
+        name: "确定",
+        value: "confirm",
+      },
+      {
+        name: "取消",
+        value: "cancel",
+      },
+    ],
+    src: "",
+    isDisabled: false,
+  },
+  methods: {
+    onSubmit(this: any, event: any) {
+      if (this.data.isDisabled) return;
+      if (this.data.disableOnClick) this.valFailure();
+      this.triggerEvent('submit', { target: event.target }, { bubbles: true, composed: true });
+    },
+    resetState(this: any) {
+      this.setData({
+        isDisabled: false,
+      });
+    },
+
+    valFailure(this: any) {
+      // 处理失败逻辑
+      if (this) this.setData({ isDisabled: true }); // 重置按钮状态
+    },
+  },
+});

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

@@ -0,0 +1,16 @@
+<view class="button {{className}} {{isDisabled ? 'disabled' : ''}}">
+  <view class="button__text">
+    <text max-lines="{{index}}" overflow="fade">
+      <slot></slot>
+    </text>
+  </view>
+  <button 
+    class="button__inner" 
+    form-type="submit" 
+    bind:tap="onSubmit" 
+    disabled="{{isDisabled}}"
+    hover-class="{{isDisabled ? '' : 'hover'}}"
+  >
+    {{list[index].name}}
+  </button>
+</view>

+ 33 - 0
miniprogram/components/calendar/func/config.js

@@ -0,0 +1,33 @@
+import WxData from './wxData'
+
+class Config extends WxData {
+  constructor(component) {
+    super(component)
+    this.Component = component
+  }
+  getCalendarConfig() {
+    if (!this.Component || !this.Component.config) return {}
+    return this.Component.config
+  }
+  setCalendarConfig(config) {
+    return new Promise((resolve, reject) => {
+      if (!this.Component || !this.Component.config) {
+        reject('异常:未找到组件配置信息')
+        return
+      }
+      let conf = { ...this.Component.config, ...config }
+
+      this.Component.config = conf
+      this.setData(
+        {
+          calendarConfig: conf
+        },
+        () => {
+          resolve(conf)
+        }
+      )
+    })
+  }
+}
+
+export default component => new Config(component)

+ 998 - 0
miniprogram/components/calendar/func/convertSolarLunar.js

@@ -0,0 +1,998 @@
+/**
+ * @1900-2100区间内的公历、农历互转
+ * @Version 1.0.3
+ * @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0]
+ * @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0]
+ */
+/* 公历年月日转农历数据 返回json */
+// calendar.solar2lunar(1987,11,01);
+/** 农历年月日转公历年月日 **/
+// calendar.lunar2solar(1987,9,10);
+// 调用以上方法后返回类似如下object(json)具体以上就不需要解释了吧!
+// c开头的是公历各属性值 l开头的自然就是农历咯 gz开头的就是天干地支纪年的数据啦~
+// {
+//  Animal: "兔",
+//  IDayCn: "初十",
+//  IMonthCn: "九月",
+//  Term: null,
+//  astro: "天蝎座",
+//  cDay: 1,
+//  cMonth: 11,
+//  cYear: 1987,
+//  gzDay: "甲寅",
+//  gzMonth: "庚戌",
+//  gzYear: "丁卯",
+//  isLeap: false,
+//  isTerm: false,
+//  isToday: false,
+//  lDay: 10,
+//  lMonth: 9,
+//  lYear: 1987,
+//  nWeek: 7,
+//  ncWeek: "星期日"
+// }
+// 该代码还有其他可以调用的方法,请自己查看代码中的详细注释
+const calendar = {
+  /**
+   * 农历1900-2100的润大小信息表
+   * @Array Of Property
+   * @return Hex
+   */
+  lunarInfo: [
+    0x04bd8,
+    0x04ae0,
+    0x0a570,
+    0x054d5,
+    0x0d260,
+    0x0d950,
+    0x16554,
+    0x056a0,
+    0x09ad0,
+    0x055d2, // 1900-1909
+    0x04ae0,
+    0x0a5b6,
+    0x0a4d0,
+    0x0d250,
+    0x1d255,
+    0x0b540,
+    0x0d6a0,
+    0x0ada2,
+    0x095b0,
+    0x14977, // 1910-1919
+    0x04970,
+    0x0a4b0,
+    0x0b4b5,
+    0x06a50,
+    0x06d40,
+    0x1ab54,
+    0x02b60,
+    0x09570,
+    0x052f2,
+    0x04970, // 1920-1929
+    0x06566,
+    0x0d4a0,
+    0x0ea50,
+    0x06e95,
+    0x05ad0,
+    0x02b60,
+    0x186e3,
+    0x092e0,
+    0x1c8d7,
+    0x0c950, // 1930-1939
+    0x0d4a0,
+    0x1d8a6,
+    0x0b550,
+    0x056a0,
+    0x1a5b4,
+    0x025d0,
+    0x092d0,
+    0x0d2b2,
+    0x0a950,
+    0x0b557, // 1940-1949
+    0x06ca0,
+    0x0b550,
+    0x15355,
+    0x04da0,
+    0x0a5b0,
+    0x14573,
+    0x052b0,
+    0x0a9a8,
+    0x0e950,
+    0x06aa0, // 1950-1959
+    0x0aea6,
+    0x0ab50,
+    0x04b60,
+    0x0aae4,
+    0x0a570,
+    0x05260,
+    0x0f263,
+    0x0d950,
+    0x05b57,
+    0x056a0, // 1960-1969
+    0x096d0,
+    0x04dd5,
+    0x04ad0,
+    0x0a4d0,
+    0x0d4d4,
+    0x0d250,
+    0x0d558,
+    0x0b540,
+    0x0b6a0,
+    0x195a6, // 1970-1979
+    0x095b0,
+    0x049b0,
+    0x0a974,
+    0x0a4b0,
+    0x0b27a,
+    0x06a50,
+    0x06d40,
+    0x0af46,
+    0x0ab60,
+    0x09570, // 1980-1989
+    0x04af5,
+    0x04970,
+    0x064b0,
+    0x074a3,
+    0x0ea50,
+    0x06b58,
+    0x055c0,
+    0x0ab60,
+    0x096d5,
+    0x092e0, // 1990-1999
+    0x0c960,
+    0x0d954,
+    0x0d4a0,
+    0x0da50,
+    0x07552,
+    0x056a0,
+    0x0abb7,
+    0x025d0,
+    0x092d0,
+    0x0cab5, // 2000-2009
+    0x0a950,
+    0x0b4a0,
+    0x0baa4,
+    0x0ad50,
+    0x055d9,
+    0x04ba0,
+    0x0a5b0,
+    0x15176,
+    0x052b0,
+    0x0a930, // 2010-2019
+    0x07954,
+    0x06aa0,
+    0x0ad50,
+    0x05b52,
+    0x04b60,
+    0x0a6e6,
+    0x0a4e0,
+    0x0d260,
+    0x0ea65,
+    0x0d530, // 2020-2029
+    0x05aa0,
+    0x076a3,
+    0x096d0,
+    0x04afb,
+    0x04ad0,
+    0x0a4d0,
+    0x1d0b6,
+    0x0d250,
+    0x0d520,
+    0x0dd45, // 2030-2039
+    0x0b5a0,
+    0x056d0,
+    0x055b2,
+    0x049b0,
+    0x0a577,
+    0x0a4b0,
+    0x0aa50,
+    0x1b255,
+    0x06d20,
+    0x0ada0, // 2040-2049
+    /** Add By JJonline@JJonline.Cn **/
+    0x14b63,
+    0x09370,
+    0x049f8,
+    0x04970,
+    0x064b0,
+    0x168a6,
+    0x0ea50,
+    0x06b20,
+    0x1a6c4,
+    0x0aae0, // 2050-2059
+    0x0a2e0,
+    0x0d2e3,
+    0x0c960,
+    0x0d557,
+    0x0d4a0,
+    0x0da50,
+    0x05d55,
+    0x056a0,
+    0x0a6d0,
+    0x055d4, // 2060-2069
+    0x052d0,
+    0x0a9b8,
+    0x0a950,
+    0x0b4a0,
+    0x0b6a6,
+    0x0ad50,
+    0x055a0,
+    0x0aba4,
+    0x0a5b0,
+    0x052b0, // 2070-2079
+    0x0b273,
+    0x06930,
+    0x07337,
+    0x06aa0,
+    0x0ad50,
+    0x14b55,
+    0x04b60,
+    0x0a570,
+    0x054e4,
+    0x0d160, // 2080-2089
+    0x0e968,
+    0x0d520,
+    0x0daa0,
+    0x16aa6,
+    0x056d0,
+    0x04ae0,
+    0x0a9d4,
+    0x0a2d0,
+    0x0d150,
+    0x0f252, // 2090-2099
+    0x0d520
+  ], // 2100
+
+  /**
+   * 公历每个月份的天数普通表
+   * @Array Of Property
+   * @return Number
+   */
+  solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+
+  /**
+   * 天干地支之天干速查表
+   * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
+   * @return Cn string
+   */
+  Gan: [
+    '\u7532',
+    '\u4e59',
+    '\u4e19',
+    '\u4e01',
+    '\u620a',
+    '\u5df1',
+    '\u5e9a',
+    '\u8f9b',
+    '\u58ec',
+    '\u7678'
+  ],
+
+  /**
+   * 天干地支之地支速查表
+   * @Array Of Property
+   * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
+   * @return Cn string
+   */
+  Zhi: [
+    '\u5b50',
+    '\u4e11',
+    '\u5bc5',
+    '\u536f',
+    '\u8fb0',
+    '\u5df3',
+    '\u5348',
+    '\u672a',
+    '\u7533',
+    '\u9149',
+    '\u620c',
+    '\u4ea5'
+  ],
+
+  /**
+   * 天干地支之地支速查表<=>生肖
+   * @Array Of Property
+   * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"]
+   * @return Cn string
+   */
+  Animals: [
+    '\u9f20',
+    '\u725b',
+    '\u864e',
+    '\u5154',
+    '\u9f99',
+    '\u86c7',
+    '\u9a6c',
+    '\u7f8a',
+    '\u7334',
+    '\u9e21',
+    '\u72d7',
+    '\u732a'
+  ],
+
+  /**
+   * 24节气速查表
+   * @Array Of Property
+   * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"]
+   * @return Cn string
+   */
+  solarTerm: [
+    '\u5c0f\u5bd2',
+    '\u5927\u5bd2',
+    '\u7acb\u6625',
+    '\u96e8\u6c34',
+    '\u60ca\u86f0',
+    '\u6625\u5206',
+    '\u6e05\u660e',
+    '\u8c37\u96e8',
+    '\u7acb\u590f',
+    '\u5c0f\u6ee1',
+    '\u8292\u79cd',
+    '\u590f\u81f3',
+    '\u5c0f\u6691',
+    '\u5927\u6691',
+    '\u7acb\u79cb',
+    '\u5904\u6691',
+    '\u767d\u9732',
+    '\u79cb\u5206',
+    '\u5bd2\u9732',
+    '\u971c\u964d',
+    '\u7acb\u51ac',
+    '\u5c0f\u96ea',
+    '\u5927\u96ea',
+    '\u51ac\u81f3'
+  ],
+
+  /**
+   * 1900-2100各年的24节气日期速查表
+   * @Array Of Property
+   * @return 0x string For splice
+   */
+  sTermInfo: [
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c3598082c95f8c965cc920f',
+    '97bd0b06bdb0722c965ce1cfcc920f',
+    'b027097bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f',
+    '97bd0b06bdb0722c965ce1cfcc920f',
+    'b027097bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f',
+    '97bd0b06bdb0722c965ce1cfcc920f',
+    'b027097bd097c36b0b6fc9274c91aa',
+    '9778397bd19801ec9210c965cc920e',
+    '97b6b97bd19801ec95f8c965cc920f',
+    '97bd09801d98082c95f8e1cfcc920f',
+    '97bd097bd097c36b0b6fc9210c8dc2',
+    '9778397bd197c36c9210c9274c91aa',
+    '97b6b97bd19801ec95f8c965cc920e',
+    '97bd09801d98082c95f8e1cfcc920f',
+    '97bd097bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c91aa',
+    '97b6b97bd19801ec95f8c965cc920e',
+    '97bcf97c3598082c95f8e1cfcc920f',
+    '97bd097bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c3598082c95f8c965cc920f',
+    '97bd097bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c3598082c95f8c965cc920f',
+    '97bd097bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f',
+    '97bd097bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f',
+    '97bd097bd07f595b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9210c8dc2',
+    '9778397bd19801ec9210c9274c920e',
+    '97b6b97bd19801ec95f8c965cc920f',
+    '97bd07f5307f595b0b0bc920fb0722',
+    '7f0e397bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c920e',
+    '97b6b97bd19801ec95f8c965cc920f',
+    '97bd07f5307f595b0b0bc920fb0722',
+    '7f0e397bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bd07f1487f595b0b0bc920fb0722',
+    '7f0e397bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f595b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f595b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b7f0e47f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '9778397bd097c36b0b6fc9210c91aa',
+    '97b6b7f0e47f149b0723b0787b0721',
+    '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '9778397bd097c36b0b6fc9210c8dc2',
+    '977837f0e37f149b0723b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722',
+    '7f0e37f5307f595b0b0bc920fb0722',
+    '7f0e397bd097c35b0b6fc9210c8dc2',
+    '977837f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e37f1487f595b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc9210c8dc2',
+    '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722',
+    '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722',
+    '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f149b0723b0787b0721',
+    '7f0e27f0e47f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '977837f0e37f14998082b0723b06bd',
+    '7f07e7f0e37f149b0723b0787b0721',
+    '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722',
+    '977837f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722',
+    '7f0e37f1487f595b0b0bb0b6fb0722',
+    '7f0e37f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722',
+    '7f0e37f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e37f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e37f14898082b072297c35',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e37f14898082b072297c35',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f149b0723b0787b0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35',
+    '7ec967f0e37f14998082b0723b06bd',
+    '7f07e7f0e47f149b0723b0787b0721',
+    '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35',
+    '7ec967f0e37f14998082b0723b06bd',
+    '7f07e7f0e37f14998083b0787b0721',
+    '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35',
+    '7ec967f0e37f14898082b0723b02d5',
+    '7f07e7f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722',
+    '7f0e36665b66aa89801e9808297c35',
+    '665f67f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722',
+    '7f0e36665b66a449801e9808297c35',
+    '665f67f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e36665b66a449801e9808297c35',
+    '665f67f0e37f14898082b072297c35',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e26665b66a449801e9808297c35',
+    '665f67f0e37f1489801eb072297c35',
+    '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722'
+  ],
+
+  /**
+   * 数字转中文速查表
+   * @Array Of Property
+   * @trans ['日','一','二','三','四','五','六','七','八','九','十']
+   * @return Cn string
+   */
+  nStr1: [
+    '\u65e5',
+    '\u4e00',
+    '\u4e8c',
+    '\u4e09',
+    '\u56db',
+    '\u4e94',
+    '\u516d',
+    '\u4e03',
+    '\u516b',
+    '\u4e5d',
+    '\u5341'
+  ],
+
+  /**
+   * 日期转农历称呼速查表
+   * @Array Of Property
+   * @trans ['初','十','廿','卅']
+   * @return Cn string
+   */
+  nStr2: ['\u521d', '\u5341', '\u5eff', '\u5345'],
+
+  /**
+   * 月份转农历称呼速查表
+   * @Array Of Property
+   * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊']
+   * @return Cn string
+   */
+  nStr3: [
+    '\u6b63',
+    '\u4e8c',
+    '\u4e09',
+    '\u56db',
+    '\u4e94',
+    '\u516d',
+    '\u4e03',
+    '\u516b',
+    '\u4e5d',
+    '\u5341',
+    '\u51ac',
+    '\u814a'
+  ],
+
+  /**
+   * 返回农历y年一整年的总天数
+   * @param lunar Year
+   * @return Number
+   * @eg:var count = calendar.lYearDays(1987) ;//count=387
+   */
+  lYearDays: function(y) {
+    let i
+    let sum = 348
+    for (i = 0x8000; i > 0x8; i >>= 1) {
+      sum += calendar.lunarInfo[y - 1900] & i ? 1 : 0
+    }
+    return sum + calendar.leapDays(y)
+  },
+
+  /**
+   * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0
+   * @param lunar Year
+   * @return Number (0-12)
+   * @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6
+   */
+  leapMonth: function(y) {
+    // 闰字编码 \u95f0
+    return calendar.lunarInfo[y - 1900] & 0xf
+  },
+
+  /**
+   * 返回农历y年闰月的天数 若该年没有闰月则返回0
+   * @param lunar Year
+   * @return Number (0、29、30)
+   * @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29
+   */
+  leapDays: function(y) {
+    if (calendar.leapMonth(y)) {
+      return calendar.lunarInfo[y - 1900] & 0x10000 ? 30 : 29
+    }
+    return 0
+  },
+
+  /**
+   * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法
+   * @param lunar Year
+   * @return Number (-1、29、30)
+   * @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29
+   */
+  monthDays: function(y, m) {
+    if (m > 12 || m < 1) return -1 // 月份参数从1至12,参数错误返回-1
+    return calendar.lunarInfo[y - 1900] & (0x10000 >> m) ? 30 : 29
+  },
+
+  /**
+   * 返回公历(!)y年m月的天数
+   * @param solar Year
+   * @return Number (-1、28、29、30、31)
+   * @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30
+   */
+  solarDays: function(y, m) {
+    if (m > 12 || m < 1) return -1 // 若参数错误 返回-1
+    const ms = m - 1
+    if (+ms === 1) {
+      // 2月份的闰平规律测算后确认返回28或29
+      return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0 ? 29 : 28
+    } else {
+      return calendar.solarMonth[ms]
+    }
+  },
+
+  /**
+   * 农历年份转换为干支纪年
+   * @param  lYear 农历年的年份数
+   * @return Cn string
+   */
+  toGanZhiYear: function(lYear) {
+    let ganKey = (lYear - 3) % 10
+    let zhiKey = (lYear - 3) % 12
+    if (+ganKey === 0) ganKey = 10 // 如果余数为0则为最后一个天干
+    if (+zhiKey === 0) zhiKey = 12 // 如果余数为0则为最后一个地支
+    return calendar.Gan[ganKey - 1] + calendar.Zhi[zhiKey - 1]
+  },
+
+  /**
+   * 公历月、日判断所属星座
+   * @param  cMonth [description]
+   * @param  cDay [description]
+   * @return Cn string
+   */
+  toAstro: function(cMonth, cDay) {
+    const s =
+      '\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf'
+    const arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]
+    return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + '\u5ea7' // 座
+  },
+
+  /**
+   * 传入offset偏移量返回干支
+   * @param offset 相对甲子的偏移量
+   * @return Cn string
+   */
+  toGanZhi: function(offset) {
+    return calendar.Gan[offset % 10] + calendar.Zhi[offset % 12]
+  },
+
+  /**
+   * 传入公历(!)y年获得该年第n个节气的公历日期
+   * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起
+   * @return day Number
+   * @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春
+   */
+  getTerm: function(y, n) {
+    if (y < 1900 || y > 2100) return -1
+    if (n < 1 || n > 24) return -1
+    const _table = calendar.sTermInfo[y - 1900]
+    const _info = [
+      parseInt('0x' + _table.substr(0, 5)).toString(),
+      parseInt('0x' + _table.substr(5, 5)).toString(),
+      parseInt('0x' + _table.substr(10, 5)).toString(),
+      parseInt('0x' + _table.substr(15, 5)).toString(),
+      parseInt('0x' + _table.substr(20, 5)).toString(),
+      parseInt('0x' + _table.substr(25, 5)).toString()
+    ]
+    const _calday = [
+      _info[0].substr(0, 1),
+      _info[0].substr(1, 2),
+      _info[0].substr(3, 1),
+      _info[0].substr(4, 2),
+
+      _info[1].substr(0, 1),
+      _info[1].substr(1, 2),
+      _info[1].substr(3, 1),
+      _info[1].substr(4, 2),
+
+      _info[2].substr(0, 1),
+      _info[2].substr(1, 2),
+      _info[2].substr(3, 1),
+      _info[2].substr(4, 2),
+
+      _info[3].substr(0, 1),
+      _info[3].substr(1, 2),
+      _info[3].substr(3, 1),
+      _info[3].substr(4, 2),
+
+      _info[4].substr(0, 1),
+      _info[4].substr(1, 2),
+      _info[4].substr(3, 1),
+      _info[4].substr(4, 2),
+
+      _info[5].substr(0, 1),
+      _info[5].substr(1, 2),
+      _info[5].substr(3, 1),
+      _info[5].substr(4, 2)
+    ]
+    return parseInt(_calday[n - 1])
+  },
+
+  /**
+   * 传入农历数字月份返回汉语通俗表示法
+   * @param lunar month
+   * @return Cn string
+   * @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月'
+   */
+  toChinaMonth: function(m) {
+    // 月 => \u6708
+    if (m > 12 || m < 1) return -1 // 若参数错误 返回-1
+    let s = calendar.nStr3[m - 1]
+    s += '\u6708' // 加上月字
+    return s
+  },
+
+  /**
+   * 传入农历日期数字返回汉字表示法
+   * @param lunar day
+   * @return Cn string
+   * @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一'
+   */
+  toChinaDay: function(d) {
+    // 日 => \u65e5
+    let s
+    switch (d) {
+      case 10:
+        s = '\u521d\u5341'
+        break
+      case 20:
+        s = '\u4e8c\u5341'
+        break
+      case 30:
+        s = '\u4e09\u5341'
+        break
+      default:
+        s = calendar.nStr2[Math.floor(d / 10)]
+        s += calendar.nStr1[d % 10]
+    }
+    return s
+  },
+
+  /**
+   * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春”
+   * @param y year
+   * @return Cn string
+   * @eg:var animal = calendar.getAnimal(1987) ;//animal='兔'
+   */
+  getAnimal: function(y) {
+    return calendar.Animals[(y - 4) % 12]
+  },
+
+  /**
+   * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON
+   * @param y  solar year
+   * @param m  solar month
+   * @param d  solar day
+   * @return JSON object
+   * @eg:console.log(calendar.solar2lunar(1987,11,01));
+   */
+  solar2lunar: function(y, m, d) {
+    // 参数区间1900.1.31~2100.12.31
+    // 年份限定、上限
+    if (y < 1900 || y > 2100) {
+      return -1 // undefined转换为数字变为NaN
+    }
+    // 公历传参最下限
+    if (+y === 1900 && +m === 1 && +d < 31) {
+      return -1
+    }
+    // 未传参 获得当天
+    let objDate
+    if (!y) {
+      objDate = new Date()
+    } else {
+      objDate = new Date(y, parseInt(m) - 1, d)
+    }
+    let i
+    let leap = 0
+    let temp = 0
+    // 修正ymd参数
+    y = objDate.getFullYear()
+    m = objDate.getMonth() + 1
+    d = objDate.getDate()
+    let offset =
+      (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) -
+        Date.UTC(1900, 0, 31)) /
+      86400000
+    for (i = 1900; i < 2101 && offset > 0; i++) {
+      temp = calendar.lYearDays(i)
+      offset -= temp
+    }
+    if (offset < 0) {
+      offset += temp
+      i--
+    }
+
+    // 是否今天
+    const isTodayObj = new Date();
+    let isToday = false
+    if (
+      isTodayObj.getFullYear() === +y &&
+      isTodayObj.getMonth() + 1 === +m &&
+      isTodayObj.getDate() === +d
+    ) {
+      isToday = true
+    }
+    // 星期几
+    let nWeek = objDate.getDay()
+    const cWeek = calendar.nStr1[nWeek]
+    // 数字表示周几顺应天朝周一开始的惯例
+    if (+nWeek === 0) {
+      nWeek = 7
+    }
+    // 农历年
+    const year = i
+    leap = calendar.leapMonth(i) // 闰哪个月
+    let isLeap = false
+
+    // 效验闰月
+    for (i = 1; i < 13 && offset > 0; i++) {
+      // 闰月
+      if (leap > 0 && i === leap + 1 && isLeap === false) {
+        --i
+        isLeap = true
+        temp = calendar.leapDays(year) // 计算农历闰月天数
+      } else {
+        temp = calendar.monthDays(year, i) // 计算农历普通月天数
+      }
+      // 解除闰月
+      if (isLeap === true && i === leap + 1) isLeap = false
+      offset -= temp
+    }
+    // 闰月导致数组下标重叠取反
+    if (offset === 0 && leap > 0 && i === leap + 1) {
+      if (isLeap) {
+        isLeap = false
+      } else {
+        isLeap = true
+        --i
+      }
+    }
+    if (offset < 0) {
+      offset += temp
+      --i
+    }
+    // 农历月
+    const month = i
+    // 农历日
+    const day = offset + 1
+    // 天干地支处理
+    const sm = m - 1
+    const gzY = calendar.toGanZhiYear(year)
+
+    // 当月的两个节气
+    // bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year`
+    const firstNode = calendar.getTerm(y, m * 2 - 1) // 返回当月「节」为几日开始
+    const secondNode = calendar.getTerm(y, m * 2) // 返回当月「节」为几日开始
+
+    // 依据12节气修正干支月
+    let gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11)
+    if (d >= firstNode) {
+      gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12)
+    }
+
+    // 传入的日期的节气与否
+    let isTerm = false
+    let Term = null
+    if (+firstNode === d) {
+      isTerm = true
+      Term = calendar.solarTerm[m * 2 - 2]
+    }
+    if (+secondNode === d) {
+      isTerm = true
+      Term = calendar.solarTerm[m * 2 - 1]
+    }
+    // 日柱 当月一日与 1900/1/1 相差天数
+    const dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10
+    const gzD = calendar.toGanZhi(dayCyclical + d - 1)
+    // 该日期所属的星座
+    const astro = calendar.toAstro(m, d)
+
+    return {
+      lYear: year,
+      lMonth: month,
+      lDay: day,
+      Animal: calendar.getAnimal(year),
+      IMonthCn: (isLeap ? '\u95f0' : '') + calendar.toChinaMonth(month),
+      IDayCn: calendar.toChinaDay(day),
+      cYear: y,
+      cMonth: m,
+      cDay: d,
+      gzYear: gzY,
+      gzMonth: gzM,
+      gzDay: gzD,
+      isToday: isToday,
+      isLeap: isLeap,
+      nWeek: nWeek,
+      ncWeek: '\u661f\u671f' + cWeek,
+      isTerm: isTerm,
+      Term: Term,
+      astro: astro
+    }
+  },
+
+  /**
+   * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON
+   * @param y  lunar year
+   * @param m  lunar month
+   * @param d  lunar day
+   * @param isLeapMonth  lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
+   * @return JSON object
+   * @eg:console.log(calendar.lunar2solar(1987,9,10));
+   */
+  lunar2solar: function(y, m, d, isLeapMonth) {
+    // 参数区间1900.1.31~2100.12.1
+    isLeapMonth = !!isLeapMonth
+    // let leapOffset = 0;
+    const leapMonth = calendar.leapMonth(y)
+    // let leapDay = calendar.leapDays(y);
+    if (isLeapMonth && leapMonth !== m) return -1 // 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
+    if (
+      (+y === 2100 && +m === 12 && +d > 1) ||
+      (+y === 1900 && +m === 1 && +d < 31)
+    )
+      return -1 // 超出了最大极限值
+    const day = calendar.monthDays(y, m)
+    let _day = day
+    // bugFix 2016-9-25
+    // if month is leap, _day use leapDays method
+    if (isLeapMonth) {
+      _day = calendar.leapDays(y, m)
+    }
+    if (y < 1900 || y > 2100 || d > _day) return -1 // 参数合法性效验
+
+    // 计算农历的时间差
+    let offset = 0
+    for (let i = 1900; i < y; i++) {
+      offset += calendar.lYearDays(i)
+    }
+    let leap = 0
+    let isAdd = false
+    for (let i = 1; i < m; i++) {
+      leap = calendar.leapMonth(y)
+      if (!isAdd) {
+        // 处理闰月
+        if (leap <= i && leap > 0) {
+          offset += calendar.leapDays(y)
+          isAdd = true
+        }
+      }
+      offset += calendar.monthDays(y, i)
+    }
+    // 转换闰月农历 需补充该年闰月的前一个月的时差
+    if (isLeapMonth) offset += day
+    // 1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
+    const stmap = Date.UTC(1900, 1, 30, 0, 0, 0)
+    const calObj = new Date((offset + d - 31) * 86400000 + stmap)
+    const cY = calObj.getUTCFullYear()
+    const cM = calObj.getUTCMonth() + 1
+    const cD = calObj.getUTCDate()
+
+    return calendar.solar2lunar(cY, cM, cD)
+  }
+}
+
+const {
+  Gan,
+  Zhi,
+  nStr1,
+  nStr2,
+  nStr3,
+  Animals,
+  solarTerm,
+  lunarInfo,
+  sTermInfo,
+  solarMonth,
+  ...rest
+} = calendar
+
+export default rest

+ 543 - 0
miniprogram/components/calendar/func/day.js

@@ -0,0 +1,543 @@
+import WxData from './wxData'
+import CalendarConfig from './config'
+import convertSolarLunar from './convertSolarLunar'
+import {
+  Logger,
+  GetDate,
+  getDateTimeStamp,
+  uniqueArrayByDate,
+  delRepeatedEnableDay,
+  convertEnableAreaToTimestamp,
+  converEnableDaysToTimestamp
+} from './utils'
+
+const logger = new Logger()
+const getDate = new GetDate()
+const toString = Object.prototype.toString
+
+class Day extends WxData {
+  constructor(component) {
+    super(component)
+    this.Component = component
+  }
+  getCalendarConfig() {
+    return this.Component.config
+  }
+  /**
+   *
+   * @param {number} year
+   * @param {number} month
+   */
+  buildDate(year, month) {
+    const today = getDate.todayDate()
+    const thisMonthDays = getDate.thisMonthDays(year, month)
+    const dates = []
+    for (let i = 1; i <= thisMonthDays; i++) {
+      const isToday =
+        +today.year === +year && +today.month === +month && i === +today.date
+      const config = this.getCalendarConfig()
+      const date = {
+        year,
+        month,
+        day: i,
+        choosed: false,
+        week: getDate.dayOfWeek(year, month, i),
+        isToday: isToday && config.highlightToday,
+        lunar: convertSolarLunar.solar2lunar(+year, +month, +i)
+      }
+      dates.push(date)
+    }
+    return dates
+  }
+  /**
+   * 指定可选日期范围
+   * @param {array} area 日期访问数组
+   */
+  enableArea(dateArea = []) {
+    if (dateArea.length === 2) {
+      const isRight = this.__judgeParam(dateArea)
+      if (isRight) {
+        let { days = [], selectedDay = [] } = this.getData('calendar')
+        const { startTimestamp, endTimestamp } = convertEnableAreaToTimestamp(
+          dateArea
+        )
+        const dataAfterHandle = this.__handleEnableArea(
+          {
+            dateArea,
+            days,
+            startTimestamp,
+            endTimestamp
+          },
+          selectedDay
+        )
+        this.setData({
+          'calendar.enableArea': dateArea,
+          'calendar.days': dataAfterHandle.dates,
+          'calendar.selectedDay': dataAfterHandle.selectedDay,
+          'calendar.enableAreaTimestamp': [startTimestamp, endTimestamp]
+        })
+      }
+    } else {
+      logger.warn(
+        'enableArea()参数需为时间范围数组,形如:["2018-8-4" , "2018-8-24"]'
+      )
+    }
+  }
+  /**
+   * 指定特定日期可选
+   * @param {array} days 指定日期数组
+   */
+  enableDays(dates = []) {
+    const { enableArea = [] } = this.getData('calendar')
+    let expectEnableDaysTimestamp = []
+    if (enableArea.length) {
+      expectEnableDaysTimestamp = delRepeatedEnableDay(dates, enableArea)
+    } else {
+      expectEnableDaysTimestamp = converEnableDaysToTimestamp(dates)
+    }
+    let { days = [], selectedDay = [] } = this.getData('calendar')
+    const dataAfterHandle = this.__handleEnableDays(
+      {
+        days,
+        expectEnableDaysTimestamp
+      },
+      selectedDay
+    )
+    this.setData({
+      'calendar.days': dataAfterHandle.dates,
+      'calendar.selectedDay': dataAfterHandle.selectedDay,
+      'calendar.enableDays': dates,
+      'calendar.enableDaysTimestamp': expectEnableDaysTimestamp
+    })
+  }
+  /**
+   * 设置多个日期选中
+   * @param {array} selected 需选中日期
+   */
+  setSelectedDays(selected) {
+    const config = CalendarConfig(this.Component).getCalendarConfig()
+    if (!config.multi) {
+      return logger.warn('单选模式下不能设置多日期选中,请配置 multi')
+    }
+    let { days } = this.getData('calendar')
+    let newSelectedDay = []
+    if (!selected) {
+      days.map(item => {
+        item.choosed = true
+        item.showTodoLabel = false
+      })
+      newSelectedDay = days
+    } else if (selected && selected.length) {
+      const { dates, selectedDates } = this.__handleSelectedDays(
+        days,
+        newSelectedDay,
+        selected
+      )
+      days = dates
+      newSelectedDay = selectedDates
+    }
+    CalendarConfig(this.Component).setCalendarConfig('multi', true)
+    this.setData({
+      'calendar.days': days,
+      'calendar.selectedDay': newSelectedDay
+    })
+  }
+  /**
+   * 禁用指定日期
+   * @param {array} dates  禁用
+   */
+  disableDays(dates) {
+    const { disableDays = [], days } = this.getData('calendar')
+    if (Object.prototype.toString.call(dates) !== '[object Array]') {
+      return logger.warn('disableDays 参数为数组')
+    }
+    let _disableDays = []
+    if (dates.length) {
+      _disableDays = uniqueArrayByDate(dates.concat(disableDays))
+      const disableDaysCol = _disableDays.map(d => getDate.toTimeStr(d))
+      days.forEach(item => {
+        const cur = getDate.toTimeStr(item)
+        if (disableDaysCol.includes(cur)) item.disable = true
+      })
+    } else {
+      days.forEach(item => {
+        item.disable = false
+      })
+    }
+    this.setData({
+      'calendar.days': days,
+      'calendar.disableDays': _disableDays
+    })
+  }
+  /**
+   * 设置连续日期选择区域
+   * @param {array} dateArea 区域开始结束日期数组
+   */
+  chooseArea(dateArea = []) {
+    return new Promise((resolve, reject) => {
+      if (dateArea.length === 1) {
+        dateArea = dateArea.concat(dateArea)
+      }
+      if (dateArea.length === 2) {
+        const isRight = this.__judgeParam(dateArea)
+        if (isRight) {
+          const config = CalendarConfig(this.Component).getCalendarConfig()
+          const { startTimestamp, endTimestamp } = convertEnableAreaToTimestamp(
+            dateArea
+          )
+          this.setData(
+            {
+              calendarConfig: {
+                ...config,
+                chooseAreaMode: true,
+                mulit: true
+              },
+              'calendar.chooseAreaTimestamp': [startTimestamp, endTimestamp]
+            },
+            () => {
+              this.__chooseContinuousDates(startTimestamp, endTimestamp)
+                .then(resolve)
+                .catch(reject)
+            }
+          )
+        }
+      }
+    })
+  }
+  __pusheNextMonthDateArea(item, startTimestamp, endTimestamp, selectedDates) {
+    const days = this.buildDate(item.year, item.month)
+    let daysLen = days.length
+    for (let i = 0; i < daysLen; i++) {
+      const item = days[i]
+      const timeStamp = getDateTimeStamp(item)
+      if (timeStamp <= endTimestamp && timeStamp >= startTimestamp) {
+        selectedDates.push({
+          ...item,
+          choosed: true
+        })
+      }
+      if (i === daysLen - 1 && timeStamp < endTimestamp) {
+        this.__pusheNextMonthDateArea(
+          getDate.nextMonth(item),
+          startTimestamp,
+          endTimestamp,
+          selectedDates
+        )
+      }
+    }
+  }
+  __pushPrevMonthDateArea(item, startTimestamp, endTimestamp, selectedDates) {
+    const days = getDate.sortDates(
+      this.buildDate(item.year, item.month),
+      'desc'
+    )
+    let daysLen = days.length
+    let firstDate = getDateTimeStamp(days[0])
+    for (let i = 0; i < daysLen; i++) {
+      const item = days[i]
+      const timeStamp = getDateTimeStamp(item)
+      if (timeStamp >= startTimestamp && timeStamp <= endTimestamp) {
+        selectedDates.push({
+          ...item,
+          choosed: true
+        })
+      }
+      if (i === daysLen - 1 && firstDate > startTimestamp) {
+        this.__pushPrevMonthDateArea(
+          getDate.prevMonth(item),
+          startTimestamp,
+          endTimestamp,
+          selectedDates
+        )
+      }
+    }
+  }
+  /**
+   * 当设置日期区域非当前时保存其他月份的日期至已选日期数组
+   * @param {object} info
+   */
+  __calcDateWhenNotInOneMonth(info) {
+    const {
+      firstDate,
+      lastDate,
+      startTimestamp,
+      endTimestamp,
+      filterSelectedDate
+    } = info
+    if (getDateTimeStamp(firstDate) > startTimestamp) {
+      this.__pushPrevMonthDateArea(
+        getDate.prevMonth(firstDate),
+        startTimestamp,
+        endTimestamp,
+        filterSelectedDate
+      )
+    }
+    if (getDateTimeStamp(lastDate) < endTimestamp) {
+      this.__pusheNextMonthDateArea(
+        getDate.nextMonth(lastDate),
+        startTimestamp,
+        endTimestamp,
+        filterSelectedDate
+      )
+    }
+    const newSelectedDates = [...getDate.sortDates(filterSelectedDate)]
+    return newSelectedDates
+  }
+  /**
+   * 设置连续日期段
+   * @param {number} startTimestamp 连续日期段开始日期时间戳
+   * @param {number} endTimestamp 连续日期段结束日期时间戳
+   */
+  __chooseContinuousDates(startTimestamp, endTimestamp) {
+    return new Promise((resolve, reject) => {
+      const { days, selectedDay = [] } = this.getData('calendar')
+      const selectedDateStr = []
+      let filterSelectedDate = []
+      selectedDay.forEach(item => {
+        const timeStamp = getDateTimeStamp(item)
+        if (timeStamp >= startTimestamp && timeStamp <= endTimestamp) {
+          filterSelectedDate.push(item)
+          selectedDateStr.push(getDate.toTimeStr(item))
+        }
+      })
+      days.forEach(item => {
+        const timeStamp = getDateTimeStamp(item)
+        const dateInSelecedArray = selectedDateStr.includes(
+          getDate.toTimeStr(item)
+        )
+        if (timeStamp >= startTimestamp && timeStamp <= endTimestamp) {
+          if (dateInSelecedArray) {
+            return
+          }
+          item.choosed = true
+          filterSelectedDate.push(item)
+        } else {
+          item.choosed = false
+          if (dateInSelecedArray) {
+            const idx = filterSelectedDate.findIndex(
+              selectedDate =>
+                getDate.toTimeStr(selectedDate) === getDate.toTimeStr(item)
+            )
+            if (idx > -1) {
+              filterSelectedDate.splice(idx, 1)
+            }
+          }
+        }
+      })
+      const firstDate = days[0]
+      const lastDate = days[days.length - 1]
+      const newSelectedDates = this.__calcDateWhenNotInOneMonth({
+        firstDate,
+        lastDate,
+        startTimestamp,
+        endTimestamp,
+        filterSelectedDate
+      })
+      try {
+        this.setData(
+          {
+            'calendar.days': [...days],
+            'calendar.selectedDay': newSelectedDates
+          },
+          () => {
+            resolve(newSelectedDates)
+          }
+        )
+      } catch (err) {
+        reject(err)
+      }
+    })
+  }
+  /**
+   * 设置指定日期样式
+   * @param {array} dates 待设置特殊样式的日期
+   */
+  setDateStyle(dates) {
+    if (toString.call(dates) !== '[object Array]') return
+    const { days, specialStyleDates } = this.getData('calendar')
+    if (toString.call(specialStyleDates) === '[object Array]') {
+      dates = uniqueArrayByDate([...specialStyleDates, ...dates])
+    }
+    const _specialStyleDates = dates.map(
+      item => `${item.year}_${item.month}_${item.day}`
+    )
+    const _days = days.map(item => {
+      const idx = _specialStyleDates.indexOf(
+        `${item.year}_${item.month}_${item.day}`
+      )
+      if (idx > -1) {
+        return {
+          ...item,
+          class: dates[idx].class
+        }
+      } else {
+        return { ...item }
+      }
+    })
+    this.setData({
+      'calendar.days': _days,
+      'calendar.specialStyleDates': dates
+    })
+  }
+  __judgeParam(dateArea) {
+    const {
+      start,
+      end,
+      startTimestamp,
+      endTimestamp
+    } = convertEnableAreaToTimestamp(dateArea)
+    if (!start || !end) return
+    const startMonthDays = getDate.thisMonthDays(start[0], start[1])
+    const endMonthDays = getDate.thisMonthDays(end[0], end[1])
+    if (start[2] > startMonthDays || start[2] < 1) {
+      logger.warn('enableArea() 开始日期错误,指定日期不在当前月份天数范围内')
+      return false
+    } else if (start[1] > 12 || start[1] < 1) {
+      logger.warn('enableArea() 开始日期错误,月份超出1-12月份')
+      return false
+    } else if (end[2] > endMonthDays || end[2] < 1) {
+      logger.warn('enableArea() 截止日期错误,指定日期不在当前月份天数范围内')
+      return false
+    } else if (end[1] > 12 || end[1] < 1) {
+      logger.warn('enableArea() 截止日期错误,月份超出1-12月份')
+      return false
+    } else if (startTimestamp > endTimestamp) {
+      logger.warn('enableArea()参数最小日期大于了最大日期')
+      return false
+    } else {
+      return true
+    }
+  }
+  __getDisableDateTimestamp() {
+    let disableDateTimestamp
+    const { date, type } = this.getCalendarConfig().disableMode || {}
+    if (date) {
+      const t = date.split('-')
+      if (t.length < 3) {
+        logger.warn('配置 disableMode.date 格式错误')
+        return {}
+      }
+      disableDateTimestamp = getDateTimeStamp({
+        year: +t[0],
+        month: +t[1],
+        day: +t[2]
+      })
+    }
+    return {
+      disableDateTimestamp,
+      disableType: type
+    }
+  }
+  __handleEnableArea(data = {}, selectedDay = []) {
+    const { area, days, startTimestamp, endTimestamp } = data
+    const enableDays = this.getData('calendar.enableDays') || []
+    let expectEnableDaysTimestamp = []
+    if (enableDays.length) {
+      expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, area)
+    }
+    const {
+      disableDateTimestamp,
+      disableType
+    } = this.__getDisableDateTimestamp()
+    const dates = [...days]
+    dates.forEach(item => {
+      const timestamp = +getDate
+        .newDate(item.year, item.month, item.day)
+        .getTime()
+      const ifOutofArea =
+        (+startTimestamp > timestamp || timestamp > +endTimestamp) &&
+        !expectEnableDaysTimestamp.includes(timestamp)
+      if (
+        ifOutofArea ||
+        (disableType === 'before' &&
+          disableDateTimestamp &&
+          timestamp < disableDateTimestamp) ||
+        (disableType === 'after' &&
+          disableDateTimestamp &&
+          timestamp > disableDateTimestamp)
+      ) {
+        item.disable = true
+        if (item.choosed) {
+          item.choosed = false
+          selectedDay = selectedDay.filter(
+            d => getDate.toTimeStr(item) !== getDate.toTimeStr(d)
+          )
+        }
+      } else if (item.disable) {
+        item.disable = false
+      }
+    })
+    return {
+      dates,
+      selectedDay
+    }
+  }
+  __handleEnableDays(data = {}, selectedDay = []) {
+    const { days, expectEnableDaysTimestamp } = data
+    const { enableAreaTimestamp = [] } = this.getData('calendar')
+    const dates = [...days]
+    dates.forEach(item => {
+      const timestamp = getDate
+        .newDate(item.year, item.month, item.day)
+        .getTime()
+      let setDisable = false
+      if (enableAreaTimestamp.length) {
+        if (
+          (+enableAreaTimestamp[0] > +timestamp ||
+            +timestamp > +enableAreaTimestamp[1]) &&
+          !expectEnableDaysTimestamp.includes(+timestamp)
+        ) {
+          setDisable = true
+        }
+      } else if (!expectEnableDaysTimestamp.includes(+timestamp)) {
+        setDisable = true
+      }
+      if (setDisable) {
+        item.disable = true
+        if (item.choosed) {
+          item.choosed = false
+          selectedDay = selectedDay.filter(
+            d => getDate.toTimeStr(item) !== getDate.toTimeStr(d)
+          )
+        }
+      } else {
+        item.disable = false
+      }
+    })
+    return {
+      dates,
+      selectedDay
+    }
+  }
+  __handleSelectedDays(days = [], newSelectedDay = [], selected) {
+    const { selectedDay, showLabelAlways } = this.getData('calendar')
+    if (selectedDay && selectedDay.length) {
+      newSelectedDay = uniqueArrayByDate(selectedDay.concat(selected))
+    } else {
+      newSelectedDay = selected
+    }
+    const { year: curYear, month: curMonth } = days[0]
+    const currentSelectedDays = []
+    newSelectedDay.forEach(item => {
+      if (+item.year === +curYear && +item.month === +curMonth) {
+        currentSelectedDays.push(getDate.toTimeStr(item))
+      }
+    })
+    ;[...days].map(item => {
+      if (currentSelectedDays.includes(getDate.toTimeStr(item))) {
+        item.choosed = true
+        if (showLabelAlways && item.showTodoLabel) {
+          item.showTodoLabel = true
+        } else {
+          item.showTodoLabel = false
+        }
+      }
+    })
+    return {
+      dates: days,
+      selectedDates: newSelectedDay
+    }
+  }
+}
+
+export default component => new Day(component)

+ 375 - 0
miniprogram/components/calendar/func/render.js

@@ -0,0 +1,375 @@
+import Day from './day'
+import Todo from './todo'
+import WxData from './wxData'
+import convertSolarLunar from './convertSolarLunar'
+import {
+  Logger,
+  GetDate,
+  delRepeatedEnableDay,
+  getDateTimeStamp,
+  converEnableDaysToTimestamp
+} from './utils'
+
+const getDate = new GetDate()
+const logger = new Logger()
+
+class Calendar extends WxData {
+  constructor(component) {
+    super(component)
+    this.Component = component
+  }
+  getCalendarConfig() {
+    return this.Component.config
+  }
+  /**
+   * 渲染日历
+   * @param {number} curYear 年份
+   * @param {number} curMonth  月份
+   * @param {number} curDate  日期
+   * @param {boolean} disableSelect 是否禁用选中
+   */
+  renderCalendar(curYear, curMonth, curDate, disableSelect) {
+    return new Promise(resolve => {
+      const config = this.getCalendarConfig()
+      this.calculateEmptyGrids(curYear, curMonth)
+      this.calculateDays(curYear, curMonth, curDate, disableSelect).then(() => {
+        const { todoLabels, specialStyleDates, enableDays, selectedDay } =
+          this.getData('calendar') || {}
+        if (
+          todoLabels &&
+          todoLabels.find(
+            item => +item.month === +curMonth && +item.year === +curYear
+          )
+        ) {
+          Todo(this.Component).setTodoLabels()
+        }
+        if (
+          specialStyleDates &&
+          specialStyleDates.length &&
+          specialStyleDates.find(
+            item => +item.month === +curMonth && +item.year === +curYear
+          )
+        ) {
+          Day(this.Component).setDateStyle(specialStyleDates)
+        }
+
+        if (
+          enableDays &&
+          enableDays.length &&
+          enableDays.find(item => {
+            let ymd = item.split('-')
+            return +ymd[1] === +curMonth && +ymd[0] === +curYear
+          })
+        ) {
+          Day(this.Component).enableDays(enableDays)
+        }
+
+        if (
+          selectedDay &&
+          selectedDay.length &&
+          selectedDay.find(
+            item => +item.month === +curMonth && +item.year === +curYear
+          ) &&
+          config.mulit
+        ) {
+          Day(this.Component).setSelectedDays(selectedDay)
+        }
+
+        if (!this.Component.firstRender) {
+          resolve({
+            firstRender: true
+          })
+        } else {
+          resolve({
+            firstRender: false
+          })
+        }
+      })
+    })
+  }
+  /**
+   * 计算当前月份前后两月应占的格子
+   * @param {number} year 年份
+   * @param {number} month 月份
+   */
+  calculateEmptyGrids(year, month) {
+    this.calculatePrevMonthGrids(year, month)
+    this.calculateNextMonthGrids(year, month)
+  }
+  /**
+   * 计算上月应占的格子
+   * @param {number} year 年份
+   * @param {number} month 月份
+   */
+  calculatePrevMonthGrids(year, month) {
+    let empytGrids = []
+    const prevMonthDays = getDate.thisMonthDays(year, month - 1)
+    let firstDayOfWeek = getDate.firstDayOfWeek(year, month)
+    const config = this.getCalendarConfig() || {}
+    if (config.firstDayOfWeek === 'Mon') {
+      if (firstDayOfWeek === 0) {
+        firstDayOfWeek = 6
+      } else {
+        firstDayOfWeek -= 1
+      }
+    }
+    if (firstDayOfWeek > 0) {
+      const len = prevMonthDays - firstDayOfWeek
+      const { onlyShowCurrentMonth } = config
+      const { showLunar } = this.getCalendarConfig()
+      for (let i = prevMonthDays; i > len; i--) {
+        if (onlyShowCurrentMonth) {
+          empytGrids.push('')
+        } else {
+          empytGrids.push({
+            day: i,
+            lunar: showLunar
+              ? convertSolarLunar.solar2lunar(year, month - 1, i)
+              : null
+          })
+        }
+      }
+      this.setData({
+        'calendar.empytGrids': empytGrids.reverse()
+      })
+    } else {
+      this.setData({
+        'calendar.empytGrids': null
+      })
+    }
+  }
+  /**
+   * 计算下一月日期是否需要多展示的日期
+   * 某些月份日期为5排,某些月份6排,统一为6排
+   * @param {number} year
+   * @param {number} month
+   * @param {object} config
+   */
+  calculateExtraEmptyDate(year, month, config) {
+    let extDate = 0
+    if (+month === 2) {
+      extDate += 7
+      let firstDayofMonth = getDate.dayOfWeek(year, month, 1)
+      if (config.firstDayOfWeek === 'Mon') {
+        if (+firstDayofMonth === 1) extDate += 7
+      } else {
+        if (+firstDayofMonth === 0) extDate += 7
+      }
+    } else {
+      let firstDayofMonth = getDate.dayOfWeek(year, month, 1)
+      if (config.firstDayOfWeek === 'Mon') {
+        if (firstDayofMonth !== 0 && firstDayofMonth < 6) {
+          extDate += 7
+        }
+      } else {
+        if (firstDayofMonth <= 5) {
+          extDate += 7
+        }
+      }
+    }
+    return extDate
+  }
+  /**
+   * 计算下月应占的格子
+   * @param {number} year 年份
+   * @param {number} month  月份
+   */
+  calculateNextMonthGrids(year, month) {
+    let lastEmptyGrids = []
+    const thisMonthDays = getDate.thisMonthDays(year, month)
+    let lastDayWeek = getDate.dayOfWeek(year, month, thisMonthDays)
+    const config = this.getCalendarConfig() || {}
+    if (config.firstDayOfWeek === 'Mon') {
+      if (lastDayWeek === 0) {
+        lastDayWeek = 6
+      } else {
+        lastDayWeek -= 1
+      }
+    }
+    let len = 7 - (lastDayWeek + 1)
+    const { onlyShowCurrentMonth, showLunar } = config
+    if (!onlyShowCurrentMonth) {
+      len = len + this.calculateExtraEmptyDate(year, month, config)
+    }
+    for (let i = 1; i <= len; i++) {
+      if (onlyShowCurrentMonth) {
+        lastEmptyGrids.push('')
+      } else {
+        lastEmptyGrids.push({
+          day: i,
+          lunar: showLunar
+            ? convertSolarLunar.solar2lunar(year, month + 1, i)
+            : null
+        })
+      }
+    }
+    this.setData({
+      'calendar.lastEmptyGrids': lastEmptyGrids
+    })
+  }
+  /**
+   * 日历初始化将默认值写入 selectDay
+   * @param {number} year
+   * @param {number} month
+   * @param {number} curDate
+   */
+  setSelectedDay(year, month, curDate) {
+    let selectedDay = []
+    const config = this.getCalendarConfig()
+    if (config.noDefault) {
+      selectedDay = []
+      config.noDefault = false
+    } else {
+      const data = this.getData('calendar') || {}
+      const { showLunar } = this.getCalendarConfig()
+      selectedDay = curDate
+        ? [
+            {
+              year,
+              month,
+              day: curDate,
+              choosed: true,
+              week: getDate.dayOfWeek(year, month, curDate),
+              lunar: showLunar
+                ? convertSolarLunar.solar2lunar(year, month, curDate)
+                : null
+            }
+          ]
+        : data.selectedDay
+    }
+    return selectedDay
+  }
+  __getDisableDateTimestamp() {
+    let disableDateTimestamp
+    const { date, type } = this.getCalendarConfig().disableMode || {}
+    if (date) {
+      const t = date.split('-')
+      if (t.length < 3) {
+        logger.warn('配置 disableMode.date 格式错误')
+        return {}
+      }
+      disableDateTimestamp = getDateTimeStamp({
+        year: +t[0],
+        month: +t[1],
+        day: +t[2]
+      })
+    }
+    return {
+      disableDateTimestamp,
+      disableType: type
+    }
+  }
+  resetDates() {
+    this.setData({
+      'calendar.days': []
+    })
+  }
+  /**
+   * 设置日历面板数据
+   * @param {number} year 年份
+   * @param {number} month  月份
+   * @param {number} curDate  日期
+   * @param {boolean} disableSelect 是否禁用选中
+   */
+  calculateDays(year, month, curDate, disableSelect) {
+    return new Promise(resolve => {
+      // 避免切换日期时样式残影
+      this.resetDates()
+      let days = []
+      const {
+        disableDays = [],
+        chooseAreaTimestamp = [],
+        selectedDay: selectedDates = []
+      } = this.getData('calendar')
+      days = Day(this.Component).buildDate(year, month)
+      let selectedDay = selectedDates
+      if (!disableSelect) {
+        selectedDay = this.setSelectedDay(year, month, curDate)
+      }
+      const selectedDayStr = selectedDay.map(d => getDate.toTimeStr(d))
+      const disableDaysStr = disableDays.map(d => getDate.toTimeStr(d))
+      const [areaStart, areaEnd] = chooseAreaTimestamp
+      days.forEach(item => {
+        const cur = getDate.toTimeStr(item)
+        const timestamp = getDateTimeStamp(item)
+        if (selectedDayStr.includes(cur) && !disableSelect) {
+          item.choosed = true
+          if (timestamp > areaEnd || timestamp < areaStart) {
+            const idx = selectedDay.findIndex(
+              selectedDate =>
+                getDate.toTimeStr(selectedDate) === getDate.toTimeStr(item)
+            )
+            selectedDay.splice(idx, 1)
+          }
+        } else if (
+          areaStart &&
+          areaEnd &&
+          timestamp >= areaStart &&
+          timestamp <= areaEnd &&
+          !disableSelect
+        ) {
+          item.choosed = true
+          selectedDay.push(item)
+        }
+        if (disableDaysStr.includes(cur)) item.disable = true
+
+        const {
+          disableDateTimestamp,
+          disableType
+        } = this.__getDisableDateTimestamp()
+        let disabelByConfig = false
+        if (disableDateTimestamp) {
+          if (
+            (disableType === 'before' && timestamp < disableDateTimestamp) ||
+            (disableType === 'after' && timestamp > disableDateTimestamp)
+          ) {
+            disabelByConfig = true
+          }
+        }
+        const isDisable = disabelByConfig || this.__isDisable(timestamp)
+        if (isDisable) {
+          item.disable = true
+          item.choosed = false
+        }
+      })
+      this.setData(
+        {
+          'calendar.days': days,
+          'calendar.selectedDay': [...selectedDay] || []
+        },
+        () => {
+          resolve()
+        }
+      )
+    })
+  }
+  __isDisable(timestamp) {
+    const {
+      enableArea = [],
+      enableDays = [],
+      enableAreaTimestamp = []
+    } = this.getData('calendar')
+    let setDisable = false
+    let expectEnableDaysTimestamp = converEnableDaysToTimestamp(enableDays)
+    if (enableArea.length) {
+      expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, enableArea)
+    }
+    if (enableAreaTimestamp.length) {
+      if (
+        (+enableAreaTimestamp[0] > +timestamp ||
+          +timestamp > +enableAreaTimestamp[1]) &&
+        !expectEnableDaysTimestamp.includes(+timestamp)
+      ) {
+        setDisable = true
+      }
+    } else if (
+      expectEnableDaysTimestamp.length &&
+      !expectEnableDaysTimestamp.includes(+timestamp)
+    ) {
+      setDisable = true
+    }
+    return setDisable
+  }
+}
+
+export default component => new Calendar(component)

+ 182 - 0
miniprogram/components/calendar/func/todo.js

@@ -0,0 +1,182 @@
+import WxData from './wxData'
+import { Logger, uniqueArrayByDate, GetDate } from './utils'
+
+const logger = new Logger()
+const getDate = new GetDate()
+
+class Todo extends WxData {
+  constructor(component) {
+    super(component)
+    this.Component = component
+  }
+  /**
+   * 设置待办事项标志
+   * @param {object} options 待办事项配置
+   */
+  setTodoLabels(options) {
+    if (options) this.Component.todoConfig = options
+    const calendar = this.getData('calendar')
+    if (!calendar || !calendar.days) {
+      return logger.warn('请等待日历初始化完成后再调用该方法')
+    }
+    const dates = [...calendar.days]
+    const { curYear, curMonth } = calendar
+    const {
+      circle,
+      dotColor = '',
+      pos = 'bottom',
+      showLabelAlways,
+      days: todoDays = []
+    } = options || this.Component.todoConfig || {}
+    const { todoLabels = [] } = calendar
+    const currentMonthTodoLabels = this.getTodoLabels({
+      year: curYear,
+      month: curMonth
+    })
+    let newTodoLabels = todoDays.filter(
+      item => +item.year === +curYear && +item.month === +curMonth
+    )
+    if (this.Component.weekMode) {
+      newTodoLabels = todoDays
+    }
+    const allTodos = currentMonthTodoLabels.concat(newTodoLabels)
+    for (let todo of allTodos) {
+      let target
+      if (this.Component.weekMode) {
+        target = dates.find(
+          date =>
+            +todo.year === +date.year &&
+            +todo.month === +date.month &&
+            +todo.day === +date.day
+        )
+      } else {
+        target = dates[todo.day - 1]
+      }
+      if (!target) continue
+      if (showLabelAlways) {
+        target.showTodoLabel = true
+      } else {
+        target.showTodoLabel = !target.choosed
+      }
+      if (target.showTodoLabel) {
+        target.todoText = todo.todoText
+      }
+      target.color = todo.color
+    }
+    const o = {
+      'calendar.days': dates,
+      'calendar.todoLabels': uniqueArrayByDate(todoLabels.concat(todoDays))
+    }
+    if (!circle) {
+      o['calendar.todoLabelPos'] = pos
+      o['calendar.todoLabelColor'] = dotColor
+    }
+    o['calendar.todoLabelCircle'] = circle || false
+    o['calendar.showLabelAlways'] = showLabelAlways || false
+    this.setData(o)
+  }
+  /**
+   *  删除指定日期的待办事项
+   * @param {array} todos 需要删除待办事项的日期
+   */
+  deleteTodoLabels(todos) {
+    if (!(todos instanceof Array) || !todos.length) return
+    const todoLabels = this.filterTodos(todos)
+    const { days: dates, curYear, curMonth } = this.getData('calendar')
+    const currentMonthTodoLabels = todoLabels.filter(
+      item => curYear === +item.year && curMonth === +item.month
+    )
+    dates.forEach(item => {
+      item.showTodoLabel = false
+    })
+    currentMonthTodoLabels.forEach(item => {
+      dates[item.day - 1].showTodoLabel = !dates[item.day - 1].choosed
+    })
+    this.setData({
+      'calendar.days': dates,
+      'calendar.todoLabels': todoLabels
+    })
+  }
+  /**
+   * 清空所有待办事项
+   */
+  clearTodoLabels() {
+    const { days = [] } = this.getData('calendar')
+    const dates = [].concat(days)
+    dates.forEach(item => {
+      item.showTodoLabel = false
+    })
+    this.setData({
+      'calendar.days': dates,
+      'calendar.todoLabels': []
+    })
+  }
+  /**
+   * 获取所有待办事项
+   * @param {object} target 指定年月
+   * @param {number} [target.year] 年
+   * @param {number} [target.month] 月
+   */
+  getTodoLabels(target) {
+    const { todoLabels = [] } = this.getData('calendar')
+    if (target) {
+      const { year, month } = target
+      const _todoLabels = todoLabels.filter(
+        item => +item.year === +year && +item.month === +month
+      )
+      return _todoLabels
+    }
+    return todoLabels
+  }
+  /**
+   * 过滤将删除的待办事项
+   * @param {array} todos 需要删除待办事项
+   */
+  filterTodos(todos) {
+    const todoLabels = this.getData('calendar.todoLabels') || []
+    const deleteTodo = todos.map(item => getDate.toTimeStr(item))
+    return todoLabels.filter(
+      item => !deleteTodo.includes(getDate.toTimeStr(item))
+    )
+  }
+  /**
+   * 单选时显示待办事项
+   * @param {array} todoDays
+   * @param {array} days
+   * @param {array} selectedDays
+   */
+  showTodoLabels(todoDays, days, selectedDays) {
+    todoDays.forEach(item => {
+      if (this.Component.weekMode) {
+        days.forEach((_item, idx) => {
+          if (+_item.day === +item.day) {
+            const day = days[idx]
+            day.hasTodo = true
+            day.todoText = item.todoText
+            if (
+              selectedDays &&
+              selectedDays.length &&
+              +selectedDays[0].day === +item.day
+            ) {
+              day.showTodoLabel = true
+            }
+          }
+        })
+      } else {
+        const day = days[item.day - 1]
+        if (!day) return
+        day.hasTodo = true
+        day.todoText = item.todoText
+        if (
+          selectedDays &&
+          selectedDays.length &&
+          +selectedDays[0].day === +item.day
+        ) {
+          days[selectedDays[0].day - 1].showTodoLabel = true
+        }
+      }
+    })
+  }
+}
+
+export default component => new Todo(component)

+ 367 - 0
miniprogram/components/calendar/func/utils.js

@@ -0,0 +1,367 @@
+import convertSolarLunar from './convertSolarLunar'
+
+let systemInfo
+export function getSystemInfo() {
+  if (systemInfo) return systemInfo
+  systemInfo = wx.getSystemInfoSync()
+  return systemInfo
+}
+
+export function isComponent(target) {
+  return (
+    target &&
+    target.__wxExparserNodeId__ !== void 0 &&
+    typeof target.setData === 'function'
+  )
+}
+
+export class Logger {
+  info(msg) {
+    console.log(
+      '%cInfo: %c' + msg,
+      'color:#FF0080;font-weight:bold',
+      'color: #FF509B'
+    )
+  }
+  warn(msg) {
+    console.log(
+      '%cWarn: %c' + msg,
+      'color:#FF6600;font-weight:bold',
+      'color: #FF9933'
+    )
+  }
+  tips(msg) {
+    console.log(
+      '%cTips: %c' + msg,
+      'color:#00B200;font-weight:bold',
+      'color: #00CC33'
+    )
+  }
+}
+
+export class Slide {
+  /**
+   * 上滑
+   * @param {object} e 事件对象
+   * @returns {boolean} 布尔值
+   */
+  isUp(gesture = {}, touche = {}) {
+    const { startX, startY } = gesture
+    const deltaX = touche.clientX - startX
+    const deltaY = touche.clientY - startY
+    if (deltaY < -60 && deltaX < 20 && deltaX > -20) {
+      this.slideLock = false
+      return true
+    } else {
+      return false
+    }
+  }
+  /**
+   * 下滑
+   * @param {object} e 事件对象
+   * @returns {boolean} 布尔值
+   */
+  isDown(gesture = {}, touche = {}) {
+    const { startX, startY } = gesture
+    const deltaX = touche.clientX - startX
+    const deltaY = touche.clientY - startY
+    if (deltaY > 60 && deltaX < 20 && deltaX > -20) {
+      return true
+    } else {
+      return false
+    }
+  }
+  /**
+   * 左滑
+   * @param {object} e 事件对象
+   * @returns {boolean} 布尔值
+   */
+  isLeft(gesture = {}, touche = {}) {
+    const { startX, startY } = gesture
+    const deltaX = touche.clientX - startX
+    const deltaY = touche.clientY - startY
+    if (deltaX < -60 && deltaY < 20 && deltaY > -20) {
+      return true
+    } else {
+      return false
+    }
+  }
+  /**
+   * 右滑
+   * @param {object} e 事件对象
+   * @returns {boolean} 布尔值
+   */
+  isRight(gesture = {}, touche = {}) {
+    const { startX, startY } = gesture
+    const deltaX = touche.clientX - startX
+    const deltaY = touche.clientY - startY
+
+    if (deltaX > 60 && deltaY < 20 && deltaY > -20) {
+      return true
+    } else {
+      return false
+    }
+  }
+}
+
+export class GetDate {
+  /**
+   * new Date 区分平台
+   * @param {number} year
+   * @param {number} month
+   * @param {number} day
+   */
+  newDate(year, month, day) {
+    let cur = `${+year}-${+month}-${+day}`
+    if (isIos()) {
+      cur = `${+year}/${+month}/${+day}`
+    }
+    return new Date(cur)
+  }
+  /**
+   * 计算指定月份共多少天
+   * @param {number} year 年份
+   * @param {number} month  月份
+   */
+  thisMonthDays(year, month) {
+    return new Date(Date.UTC(year, month, 0)).getUTCDate()
+  }
+  /**
+   * 计算指定月份第一天星期几
+   * @param {number} year 年份
+   * @param {number} month  月份
+   */
+  firstDayOfWeek(year, month) {
+    return new Date(Date.UTC(year, month - 1, 1)).getUTCDay()
+  }
+  /**
+   * 计算指定日期星期几
+   * @param {number} year 年份
+   * @param {number} month  月份
+   * @param {number} date 日期
+   */
+  dayOfWeek(year, month, date) {
+    return new Date(Date.UTC(year, month - 1, date)).getUTCDay()
+  }
+  todayDate() {
+    const _date = new Date()
+    const year = _date.getFullYear()
+    const month = _date.getMonth() + 1
+    const date = _date.getDate()
+    return {
+      year,
+      month,
+      date
+    }
+  }
+  todayTimestamp() {
+    const { year, month, date } = this.todayDate()
+    const timestamp = this.newDate(year, month, date).getTime()
+    return timestamp
+  }
+  toTimeStr(dateInfo) {
+    if (dateInfo.day) {
+      dateInfo.date = dateInfo.day
+    }
+    return `${+dateInfo.year}-${+dateInfo.month}-${+dateInfo.date}`
+  }
+  sortDates(dates, sortType) {
+    return dates.sort(function(a, b) {
+      const at = getDateTimeStamp(a)
+      const bt = getDateTimeStamp(b)
+      if (at < bt && sortType !== 'desc') {
+        return -1
+      } else {
+        return 1
+      }
+    })
+  }
+  prevMonth(dataInfo) {
+    const prevMonthInfo =
+      +dataInfo.month > 1
+        ? {
+            year: dataInfo.year,
+            month: dataInfo.month - 1
+          }
+        : {
+            year: dataInfo.year - 1,
+            month: 12
+          }
+    return prevMonthInfo
+  }
+  nextMonth(dataInfo) {
+    const nextMonthInfo =
+      +dataInfo.month < 12
+        ? {
+            year: dataInfo.year,
+            month: dataInfo.month + 1
+          }
+        : {
+            year: dataInfo.year + 1,
+            month: 1
+          }
+    return nextMonthInfo
+  }
+  convertLunar(dates = []) {
+    const datesWithLunar = dates.map(date => {
+      if (date) {
+        date.lunar = convertSolarLunar.solar2lunar(
+          +date.year,
+          +date.month,
+          +date.day
+        )
+      }
+      return date
+    })
+    return datesWithLunar
+  }
+}
+
+export function isIos() {
+  const sys = getSystemInfo()
+  return /iphone|ios/i.test(sys.platform)
+}
+
+/**
+ * 浅比较对象是否相等
+ * @param {Object} origin 对比源
+ * @param {Object} target 对比目标
+ * @return {Boolean} true 为相等,false 为不等
+ */
+export function shallowEqual(origin, target) {
+  if (origin === target) {
+    return true
+  } else if (
+    typeof origin === 'object' &&
+    origin != null &&
+    typeof target === 'object' &&
+    target != null
+  ) {
+    if (Object.keys(origin).length !== Object.keys(target).length) return false
+    for (var prop in origin) {
+      if (target.hasOwnProperty(prop)) {
+        if (!shallowEqual(origin[prop], target[prop])) return false
+      } else return false
+    }
+    return true
+  } else return false
+}
+
+/**
+ * 获取当前页面实例
+ */
+export function getCurrentPage() {
+  const pages = getCurrentPages()
+  const last = pages.length - 1
+  return pages[last]
+}
+
+export function getComponent(componentId) {
+  const logger = new Logger()
+  let page = getCurrentPage() || {}
+  if (page.selectComponent && typeof page.selectComponent === 'function') {
+    if (componentId) {
+      return page.selectComponent(componentId)
+    } else {
+      logger.warn('请传入组件ID')
+    }
+  } else {
+    logger.warn('该基础库暂不支持多个小程序日历组件')
+  }
+}
+
+/**
+ * 日期数组根据日期去重
+ * @param {array} array 数组
+ */
+export function uniqueArrayByDate(array = []) {
+  let uniqueObject = {}
+  let uniqueArray = []
+  array.forEach(item => {
+    uniqueObject[`${item.year}-${item.month}-${item.day}`] = item
+  })
+  for (let i in uniqueObject) {
+    uniqueArray.push(uniqueObject[i])
+  }
+  return uniqueArray
+}
+
+/**
+ * 指定可选日期及可选日期数组去重
+ * @param {array} enableDays 特定可选日期数组
+ * @param {array} enableArea 可选日期区域数组
+ */
+export function delRepeatedEnableDay(enableDays = [], enableArea = []) {
+  let _startTimestamp
+  let _endTimestamp
+  if (enableArea.length === 2) {
+    const { startTimestamp, endTimestamp } = convertEnableAreaToTimestamp(
+      enableArea
+    )
+    _startTimestamp = startTimestamp
+    _endTimestamp = endTimestamp
+  }
+  const enableDaysTimestamp = converEnableDaysToTimestamp(enableDays)
+  const tmp = enableDaysTimestamp.filter(
+    item => item < _startTimestamp || item > _endTimestamp
+  )
+  return tmp
+}
+
+/**
+ *  指定日期区域转时间戳
+ * @param {array} timearea 时间区域
+ */
+export function convertEnableAreaToTimestamp(timearea = []) {
+  const getDate = new GetDate()
+  const start = timearea[0].split('-')
+  const end = timearea[1].split('-')
+  const logger = new Logger()
+  if (start.length !== 3 || end.length !== 3) {
+    logger.warn('enableArea() 参数格式为: ["2018-2-1", "2018-3-1"]')
+    return {}
+  }
+  const startTimestamp = getDate.newDate(start[0], start[1], start[2]).getTime()
+  const endTimestamp = getDate.newDate(end[0], end[1], end[2]).getTime()
+  return {
+    start,
+    end,
+    startTimestamp,
+    endTimestamp
+  }
+}
+
+/**
+ * 计算指定日期时间戳
+ * @param {object} dateInfo
+ */
+export function getDateTimeStamp(dateInfo) {
+  if (Object.prototype.toString.call(dateInfo) !== '[object Object]') return
+  const getDate = new GetDate()
+  return getDate.newDate(dateInfo.year, dateInfo.month, dateInfo.day).getTime()
+}
+
+/**
+ *  指定特定日期数组转时间戳
+ * @param {array} enableDays 指定时间数组
+ */
+export function converEnableDaysToTimestamp(enableDays = []) {
+  const logger = new Logger()
+  const getDate = new GetDate()
+  const enableDaysTimestamp = []
+  enableDays.forEach(item => {
+    if (typeof item !== 'string')
+      return logger.warn('enableDays()入参日期格式错误')
+    const tmp = item.split('-')
+    if (tmp.length !== 3) return logger.warn('enableDays()入参日期格式错误')
+    const timestamp = getDate.newDate(tmp[0], tmp[1], tmp[2]).getTime()
+    enableDaysTimestamp.push(timestamp)
+  })
+  return enableDaysTimestamp
+}
+
+// 同一页面多个日历组件按先后顺序渲染
+export const initialTasks = {
+  flag: 'finished', // process 处理中,finished 处理完成
+  tasks: []
+}

+ 601 - 0
miniprogram/components/calendar/func/week.js

@@ -0,0 +1,601 @@
+import Day from './day'
+import WxData from './wxData'
+import Render from './render'
+import CalendarConfig from './config'
+import convertSolarLunar from './convertSolarLunar'
+import { GetDate, Logger, getDateTimeStamp } from './utils'
+
+const getDate = new GetDate()
+const logger = new Logger()
+
+class WeekMode extends WxData {
+  constructor(component) {
+    super(component)
+    this.Component = component
+    this.getCalendarConfig = CalendarConfig(this.Component).getCalendarConfig
+  }
+  /**
+   * 周、月视图切换
+   * @param {string} view  视图 [week, month]
+   * @param {object} date  {year: 2017, month: 11, day: 1}
+   */
+  switchWeek(view, date) {
+    return new Promise((resolve, reject) => {
+      const config = CalendarConfig(this.Component).getCalendarConfig()
+      if (config.multi) return logger.warn('多选模式不能切换周月视图')
+      const { selectedDay = [], curYear, curMonth } = this.getData('calendar')
+      let currentDate = []
+      let disableSelected = false
+      if (!selectedDay.length) {
+        currentDate = getDate.todayDate()
+        currentDate.day = currentDate.date
+        disableSelected = true
+        // return this.__tipsWhenCanNotSwtich();
+      } else {
+        currentDate = selectedDay[0]
+      }
+      let selectedDate = date || currentDate
+      const { year, month } = selectedDate
+      const notInCurrentMonth = curYear !== year || curMonth !== month
+      if (view === 'week') {
+        if (this.Component.weekMode) return
+        if ((selectedDay.length && notInCurrentMonth) || !selectedDay.length) {
+          // return this.__tipsWhenCanNotSwtich();
+          disableSelected = true
+          selectedDate = {
+            year: curYear,
+            month: curMonth,
+            day: selectedDate.day
+          }
+        }
+        this.Component.weekMode = true
+        this.setData({
+          'calendarConfig.weekMode': true
+        })
+        this.jump(selectedDate, disableSelected)
+          .then(resolve)
+          .catch(reject)
+      } else {
+        this.Component.weekMode = false
+        this.setData({
+          'calendarConfig.weekMode': false
+        })
+        const disableSelected =
+          (selectedDay.length && notInCurrentMonth) || !selectedDay.length
+        Render(this.Component)
+          .renderCalendar(curYear, curMonth, selectedDate.day, disableSelected)
+          .then(resolve)
+          .catch(reject)
+      }
+    })
+  }
+  /**
+   * 更新当前年月
+   */
+  updateCurrYearAndMonth(type) {
+    let { days, curYear, curMonth } = this.getData('calendar')
+    const { month: firstMonth } = days[0]
+    const { month: lastMonth } = days[days.length - 1]
+    const lastDayOfThisMonth = getDate.thisMonthDays(curYear, curMonth)
+    const lastDayOfThisWeek = days[days.length - 1]
+    const firstDayOfThisWeek = days[0]
+    if (
+      (lastDayOfThisWeek.day + 7 > lastDayOfThisMonth ||
+        (curMonth === firstMonth && firstMonth !== lastMonth)) &&
+      type === 'next'
+    ) {
+      curMonth = curMonth + 1
+      if (curMonth > 12) {
+        curYear = curYear + 1
+        curMonth = 1
+      }
+    } else if (
+      (+firstDayOfThisWeek.day <= 7 ||
+        (curMonth === lastMonth && firstMonth !== lastMonth)) &&
+      type === 'prev'
+    ) {
+      curMonth = curMonth - 1
+      if (curMonth <= 0) {
+        curYear = curYear - 1
+        curMonth = 12
+      }
+    }
+    return {
+      Uyear: curYear,
+      Umonth: curMonth
+    }
+  }
+  /**
+   * 计算周视图下当前这一周和当月的最后一天
+   */
+  calculateLastDay() {
+    const { days = [], curYear, curMonth } = this.getData('calendar')
+    const lastDayInThisWeek = days[days.length - 1].day
+    const lastDayInThisMonth = getDate.thisMonthDays(curYear, curMonth)
+    return { lastDayInThisWeek, lastDayInThisMonth }
+  }
+  /**
+   * 计算周视图下当前这一周第一天
+   */
+  calculateFirstDay() {
+    const { days } = this.getData('calendar')
+    const firstDayInThisWeek = days[0].day
+    return { firstDayInThisWeek }
+  }
+  /**
+   * 当月第一周所有日期范围
+   * @param {number} year
+   * @param {number} month
+   * @param {boolean} firstDayOfWeekIsMon 每周是否配置为以周一开始
+   */
+  firstWeekInMonth(year, month, firstDayOfWeekIsMon) {
+    let firstDay = getDate.dayOfWeek(year, month, 1)
+    if (firstDayOfWeekIsMon && firstDay === 0) {
+      firstDay = 7
+    }
+    const [, end] = [0, 7 - firstDay]
+    let days = this.getData('calendar.days') || []
+    if (this.Component.weekMode) {
+      days = Day(this.Component).buildDate(year, month)
+    }
+    const daysCut = days.slice(0, firstDayOfWeekIsMon ? end + 1 : end)
+    return daysCut
+  }
+  /**
+   * 当月最后一周所有日期范围
+   * @param {number} year
+   * @param {number} month
+   * @param {boolean} firstDayOfWeekIsMon 每周是否配置为以周一开始
+   */
+  lastWeekInMonth(year, month, firstDayOfWeekIsMon) {
+    const lastDay = getDate.thisMonthDays(year, month)
+    const lastDayWeek = getDate.dayOfWeek(year, month, lastDay)
+    const [start, end] = [lastDay - lastDayWeek, lastDay]
+    let days = this.getData('calendar.days') || []
+    if (this.Component.weekMode) {
+      days = Day(this.Component).buildDate(year, month)
+    }
+    const daysCut = days.slice(firstDayOfWeekIsMon ? start : start - 1, end)
+    return daysCut
+  }
+  __getDisableDateTimestamp(config) {
+    const { date, type } = config.disableMode || {}
+    let disableDateTimestamp
+    if (date) {
+      const t = date.split('-')
+      if (t.length < 3) {
+        logger.warn('配置 disableMode.date 格式错误')
+        return {}
+      }
+      disableDateTimestamp = getDateTimeStamp({
+        year: +t[0],
+        month: +t[1],
+        day: +t[2]
+      })
+    }
+    return {
+      disableDateTimestamp,
+      disableType: type
+    }
+  }
+  /**
+   * 渲染日期之前初始化已选日期
+   * @param {array} dates 当前日期数组
+   */
+  initSelectedDay(dates) {
+    let datesCopy = [...dates]
+    const { selectedDay = [] } = this.getData('calendar')
+    const selectedDayStr = selectedDay.map(
+      item => `${+item.year}-${+item.month}-${+item.day}`
+    )
+    const config = this.getCalendarConfig()
+    const {
+      disableDateTimestamp,
+      disableType
+    } = this.__getDisableDateTimestamp(config)
+    datesCopy = datesCopy.map(item => {
+      if (!item) return {}
+      const dateTimestamp = getDateTimeStamp(item)
+      let date = { ...item }
+      if (
+        selectedDayStr.includes(`${+date.year}-${+date.month}-${+date.day}`)
+      ) {
+        date.choosed = true
+      } else {
+        date.choosed = false
+      }
+      if (
+        (disableType === 'after' && dateTimestamp > disableDateTimestamp) ||
+        (disableType === 'before' && dateTimestamp < disableDateTimestamp)
+      ) {
+        date.disable = true
+      }
+      date = this.__setTodoWhenJump(date, config)
+      if (config.showLunar) {
+        date = this.__setSolarLunar(date)
+      }
+      if (config.highlightToday) {
+        date = this.__highlightToday(date)
+      }
+      return date
+    })
+    return datesCopy
+  }
+  /**
+   * 周视图下设置可选日期范围
+   * @param {object} days 当前展示的日期
+   */
+  setEnableAreaOnWeekMode(dates = []) {
+    let { enableAreaTimestamp = [], enableDaysTimestamp = [] } = this.getData(
+      'calendar'
+    )
+    dates.forEach(item => {
+      const timestamp = getDate
+        .newDate(item.year, item.month, item.day)
+        .getTime()
+
+      let setDisable = false
+      if (enableAreaTimestamp.length) {
+        if (
+          (+enableAreaTimestamp[0] > +timestamp ||
+            +timestamp > +enableAreaTimestamp[1]) &&
+          !enableDaysTimestamp.includes(+timestamp)
+        ) {
+          setDisable = true
+        }
+      } else if (
+        enableDaysTimestamp.length &&
+        !enableDaysTimestamp.includes(+timestamp)
+      ) {
+        setDisable = true
+      }
+      if (setDisable) {
+        item.disable = true
+        item.choosed = false
+      }
+      const config = CalendarConfig(this.Component).getCalendarConfig()
+      const {
+        disableDateTimestamp,
+        disableType
+      } = this.__getDisableDateTimestamp(config)
+      if (
+        (disableType === 'before' && timestamp < disableDateTimestamp) ||
+        (disableType === 'after' && timestamp > disableDateTimestamp)
+      ) {
+        item.disable = true
+      }
+    })
+  }
+  updateYMWhenSwipeCalendarHasSelected(dates) {
+    const hasSelectedDate = dates.filter(date => date.choosed)
+    if (hasSelectedDate && hasSelectedDate.length) {
+      const { year, month } = hasSelectedDate[0]
+      return {
+        year,
+        month
+      }
+    }
+    return {}
+  }
+  /**
+   * 计算下一周的日期
+   */
+  calculateNextWeekDays() {
+    let { lastDayInThisWeek, lastDayInThisMonth } = this.calculateLastDay()
+    let { curYear, curMonth } = this.getData('calendar')
+    let days = []
+    if (lastDayInThisMonth - lastDayInThisWeek >= 7) {
+      const { Uyear, Umonth } = this.updateCurrYearAndMonth('next')
+      curYear = Uyear
+      curMonth = Umonth
+      for (let i = lastDayInThisWeek + 1; i <= lastDayInThisWeek + 7; i++) {
+        days.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+    } else {
+      for (let i = lastDayInThisWeek + 1; i <= lastDayInThisMonth; i++) {
+        days.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+      const { Uyear, Umonth } = this.updateCurrYearAndMonth('next')
+      curYear = Uyear
+      curMonth = Umonth
+      for (let i = 1; i <= 7 - (lastDayInThisMonth - lastDayInThisWeek); i++) {
+        days.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+    }
+    days = this.initSelectedDay(days)
+    const {
+      year: updateYear,
+      month: updateMonth
+    } = this.updateYMWhenSwipeCalendarHasSelected(days)
+    if (updateYear && updateMonth) {
+      curYear = updateYear
+      curMonth = updateMonth
+    }
+    this.setEnableAreaOnWeekMode(days)
+    this.setData(
+      {
+        'calendar.curYear': curYear,
+        'calendar.curMonth': curMonth,
+        'calendar.days': days
+      },
+      () => {
+        Day(this.Component).setDateStyle()
+      }
+    )
+  }
+  /**
+   * 计算上一周的日期
+   */
+  calculatePrevWeekDays() {
+    let { firstDayInThisWeek } = this.calculateFirstDay()
+    let { curYear, curMonth } = this.getData('calendar')
+    let days = []
+
+    if (firstDayInThisWeek - 7 > 0) {
+      const { Uyear, Umonth } = this.updateCurrYearAndMonth('prev')
+      curYear = Uyear
+      curMonth = Umonth
+      for (let i = firstDayInThisWeek - 7; i < firstDayInThisWeek; i++) {
+        days.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+    } else {
+      let temp = []
+      for (let i = 1; i < firstDayInThisWeek; i++) {
+        temp.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+      const { Uyear, Umonth } = this.updateCurrYearAndMonth('prev')
+      curYear = Uyear
+      curMonth = Umonth
+      const prevMonthDays = getDate.thisMonthDays(curYear, curMonth)
+      for (
+        let i = prevMonthDays - Math.abs(firstDayInThisWeek - 7);
+        i <= prevMonthDays;
+        i++
+      ) {
+        days.push({
+          year: curYear,
+          month: curMonth,
+          day: i,
+          week: getDate.dayOfWeek(curYear, curMonth, i)
+        })
+      }
+      days = days.concat(temp)
+    }
+    days = this.initSelectedDay(days)
+    const {
+      year: updateYear,
+      month: updateMonth
+    } = this.updateYMWhenSwipeCalendarHasSelected(days)
+    if (updateYear && updateMonth) {
+      curYear = updateYear
+      curMonth = updateMonth
+    }
+    this.setEnableAreaOnWeekMode(days)
+    this.setData(
+      {
+        'calendar.curYear': curYear,
+        'calendar.curMonth': curMonth,
+        'calendar.days': days
+      },
+      () => {
+        Day(this.Component).setDateStyle()
+      }
+    )
+  }
+  calculateDatesWhenJump(
+    { year, month, day },
+    { firstWeekDays, lastWeekDays },
+    firstDayOfWeekIsMon
+  ) {
+    const inFirstWeek = this.__dateIsInWeek({ year, month, day }, firstWeekDays)
+    const inLastWeek = this.__dateIsInWeek({ year, month, day }, lastWeekDays)
+    let dates = []
+    if (inFirstWeek) {
+      dates = this.__calculateDatesWhenInFirstWeek(
+        firstWeekDays,
+        firstDayOfWeekIsMon
+      )
+    } else if (inLastWeek) {
+      dates = this.__calculateDatesWhenInLastWeek(
+        lastWeekDays,
+        firstDayOfWeekIsMon
+      )
+    } else {
+      dates = this.__calculateDates({ year, month, day }, firstDayOfWeekIsMon)
+    }
+    return dates
+  }
+  jump({ year, month, day }, disableSelected) {
+    return new Promise(resolve => {
+      if (!day) return
+      const config = this.getCalendarConfig()
+      const firstDayOfWeekIsMon = config.firstDayOfWeek === 'Mon'
+      const firstWeekDays = this.firstWeekInMonth(
+        year,
+        month,
+        firstDayOfWeekIsMon
+      )
+      let lastWeekDays = this.lastWeekInMonth(year, month, firstDayOfWeekIsMon)
+      let dates = this.calculateDatesWhenJump(
+        { year, month, day },
+        {
+          firstWeekDays,
+          lastWeekDays
+        },
+        firstDayOfWeekIsMon
+      )
+      dates = dates.map(d => {
+        let date = { ...d }
+        if (
+          +date.year === +year &&
+          +date.month === +month &&
+          +date.day === +day &&
+          !disableSelected
+        ) {
+          date.choosed = true
+        }
+        date = this.__setTodoWhenJump(date, config)
+        if (config.showLunar) {
+          date = this.__setSolarLunar(date)
+        }
+        if (config.highlightToday) {
+          date = this.__highlightToday(date)
+        }
+        return date
+      })
+      this.setEnableAreaOnWeekMode(dates)
+      const tmpData = {
+        'calendar.days': dates,
+        'calendar.curYear': year,
+        'calendar.curMonth': month,
+        'calendar.empytGrids': [],
+        'calendar.lastEmptyGrids': []
+      }
+      if (!disableSelected) {
+        tmpData['calendar.selectedDay'] = dates.filter(item => item.choosed)
+      }
+      this.setData(tmpData, () => {
+        Day(this.Component).setDateStyle()
+        resolve({ year, month, date: day })
+      })
+    })
+  }
+  __setTodoWhenJump(dateInfo) {
+    const date = { ...dateInfo }
+    const { todoLabels = [], showLabelAlways } = this.getData('calendar')
+    const todosStr = todoLabels.map(d => `${+d.year}-${+d.month}-${+d.day}`)
+    const idx = todosStr.indexOf(`${+date.year}-${+date.month}-${+date.day}`)
+    if (idx !== -1) {
+      if (showLabelAlways) {
+        date.showTodoLabel = true
+      } else {
+        date.showTodoLabel = !date.choosed
+      }
+      const todo = todoLabels[idx] || {}
+      if (date.showTodoLabel && todo.todoText) date.todoText = todo.todoText
+      if (todo.color) date.color = todo.color
+    }
+    return date
+  }
+  __setSolarLunar(dateInfo) {
+    const date = { ...dateInfo }
+    date.lunar = convertSolarLunar.solar2lunar(
+      +date.year,
+      +date.month,
+      +date.day
+    )
+    return date
+  }
+  __highlightToday(dateInfo) {
+    const date = { ...dateInfo }
+    const today = getDate.todayDate()
+    const isToday =
+      +today.year === +date.year &&
+      +today.month === +date.month &&
+      +date.day === +today.date
+    date.isToday = isToday
+    return date
+  }
+  __calculateDatesWhenInFirstWeek(firstWeekDays) {
+    const dates = [...firstWeekDays]
+    if (dates.length < 7) {
+      let { year, month } = dates[0]
+      let len = 7 - dates.length
+      let lastDate
+      if (month > 1) {
+        month -= 1
+        lastDate = getDate.thisMonthDays(year, month)
+      } else {
+        month = 12
+        year -= 1
+        lastDate = getDate.thisMonthDays(year, month)
+      }
+      while (len) {
+        dates.unshift({
+          year,
+          month,
+          day: lastDate,
+          week: getDate.dayOfWeek(year, month, lastDate)
+        })
+        lastDate -= 1
+        len -= 1
+      }
+    }
+    return dates
+  }
+  __calculateDatesWhenInLastWeek(lastWeekDays) {
+    const dates = [...lastWeekDays]
+    if (dates.length < 7) {
+      let { year, month } = dates[0]
+      let len = 7 - dates.length
+      let firstDate = 1
+      if (month > 11) {
+        month = 1
+        year += 1
+      } else {
+        month += 1
+      }
+      while (len) {
+        dates.push({
+          year,
+          month,
+          day: firstDate,
+          week: getDate.dayOfWeek(year, month, firstDate)
+        })
+        firstDate += 1
+        len -= 1
+      }
+    }
+    return dates
+  }
+  __calculateDates({ year, month, day }, firstDayOfWeekIsMon) {
+    const week = getDate.dayOfWeek(year, month, day)
+    let range = [day - week, day + (6 - week)]
+    if (firstDayOfWeekIsMon) {
+      range = [day + 1 - week, day + (7 - week)]
+    }
+    const dates = Day(this.Component).buildDate(year, month)
+    const weekDates = dates.slice(range[0] - 1, range[1])
+    return weekDates
+  }
+  __dateIsInWeek(date, week) {
+    return week.find(
+      item =>
+        +item.year === +date.year &&
+        +item.month === +date.month &&
+        +item.day === +date.day
+    )
+  }
+  __tipsWhenCanNotSwtich() {
+    logger.info(
+      '当前月份未选中日期下切换为周视图,不能明确该展示哪一周的日期,故此情况不允许切换'
+    )
+  }
+}
+
+export default component => new WeekMode(component)

+ 26 - 0
miniprogram/components/calendar/func/wxData.js

@@ -0,0 +1,26 @@
+class WxData {
+  constructor(component) {
+    this.Component = component
+  }
+  getData(key) {
+    const data = this.Component.data
+    if (!key) return data
+    if (key.includes('.')) {
+      let keys = key.split('.')
+      const tmp = keys.reduce((prev, next) => {
+        return prev[next]
+      }, data)
+      return tmp
+    } else {
+      return this.Component.data[key]
+    }
+  }
+  setData(data, cb = () => {}) {
+    if (!data) return
+    if (typeof data === 'object') {
+      this.Component.setData(data, cb)
+    }
+  }
+}
+
+export default WxData

+ 6 - 0
miniprogram/components/calendar/index.json

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

+ 342 - 0
miniprogram/components/calendar/index.scss

@@ -0,0 +1,342 @@
+@import "./theme/iconfont.wxss";
+@import "./theme/theme-default.wxss";
+@import "./theme/theme-elegant.wxss";
+
+.b {
+  display: flex;
+}
+
+.lr {
+  flex-direction: row;
+}
+
+.tb {
+  flex-direction: column;
+}
+
+.pc {
+  justify-content: center;
+}
+
+.ac {
+  align-items: center;
+}
+
+.cc {
+  align-items: center;
+  justify-content: center;
+}
+
+.wrap {
+  flex-wrap: wrap;
+}
+
+.flex {
+  flex-grow: 1;
+}
+
+.bg {
+  background-image: linear-gradient(to bottom, #faefe7, #ffcbd7);
+  overflow: hidden;
+}
+
+.white-color {
+  color: #fff;
+}
+
+.fs24 {
+  font-size: 24rpx;
+}
+
+.fs28 {
+  font-size: 28rpx;
+}
+
+.fs32 {
+  font-size: 32rpx;
+}
+
+.fs36 {
+  font-size: 36rpx;
+  display: none;
+}
+
+.calendar {
+  width: 100%;
+  box-sizing: border-box;
+}
+
+/* 日历操作栏 */
+
+.handle {
+  height: 80rpx;
+  /* display: none; */
+}
+
+.prev-handle,
+.next-handle {
+  padding: 20rpx;
+}
+
+.date-in-handle {
+  height: 80rpx;
+}
+
+/* 星期栏 */
+
+.weeks {
+  height: 50rpx;
+  line-height: 50rpx;
+  opacity: 0.5;
+}
+
+.week {
+  text-align: center;
+}
+.default_prev-month-date {
+  color: #a2a3a4 !important;
+}
+.default_normal-date {
+  color: black !important;
+}
+.default_next-month-date {
+  color: #a2a3a4 !important;
+}
+.grid,
+.week {
+  width: 14.2857%;
+  color: #171a1d;
+}
+
+.date-wrap {
+  width: 100%;
+  height: 80rpx;
+  position: relative;
+  left: 0;
+  top: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.date {
+  position: relative;
+  left: 0;
+  top: 0;
+  width: 55rpx;
+  height: 55rpx;
+  text-align: center;
+  line-height: 55rpx;
+  font-size: 26rpx;
+  font-weight: 200;
+  border-radius: 50%;
+  transition: all 0.3s;
+  animation-name: choosed;
+  animation-duration: 0.5s;
+  animation-timing-function: linear;
+  animation-iteration-count: 1;
+}
+
+.date-area-mode {
+  width: 100%;
+  border-radius: 0;
+}
+
+.date-desc {
+  width: 150%;
+  height: 32rpx;
+  font-size: 20rpx;
+  line-height: 32rpx;
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  overflow: hidden;
+  word-break: break-all;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  -webkit-line-clamp: 1;
+  text-align: center;
+}
+
+@keyframes choosed {
+  from {
+    transform: scale(1);
+  }
+
+  50% {
+    transform: scale(0.9);
+  }
+
+  to {
+    transform: scale(1);
+  }
+}
+
+/* 日期圆圈标记 */
+.todo-circle {
+  border-width: 1rpx;
+  border-style: solid;
+  box-sizing: border-box;
+}
+
+/* 待办点标记相关样式 */
+.todo-dot {
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.todo-dot-top {
+  top: 3rpx;
+}
+
+.todo-dot.todo-dot-top-always {
+  top: -8rpx;
+}
+
+.todo-dot.todo-dot-bottom {
+  bottom: 0;
+}
+
+.todo-dot.todo-dot-bottom-always {
+  bottom: -10rpx;
+}
+
+/* 日期描述文字(待办文字/农历)相关样式 */
+
+.date-desc.date-desc-top {
+  top: -6rpx;
+}
+
+.date-desc.date-desc-top-always {
+  top: -20rpx;
+}
+
+.date-desc.date-desc-bottom {
+  bottom: -14rpx;
+}
+
+.todo-circle .date-desc.date-desc-bottom {
+  bottom: -30rpx;
+}
+
+.date-desc.date-desc-bottom-always {
+  bottom: -28rpx;
+}
+.grid.dim .date,
+.grid.dim .date-desc,
+.grid.dim .dot {
+  // opacity: 0.5;
+}
+.grid.other-month .date,
+.grid.other-month .date-desc,
+.grid.other-month .dot {
+  color: #ccc;
+}
+.grid.current-month .date,
+.grid.current-month .date-desc {
+  color: #222;
+}
+.grid.today .date {
+  background: #eaf4ff;
+  color: #2e7fff;
+  font-weight: 600;
+}
+.grid.choosed .date {
+  background: #2e7fff;
+  color: #fff;
+  font-weight: bold;
+  box-shadow: 0 0 8rpx #2e7fff66;
+  border-radius: 50%;
+  border: none;
+
+ 
+}
+.dot-self-unchecked {
+  background: #ff9800;
+}
+.dot-self-checked {
+  background: #2e7fff;
+}
+.dot-other-unchecked {
+  background: #ff4d4f;
+}
+
+.dot {
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  margin: 0 auto;
+  margin-top: 4rpx;
+}
+
+/* 不同状态不同颜色 */
+.dot-3 {
+  background: #2e7fff;
+} /* 打卡完成 */
+.dot-1 {
+    background: #ff4d4f;
+  } /* 本项目未打卡 */
+
+.dot-2 {
+  background: #ff9800;
+} /* 其他项目未打卡 */
+
+.status-box {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20rpx;
+  .patch-btn {
+    //   margin-left: 24rpx;
+    background: #1d6ff6;
+    color: #fff;
+    border-radius: 12rpx;
+    //   padding: 0 32rpx;
+    height: 56rpx;
+    line-height: 56rpx;
+    font-size: 28rpx;
+    border: none;
+  }
+}
+.calendar-footer {
+  margin-top: 48rpx;
+  width: 100%;
+  .status-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20rpx;
+
+    .status-label {
+    //   font-size: 26rpx;
+    }
+
+    .status-content {
+    //   font-size: 26rpx;
+    }
+
+    .unpunch {
+      color: #d54941;
+    }
+
+    .punch {
+      color: #222;
+      margin-right: 10rpx;
+    }
+
+    .patch-btn {
+    //   margin-left: 24rpx;
+      background: #1d6ff6;
+      color: #fff;
+      border-radius: 12rpx;
+    //   padding: 0 32rpx;
+      height: 56rpx;
+      line-height: 56rpx;
+      font-size: 28rpx;
+      border: none;
+    }
+  }
+}

+ 502 - 0
miniprogram/components/calendar/index.ts

@@ -0,0 +1,502 @@
+import Week from "./func/week";
+import { Logger, Slide, GetDate, initialTasks } from "./func/utils";
+import initCalendar, {
+  jump,
+  getCurrentYM,
+  whenChangeDate,
+  renderCalendar,
+  whenMulitSelect,
+  whenSingleSelect,
+  whenChooseArea,
+  getCalendarDates,
+} from "./main.js";
+
+import {
+  getPatientOnlineRecord,
+  addPatientOnlineRecord,
+} from "../../pages/home/request";
+import tickleBehavior, {
+  getTickleContext,
+} from "../../core/behavior/tickle.behavior";
+const slide = new Slide();
+const logger = new Logger();
+const getDate = new GetDate();
+
+// 获取上个月
+function getPrevMonth(year, month) {
+  if (month === 1) {
+    return { year: year - 1, month: 12 };
+  }
+  return { year, month: month - 1 };
+}
+// 获取下个月
+function getNextMonth(year, month) {
+  if (month === 12) {
+    return { year: year + 1, month: 1 };
+  }
+  return { year, month: month + 1 };
+}
+
+function pad(num) {
+  return num < 10 ? "0" + num : "" + num;
+}
+
+Component({
+  options: {
+    styleIsolation: "apply-shared",
+    multipleSlots: true, // 在组件定义时的选项中启用多slot支持
+  },
+  properties: {
+    calendarConfig: {
+      type: Object,
+      value: {},
+    },
+  },
+  behaviors: [tickleBehavior],
+  data: {
+    noClockIn: [],
+    clockIn: [],
+    id: "",
+    handleMap: {
+      prev_year: "chooseYear",
+      prev_month: "chooseMonth",
+      next_month: "chooseMonth",
+      next_year: "chooseYear",
+    },
+    currentMonth: null,
+    currentYear: null,
+    firstDate: null,
+    lastDate: null,
+    dateArrType: 1,
+    days: [],
+    dayData: "",
+    isFutureDate: false,
+  },
+  observers: {
+    "calendar.days": function (newVal: any) {
+      // 简化条件判断,只检查 newVal
+      if (
+        newVal &&
+        newVal.length > 0 &&
+        newVal[0] &&
+        newVal[0].year &&
+        newVal[0].month
+      ) {
+        this.setData({
+          currentMonth: newVal[0].month,
+          currentYear: newVal[0].year,
+        });
+        let firstDate = null;
+        const empytGrids = this.data.calendar.empytGrids || [];
+        const days = newVal || [];
+        let prevData = null;
+        if (empytGrids.length > 0) {
+          // 有上月补齐格子,第一个就是上月的
+          prevData = {
+            year: getPrevMonth(days[0].year, days[0].month).year,
+            month: getPrevMonth(days[0].year, days[0].month).month,
+            day: empytGrids[0].day,
+          };
+        } else if (days.length > 0) {
+          // 没有上月补齐格子,第一个就是本月1号
+          prevData = {
+            year: days[0].year,
+            month: days[0].month,
+            day: days[0].day,
+          };
+        }
+        let lastDate = null;
+        let nextData = null;
+        const lastEmptyGrids = this.data.calendar.lastEmptyGrids || [];
+        if (lastEmptyGrids.length > 0) {
+          nextData = {
+            year: getNextMonth(days[0].year, days[0].month).year,
+            month: getNextMonth(days[0].year, days[0].month).month,
+            day: lastEmptyGrids[lastEmptyGrids.length - 1].day,
+          };
+        } else if (days.length > 0) {
+          nextData = {
+            year: days[days.length - 1].year,
+            month: days[days.length - 1].month,
+            day: days[days.length - 1].day,
+          };
+        }
+        lastDate =
+          nextData.year + "-" + pad(nextData.month) + "-" + pad(nextData.day);
+        firstDate =
+          prevData.year + "-" + pad(prevData.month) + "-" + pad(prevData.day);
+        this.setData({
+          firstDate,
+          lastDate,
+        });
+        this.setData({
+          id: wx.getStorageSync("recordId") || "",
+        });
+        if (this.data.id) {
+          this.getCaRecord(this.data.id, firstDate, lastDate);
+        } else {
+          setTimeout(() => {
+            this.getCaRecord(this.data.id, firstDate, lastDate);
+          }, 2000);
+        }
+      }
+    },
+  },
+  lifetimes: {
+    attached: async function () {
+      this.initComp();
+    },
+    detached: function () {
+      initialTasks.flag = "finished";
+      initialTasks.tasks.length = 0;
+    },
+  },
+  methods: {
+    // 补卡
+    async onPatchCard(e: any) {
+      try {
+        const cardId = e.currentTarget.dataset.id;
+        await addPatientOnlineRecord(cardId);
+        wx.showToast({
+          title: "补卡成功",
+          icon: "success",
+          duration: 1500,
+        });
+        await this.getCaRecord(
+          this.data.id,
+          this.data.firstDate,
+          this.data.lastDate
+        );
+      } catch (error: any) {
+        getTickleContext.call(this).showWarnMessage(error.errMsg);
+      }
+    },
+    async getCaRecord(id: string | number, startDate: string, endDate: string) {
+      try {
+        const res = await getPatientOnlineRecord(id, startDate, endDate);
+        const { currentYear, currentMonth } = this.data;
+        if (res && res.length > 0) {
+          // 1. 先筛选出本月的数据
+          const monthData = (res || []).filter(
+            (item: any) =>
+              item.arrangeYear == currentYear && item.arrangeMon == currentMonth
+          );
+          // 2. 遍历本月 days,赋值
+          (this.data.calendar.days || []).forEach((dayItem: any) => {
+            // 只处理本月的格子
+            if (dayItem.year == currentYear && dayItem.month == currentMonth) {
+              // 只给当前日期及之前的日期赋值
+              const currentDate = new Date();
+              const itemDate = new Date(
+                dayItem.year,
+                dayItem.month - 1,
+                dayItem.day
+              );
+
+              // 如果日期是今天或之前,才进行赋值
+              if (itemDate <= currentDate) {
+                const match = monthData.find(
+                  (d: any) => d.arrangeDay == dayItem.day
+                );
+
+                if (match) {
+                  dayItem.clockIn = [...match.clockIn];
+                  dayItem.noClockIn = [...match.noClockIn];
+                  dayItem.type = match.type;
+                }
+              } else {
+                // 未来日期设置空的打卡数据,确保不显示打卡和补卡功能
+                dayItem.clockIn = [];
+                dayItem.noClockIn = [];
+                dayItem.type = null;
+              }
+            }
+          });
+          const matchData = monthData.filter(
+            (d: any) => d.arrangeDay == this.data.dayData
+          );
+
+          if (matchData.length > 0) {
+            this.setData({
+              noClockIn: matchData[0].noClockIn,
+              clockIn: matchData[0].clockIn,
+            });
+          }
+          this.setData({
+            days: this.data.calendar.days,
+          });
+
+          if (this.data.dateArrType == 1) {
+            const currentDay = this.data.days.find(
+              (item: any) => item.lunar.isToday
+            );
+            if (currentDay) {
+              // 检查当前日期是否为未来日期
+              const currentDate = new Date();
+              const itemDate = new Date(
+                currentDay.year,
+                currentDay.month - 1,
+                currentDay.day
+              );
+              const isCurrentDayFuture = itemDate > currentDate;
+
+              if (!isCurrentDayFuture) {
+                this.setData({
+                  noClockIn: currentDay.noClockIn,
+                  clockIn: currentDay.clockIn,
+                  isFutureDate: false,
+                });
+              } else {
+                this.setData({
+                  noClockIn: [],
+                  clockIn: [],
+                  isFutureDate: true,
+                });
+              }
+            }
+          }
+        }
+      } catch (error) {
+        getTickleContext.call(this).showWarnMessage(error.errMsg);
+      }
+    },
+    initComp() {
+      const calendarConfig = this.setDefaultDisableDate();
+      this.setConfig(calendarConfig);
+    },
+    setDefaultDisableDate() {
+      const calendarConfig = this.properties.calendarConfig || {};
+      if (calendarConfig.disableMode && !calendarConfig.disableMode.date) {
+        calendarConfig.disableMode.date = getDate.toTimeStr(
+          getDate.todayDate()
+        );
+      }
+      return calendarConfig;
+    },
+    setConfig(config) {
+      if (config.markToday && typeof config.markToday === "string") {
+        config.highlightToday = true;
+      }
+      config.theme = config.theme || "default";
+      this.weekMode = config.weekMode;
+      this.setData(
+        {
+          calendarConfig: config,
+        },
+        () => {
+          initCalendar(this, config);
+        }
+      );
+    },
+    chooseDate(e) {
+      const { type } = e.currentTarget.dataset;
+      if (!type) return;
+      const methodName = this.data.handleMap[type];
+      this[methodName](type);
+    },
+    chooseYear(type) {
+      const { curYear, curMonth } = this.data.calendar;
+      if (!curYear || !curMonth) return logger.warn("异常:未获取到当前年月");
+      if (this.weekMode) {
+        return console.warn("周视图下不支持点击切换年月");
+      }
+      let newYear = +curYear;
+      let newMonth = +curMonth;
+      if (type === "prev_year") {
+        newYear -= 1;
+      } else if (type === "next_year") {
+        newYear += 1;
+      }
+      this.render(curYear, curMonth, newYear, newMonth);
+    },
+    chooseMonth(type) {
+      const { curYear, curMonth } = this.data.calendar;
+      if (!curYear || !curMonth) return logger.warn("异常:未获取到当前年月");
+      if (this.weekMode) return console.warn("周视图下不支持点击切换年月");
+      let newYear = +curYear;
+      let newMonth = +curMonth;
+      if (type === "prev_month") {
+        newMonth = newMonth - 1;
+        if (newMonth < 1) {
+          newYear -= 1;
+          newMonth = 12;
+        }
+      } else if (type === "next_month") {
+        newMonth += 1;
+        if (newMonth > 12) {
+          newYear += 1;
+          newMonth = 1;
+        }
+      }
+      this.render(curYear, curMonth, newYear, newMonth);
+    },
+    render(curYear, curMonth, newYear, newMonth) {
+      whenChangeDate.call(this, {
+        curYear,
+        curMonth,
+        newYear,
+        newMonth,
+      });
+      this.setData({
+        "calendar.curYear": newYear,
+        "calendar.curMonth": newMonth,
+      });
+      renderCalendar.call(this, newYear, newMonth);
+    },
+    /**
+     * 日期点击事件
+     * @param {!object} e 事件对象
+     */
+    tapDayItem(e) {
+      this.setData({
+        dateArrType: 2,
+      });
+      const { idx, date = {} } = e.currentTarget.dataset;
+      const selectedDate = `${date.year}  ${date.month}.${date.day}`;
+      this.triggerEvent("onSelect", {
+        selectedDate,
+      });
+      const { day, disable, clockIn, noClockIn, type } = date;
+
+      // 检查是否为未来日期
+      const currentDate = new Date();
+      const itemDate = new Date(date.year, date.month - 1, date.day);
+      const isFutureDate = itemDate > currentDate;
+
+      // 如果是未来日期,设置空的打卡数据,但仍然可以点击
+      if (isFutureDate) {
+        this.setData({
+          noClockIn: [],
+          clockIn: [],
+          dayData: day,
+          isFutureDate: true,
+        });
+      } else {
+        this.setData({
+          noClockIn: noClockIn || [],
+          clockIn: clockIn || [],
+          dayData: day,
+          isFutureDate: false,
+        });
+      }
+
+      if (disable || !day) return;
+      const config = this.data.calendarConfig || this.config || {};
+      const { multi, chooseAreaMode } = config;
+      if (multi) {
+        whenMulitSelect.call(this, idx);
+      } else if (chooseAreaMode) {
+        whenChooseArea.call(this, idx);
+      } else {
+        whenSingleSelect.call(this, idx);
+      }
+      this.setData({
+        "calendar.noDefault": false,
+      });
+    },
+    doubleClickToToday() {
+      if (this.config.multi || this.weekMode) return;
+      if (this.count === undefined) {
+        this.count = 1;
+      } else {
+        this.count += 1;
+      }
+      if (this.lastClick) {
+        const difference = new Date().getTime() - this.lastClick;
+        if (difference < 500 && this.count >= 2) {
+          jump.call(this);
+        }
+        this.count = undefined;
+        this.lastClick = undefined;
+      } else {
+        this.lastClick = new Date().getTime();
+      }
+    },
+    /**
+     * 日历滑动开始
+     * @param {object} e
+     */
+    calendarTouchstart(e) {
+      const t = e.touches[0];
+      const startX = t.clientX;
+      const startY = t.clientY;
+      this.slideLock = true; // 滑动事件加锁
+      this.setData({
+        "gesture.startX": startX,
+        "gesture.startY": startY,
+      });
+    },
+    /**
+     * 日历滑动中
+     * @param {object} e
+     */
+    calendarTouchmove(e) {
+      const { gesture } = this.data;
+      const { preventSwipe } = this.properties.calendarConfig;
+      if (!this.slideLock || preventSwipe) return;
+      if (slide.isLeft(gesture, e.touches[0])) {
+        this.handleSwipe("left");
+        this.slideLock = false;
+      }
+      if (slide.isRight(gesture, e.touches[0])) {
+        this.handleSwipe("right");
+        this.slideLock = false;
+      }
+    },
+    calendarTouchend(e) {
+      this.setData({
+        "calendar.leftSwipe": 0,
+        "calendar.rightSwipe": 0,
+      });
+    },
+    handleSwipe(direction) {
+      let swipeKey = "calendar.leftSwipe";
+      let swipeCalendarType = "next_month";
+      let weekChangeType = "next_week";
+      if (direction === "right") {
+        swipeKey = "calendar.rightSwipe";
+        swipeCalendarType = "prev_month";
+        weekChangeType = "prev_week";
+      }
+      this.setData({
+        [swipeKey]: 1,
+      });
+      this.currentYM = getCurrentYM();
+      if (this.weekMode) {
+        this.slideLock = false;
+        this.currentDates = getCalendarDates();
+        if (weekChangeType === "prev_week") {
+          Week(this).calculatePrevWeekDays();
+        } else if (weekChangeType === "next_week") {
+          Week(this).calculateNextWeekDays();
+        }
+        this.onSwipeCalendar(weekChangeType);
+        this.onWeekChange(weekChangeType);
+        return;
+      }
+      this.chooseMonth(swipeCalendarType);
+      this.onSwipeCalendar(swipeCalendarType);
+    },
+    onSwipeCalendar(direction) {
+      this.triggerEvent("onSwipe", {
+        directionType: direction,
+        currentYM: this.currentYM,
+      });
+    },
+    onWeekChange(direction) {
+      this.triggerEvent("whenChangeWeek", {
+        current: {
+          currentYM: this.currentYM,
+          dates: [...this.currentDates],
+        },
+        next: {
+          currentYM: getCurrentYM(),
+          dates: getCalendarDates(),
+        },
+        directionType: direction,
+      });
+      this.currentDates = null;
+      this.currentYM = null;
+    },
+  },
+});

+ 98 - 0
miniprogram/components/calendar/index.wxml

@@ -0,0 +1,98 @@
+<view class="flex b tb ac" wx:if="{{calendar}}">
+  <view class="calendar b tb">
+    <!-- 头部操作栏 -->
+    <view wx:if="{{!calendarConfig.hideHeadOnWeekMode}}" class="handle {{calendarConfig.theme}}_handle-color fs28 b lr ac pc">
+      <view class="prev fs36" wx:if="{{calendarConfig.showHandlerOnWeekMode || !calendarConfig.weekMode}}">
+        <text class="prev-handle iconfont icon-doubleleft" bindtap="chooseDate" data-type="prev_year"></text>
+        <text class="prev-handle iconfont icon-left" bindtap="chooseDate" data-type="prev_month"></text>
+      </view>
+      <view class="flex date-in-handle b lr cc" bindtap="doubleClickToToday">
+        {{calendar.curYear || "--"}} 年 {{calendar.curMonth || "--"}} 月
+      </view>
+      <view class="next fs36" wx:if="{{calendarConfig.showHandlerOnWeekMode || !calendarConfig.weekMode}}">
+        <text class="next-handle iconfont icon-right" bindtap="chooseDate" data-type="next_month"></text>
+        <text class="next-handle iconfont icon-doubleright" bindtap="chooseDate" data-type="next_year"></text>
+      </view>
+    </view>
+
+    <!-- 星期栏 -->
+    <view class="weeks b lr ac {{calendarConfig.theme}}_week-color">
+      <view class="week fs28" wx:for="{{calendar.weeksCh}}" wx:key="index" data-idx="{{index}}">
+        {{item}}
+      </view>
+    </view>
+
+    <!-- 日历面板主体 -->
+    <view class="b lr wrap"
+      bindtouchstart="calendarTouchstart"
+      catchtouchmove="calendarTouchmove"
+      catchtouchend="calendarTouchend">
+
+      <!-- 上月日期格子 -->
+      <view
+        class="grid other-month b ac pc"
+        wx:for="{{calendar.empytGrids}}"
+        wx:key="index"
+        data-idx="{{index}}">
+        <view class="date-wrap b cc">
+          <view class="date">
+            {{item.day}}
+          </view>
+        </view>
+      </view>
+
+      <!-- 本月日期格子 -->
+      <view
+        wx:for="{{days}}"
+        wx:key="index"
+        data-idx="{{index}}"
+        data-date="{{item}}"
+        bindtap="tapDayItem"
+        class="grid current-month{{item.lunar.isToday ? ' today' : ''}}{{item.choosed ? ' dim choosed' : ''}} b ac pc">
+        <view class="date-wrap b cc {{(item.week === 0 || item.week === 6) ? calendarConfig.theme + '_weekend-color' : ''}}">
+          <view class="date b ac pc">
+            {{calendarConfig.markToday && item.lunar.isToday ? calendarConfig.markToday : item.day}}
+          </view>
+           <!-- 小圆点,根据状态显示不同颜色 -->
+            <view class="dot dot-{{item.type}}"></view>
+        </view>
+      </view>
+
+      <!-- 下月日期格子 -->
+      <view
+        class="grid other-month b ac pc"
+        wx:for="{{calendar.lastEmptyGrids}}"
+        wx:key="index"
+        data-idx="{{index}}">
+        <view class="date-wrap b cc">
+          <view class="date">
+            {{item.day}}  
+          </view>
+        </view>
+      </view>
+      
+
+<view style="width:100%;margin-top: 36rpx;">
+ <t-divider />
+  <view class="calendar-footer" wx:if="{{!isFutureDate}}">
+  <view class="status-box" wx:for="{{noClockIn}}" wx:if="{{!isFutureDate}}">
+    <view class="status-row" >
+      <text class="status-label unpunch">未打卡:</text>
+      <text class="status-content unpunch">{{item.conditioningProgramName}}</text>
+    </view>
+     <button class="patch-btn" bindtap="onPatchCard" data-id="{{item.id}}" >补卡</button>
+    </view>
+
+    <view class="status-row" wx:for="{{clockIn}}" wx:if="{{!isFutureDate}}">
+      <text class="status-label punch">已打卡:</text>
+      <text class="status-content punch">{{item.conditioningProgramName}}</text>
+    </view>
+  </view>
+</view>
+
+
+
+
+    </view>
+  </view>
+</view>

+ 878 - 0
miniprogram/components/calendar/main.js

@@ -0,0 +1,878 @@
+import Day from './func/day'
+import Week from './func/week'
+import Todo from './func/todo'
+import WxData from './func/wxData'
+import Calendar from './func/render'
+import CalendarConfig from './func/config'
+import convertSolarLunar from './func/convertSolarLunar'
+import {
+  Logger,
+  GetDate,
+  isComponent,
+  initialTasks,
+  getCurrentPage,
+  getComponent,
+  getDateTimeStamp
+} from './func/utils'
+
+let Component = {}
+let logger = new Logger()
+let getDate = new GetDate()
+let dataInstance = null
+
+/**
+ * 全局赋值正在操作的组件实例,方便读/写各自的 data
+ * @param {string} componentId 要操作的日历组件ID
+ */
+function bindCurrentComponent(componentId) {
+  if (componentId) {
+    Component = getComponent(componentId)
+  }
+  return Component
+}
+/**
+ * 获取日历内部数据
+ * @param {string} key 获取值的键名
+ * @param {string} componentId 要操作的日历组件ID
+ */
+function getData(key, componentId) {
+  bindCurrentComponent(componentId)
+  dataInstance = new WxData(Component)
+  return dataInstance.getData(key)
+}
+/**
+ * 设置日历内部数据
+ * @param {object}} data 待设置的数据
+ * @param {function} callback 设置成功回调函数
+ */
+function setData(data, callback = () => {}) {
+  const dataInstance = new WxData(Component)
+  return dataInstance.setData(data, callback)
+}
+
+const conf = {
+  /**
+   * 渲染日历
+   * @param {number} curYear
+   * @param {number} curMonth
+   * @param {number} curDate
+   */
+  renderCalendar(curYear, curMonth, curDate) {
+    if (isComponent(this)) Component = this
+    return new Promise((resolve, reject) => {
+      Calendar(Component)
+        .renderCalendar(curYear, curMonth, curDate)
+        .then((info = {}) => {
+          if (!info.firstRender) {
+            return resolve({
+              year: curYear,
+              month: curMonth,
+              date: curDate
+            })
+          }
+          mountEventsOnPage(getCurrentPage())
+          Component.triggerEvent('afterCalendarRender', Component)
+          Component.firstRender = true
+          initialTasks.flag = 'finished'
+          if (initialTasks.tasks.length) {
+            initialTasks.tasks.shift()()
+          }
+          resolve({
+            year: curYear,
+            month: curMonth,
+            date: curDate
+          })
+        })
+        .catch(err => {
+          reject(err)
+        })
+    })
+  },
+  /**
+   * 当改变月份时触发
+   * @param {object} param
+   */
+  whenChangeDate({ curYear, curMonth, newYear, newMonth }) {
+    Component.triggerEvent('whenChangeMonth', {
+      current: {
+        year: curYear,
+        month: curMonth
+      },
+      next: {
+        year: newYear,
+        month: newMonth
+      }
+    })
+  },
+  /**
+   * 多选
+   * @param {number} dateIdx 当前选中日期索引值
+   */
+  whenMulitSelect(dateIdx) {
+    if (isComponent(this)) Component = this
+    const { calendar = {} } = getData()
+    const { days, todoLabels } = calendar
+    const config = CalendarConfig(Component).getCalendarConfig()
+    let { selectedDay: selectedDays = [] } = calendar
+    const currentDay = days[dateIdx]
+    if (!currentDay) return
+    currentDay.choosed = !currentDay.choosed
+    if (!currentDay.choosed) {
+      currentDay.cancel = true // 该次点击是否为取消日期操作
+      const currentDayStr = getDate.toTimeStr(currentDay)
+      selectedDays = selectedDays.filter(
+        item => currentDayStr !== getDate.toTimeStr(item)
+      )
+      if (todoLabels) {
+        todoLabels.forEach(item => {
+          if (currentDayStr === getDate.toTimeStr(item)) {
+            currentDay.showTodoLabel = true
+          }
+        })
+      }
+    } else {
+      currentDay.cancel = false
+      const { showLabelAlways } = getData('calendar')
+      if (showLabelAlways && currentDay.showTodoLabel) {
+        currentDay.showTodoLabel = true
+      } else {
+        currentDay.showTodoLabel = false
+      }
+      if (!config.takeoverTap) {
+        selectedDays.push(currentDay)
+      }
+    }
+    if (config.takeoverTap) {
+      return Component.triggerEvent('onTapDay', currentDay)
+    }
+    setData({
+      'calendar.days': days,
+      'calendar.selectedDay': selectedDays
+    })
+    conf.afterTapDay(currentDay, selectedDays)
+  },
+  /**
+   * 单选
+   * @param {number} dateIdx 当前选中日期索引值
+   */
+  whenSingleSelect(dateIdx) {
+    if (isComponent(this)) Component = this
+    const { calendar = {} } = getData()
+    const { days, selectedDay: selectedDays = [], todoLabels } = calendar
+    let shouldMarkerTodoDay = []
+    const currentDay = days[dateIdx]
+    if (!currentDay) return
+    const preSelectedDate = [...selectedDays].pop() || {}
+    const { month: dMonth, year: dYear } = days[0] || {}
+    const config = CalendarConfig(Component).getCalendarConfig()
+    if (config.takeoverTap) {
+      return Component.triggerEvent('onTapDay', currentDay)
+    }
+    conf.afterTapDay(currentDay)
+    if (!config.inverse && preSelectedDate.day === currentDay.day) return
+    days.forEach((item, idx) => {
+      if (+item.day === +preSelectedDate.day) days[idx].choosed = false
+    })
+    if (todoLabels) {
+      // 筛选当月待办事项的日期
+      shouldMarkerTodoDay = todoLabels.filter(
+        item => +item.year === dYear && +item.month === dMonth
+      )
+    }
+    Todo(Component).showTodoLabels(shouldMarkerTodoDay, days, selectedDays)
+    const tmp = {
+      'calendar.days': days
+    }
+    if (preSelectedDate.day !== currentDay.day) {
+      preSelectedDate.choosed = false
+      currentDay.choosed = true
+      if (!calendar.showLabelAlways || !currentDay.showTodoLabel) {
+        currentDay.showTodoLabel = false
+      }
+      tmp['calendar.selectedDay'] = [currentDay]
+    } else if (config.inverse) {
+      if (currentDay.choosed) {
+        if (currentDay.showTodoLabel && calendar.showLabelAlways) {
+          currentDay.showTodoLabel = true
+        } else {
+          currentDay.showTodoLabel = false
+        }
+      }
+      tmp['calendar.selectedDay'] = []
+    }
+    if (config.weekMode) {
+      tmp['calendar.curYear'] = currentDay.year
+      tmp['calendar.curMonth'] = currentDay.month
+    }
+    setData(tmp)
+  },
+  gotoSetContinuousDates(start, end) {
+    return chooseDateArea([
+      `${getDate.toTimeStr(start)}`,
+      `${getDate.toTimeStr(end)}`
+    ])
+  },
+  timeRangeHelper(currentDate, selectedDay) {
+    const currentDateTimestamp = getDateTimeStamp(currentDate)
+    const startDate = selectedDay[0]
+    let endDate
+    let endDateTimestamp
+    let selectedLen = selectedDay.length
+    if (selectedLen > 1) {
+      endDate = selectedDay[selectedLen - 1]
+      endDateTimestamp = getDateTimeStamp(endDate)
+    }
+    const startTimestamp = getDateTimeStamp(startDate)
+    return {
+      endDate,
+      startDate,
+      currentDateTimestamp,
+      endDateTimestamp,
+      startTimestamp
+    }
+  },
+  /**
+   * 计算连续日期选择的开始及结束日期
+   * @param {object} currentDate 当前选择日期
+   * @param {array} selectedDay 已选择的的日期
+   */
+  calculateDateRange(currentDate, selectedDay) {
+    const {
+      endDate,
+      startDate,
+      currentDateTimestamp,
+      endDateTimestamp,
+      startTimestamp
+    } = this.timeRangeHelper(currentDate, selectedDay)
+    let range = []
+    let selectedLen = selectedDay.length
+    const isWantToChooseOneDate = selectedDay.filter(
+      item => getDate.toTimeStr(item) === getDate.toTimeStr(currentDate)
+    )
+    if (selectedLen === 2 && isWantToChooseOneDate.length) {
+      range = [currentDate, currentDate]
+      return range
+    }
+    if (
+      currentDateTimestamp >= startTimestamp &&
+      endDateTimestamp &&
+      currentDateTimestamp <= endDateTimestamp
+    ) {
+      const currentDateIdxInChoosedDateArea = selectedDay.findIndex(
+        item => getDate.toTimeStr(item) === getDate.toTimeStr(currentDate)
+      )
+      if (selectedLen / 2 > currentDateIdxInChoosedDateArea) {
+        range = [currentDate, endDate]
+      } else {
+        range = [startDate, currentDate]
+      }
+    } else if (currentDateTimestamp < startTimestamp) {
+      range = [currentDate, endDate]
+    } else if (currentDateTimestamp > startTimestamp) {
+      range = [startDate, currentDate]
+    }
+    return range
+  },
+  chooseAreaWhenExistArea(currentDate, selectedDay) {
+    return new Promise((resolve, reject) => {
+      const range = conf.calculateDateRange(
+        currentDate,
+        getDate.sortDates(selectedDay)
+      )
+      conf
+        .gotoSetContinuousDates(...range)
+        .then(data => {
+          resolve(data)
+          conf.afterTapDay(currentDate)
+        })
+        .catch(err => {
+          reject(err)
+          conf.afterTapDay(currentDate)
+        })
+    })
+  },
+  chooseAreaWhenHasOneDate(currentDate, selectedDay, lastChoosedDate) {
+    return new Promise((resolve, reject) => {
+      const startDate = lastChoosedDate || selectedDay[0]
+      let range = [startDate, currentDate]
+      const currentDateTimestamp = getDateTimeStamp(currentDate)
+      const lastChoosedDateTimestamp = getDateTimeStamp(startDate)
+      if (lastChoosedDateTimestamp > currentDateTimestamp) {
+        range = [currentDate, startDate]
+      }
+      conf
+        .gotoSetContinuousDates(...range)
+        .then(data => {
+          resolve(data)
+          conf.afterTapDay(currentDate)
+        })
+        .catch(err => {
+          reject(err)
+          conf.afterTapDay(currentDate)
+        })
+    })
+  },
+  /**
+   * 日期范围选择模式
+   * @param {number} dateIdx 当前选中日期索引值
+   */
+  whenChooseArea(dateIdx) {
+    return new Promise((resolve, reject) => {
+      if (isComponent(this)) Component = this
+      if (Component.weekMode) return
+      const { days = [], selectedDay, lastChoosedDate } = getData('calendar')
+      const currentDate = days[dateIdx]
+      if (currentDate.disable) return
+      const config = CalendarConfig(Component).getCalendarConfig()
+      if (config.takeoverTap) {
+        return Component.triggerEvent('onTapDay', currentDate)
+      }
+      if (selectedDay && selectedDay.length > 1) {
+        conf
+          .chooseAreaWhenExistArea(currentDate, selectedDay)
+          .then(dates => {
+            resolve(dates)
+          })
+          .catch(err => {
+            reject(err)
+          })
+      } else if (lastChoosedDate || (selectedDay && selectedDay.length === 1)) {
+        conf
+          .chooseAreaWhenHasOneDate(currentDate, selectedDay, lastChoosedDate)
+          .then(dates => {
+            resolve(dates)
+          })
+          .catch(err => {
+            reject(err)
+          })
+      } else {
+        days.forEach(date => {
+          if (+date.day === +currentDate.day) {
+            date.choosed = true
+          } else {
+            date.choosed = false
+          }
+        })
+
+        const dataInstance = new WxData(Component)
+        dataInstance.setData({
+          'calendar.days': [...days],
+          'calendar.lastChoosedDate': currentDate
+        })
+      }
+    })
+  },
+  /**
+   * 点击日期后触发事件
+   * @param {object} currentSelected 当前选择的日期
+   * @param {array} selectedDates  多选状态下选中的日期
+   */
+  afterTapDay(currentSelected, selectedDates) {
+    const config = CalendarConfig(Component).getCalendarConfig()
+    const { multi } = config
+    if (!multi) {
+      Component.triggerEvent('afterTapDay', currentSelected)
+    } else {
+      Component.triggerEvent('afterTapDay', {
+        currentSelected,
+        selectedDates
+      })
+    }
+  },
+  /**
+   * 跳转至今天
+   */
+  jumpToToday() {
+    return new Promise((resolve, reject) => {
+      const { year, month, date } = getDate.todayDate()
+      const timestamp = getDate.todayTimestamp()
+      const config = CalendarConfig(Component).getCalendarConfig()
+      setData({
+        'calendar.curYear': year,
+        'calendar.curMonth': month,
+        'calendar.selectedDay': [
+          {
+            year: year,
+            day: date,
+            month: month,
+            choosed: true,
+            lunar: config.showLunar
+              ? convertSolarLunar.solar2lunar(year, month, date)
+              : null
+          }
+        ],
+        'calendar.todayTimestamp': timestamp
+      })
+      conf
+        .renderCalendar(year, month, date)
+        .then(() => {
+          resolve({ year, month, date })
+        })
+        .catch(() => {
+          reject('jump failed')
+        })
+    })
+  }
+}
+
+export const whenChangeDate = conf.whenChangeDate
+export const renderCalendar = conf.renderCalendar
+export const whenSingleSelect = conf.whenSingleSelect
+export const whenChooseArea = conf.whenChooseArea
+export const whenMulitSelect = conf.whenMulitSelect
+export const calculatePrevWeekDays = conf.calculatePrevWeekDays
+export const calculateNextWeekDays = conf.calculateNextWeekDays
+
+/**
+ * 获取当前年月
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function getCurrentYM(componentId) {
+  bindCurrentComponent(componentId)
+  return {
+    year: getData('calendar.curYear'),
+    month: getData('calendar.curMonth')
+  }
+}
+
+/**
+ * 获取已选择的日期
+ * @param {object } options 日期配置选项 {lunar} 是否返回农历信息
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function getSelectedDay(options = {}, componentId) {
+  bindCurrentComponent(componentId)
+  const config = getCalendarConfig()
+  const dates = getData('calendar.selectedDay') || []
+  if (options.lunar && !config.showLunar) {
+    const datesWithLunar = getDate.convertLunar(dates)
+    return datesWithLunar
+  } else {
+    return dates
+  }
+}
+
+/**
+ * 取消选中日期
+ * @param {array} dates 需要取消的日期,不传则取消所有已选择的日期
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function cancelSelectedDates(dates, componentId) {
+  bindCurrentComponent(componentId)
+  const { days = [], selectedDay = [] } = getData('calendar') || {}
+  if (!dates || !dates.length) {
+    days.forEach(item => {
+      item.choosed = false
+    })
+    setData({
+      'calendar.days': days,
+      'calendar.selectedDay': []
+    })
+  } else {
+    const cancelDatesStr = dates.map(
+      date => `${+date.year}-${+date.month}-${+date.day}`
+    )
+    const filterSelectedDates = selectedDay.filter(
+      date =>
+        !cancelDatesStr.includes(`${+date.year}-${+date.month}-${+date.day}`)
+    )
+    days.forEach(date => {
+      if (
+        cancelDatesStr.includes(`${+date.year}-${+date.month}-${+date.day}`)
+      ) {
+        date.choosed = false
+      }
+    })
+    setData({
+      'calendar.days': days,
+      'calendar.selectedDay': filterSelectedDates
+    })
+  }
+}
+/**
+ * 周视图跳转
+ * @param {object} date info
+ * @param {boolean} disableSelected 跳转时是否需要选中,周视图切换调用该方法,如未选择日期时不选中日期
+ */
+function jumpWhenWeekMode({ year, month, day }, disableSelected) {
+  return new Promise((resolve, reject) => {
+    Week(Component)
+      .jump(
+        {
+          year: +year,
+          month: +month,
+          day: +day
+        },
+        disableSelected
+      )
+      .then(date => {
+        resolve(date)
+        Component.triggerEvent('afterCalendarRender', Component)
+      })
+      .catch(err => {
+        reject(err)
+        Component.triggerEvent('afterCalendarRender', Component)
+      })
+  })
+}
+
+/**
+ * 月视图跳转
+ * @param {object} date info
+ */
+function jumpWhenNormalMode({ year, month, day }) {
+  return new Promise((resolve, reject) => {
+    if (typeof +year !== 'number' || typeof +month !== 'number') {
+      return logger.warn('jump 函数年月日参数必须为数字')
+    }
+    const timestamp = getDate.todayTimestamp()
+    let tmp = {
+      'calendar.curYear': +year,
+      'calendar.curMonth': +month,
+      'calendar.todayTimestamp': timestamp
+    }
+    setData(tmp, () => {
+      conf
+        .renderCalendar(+year, +month, +day)
+        .then(date => {
+          resolve(date)
+        })
+        .catch(err => {
+          reject(err)
+        })
+    })
+  })
+}
+
+/**
+ * 跳转至指定日期
+ * @param {number} year
+ * @param {number} month
+ * @param {number} day
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function jump(year, month, day, componentId) {
+  return new Promise((resolve, reject) => {
+    bindCurrentComponent(componentId)
+    const { selectedDay = [] } = getData('calendar') || {}
+    const { weekMode } = getData('calendarConfig') || {}
+    const { year: y, month: m, day: d } = selectedDay[0] || {}
+    if (+y === +year && +m === +month && +d === +day) {
+      return
+    }
+    if (weekMode) {
+      let disableSelected = false
+      if (!year || !month || !day) {
+        const today = getDate.todayDate()
+        year = today.year
+        month = today.month
+        day = today.date
+        disableSelected = true
+      }
+      jumpWhenWeekMode({ year, month, day }, disableSelected)
+        .then(date => {
+          resolve(date)
+        })
+        .catch(err => {
+          reject(err)
+        })
+      mountEventsOnPage(getCurrentPage())
+      return
+    }
+    if (year && month) {
+      jumpWhenNormalMode({ year, month, day })
+        .then(date => {
+          resolve(date)
+        })
+        .catch(err => {
+          reject(err)
+        })
+    } else {
+      conf
+        .jumpToToday()
+        .then(date => {
+          resolve(date)
+        })
+        .catch(err => {
+          reject(err)
+        })
+    }
+  })
+}
+
+/**
+ * 设置待办事项日期标记
+ * @param {object} todos  待办事项配置
+ * @param {string} [todos.pos] 标记显示位置,默认值'bottom' ['bottom', 'top']
+ * @param {string} [todos.dotColor] 标记点颜色,backgroundColor 支持的值都行
+ * @param {object[]} [todos.days] 需要标记的所有日期,如:[{year: 2015, month: 5, day: 12}],其中年月日字段必填
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function setTodoLabels(todos, componentId) {
+  bindCurrentComponent(componentId)
+  Todo(Component).setTodoLabels(todos)
+}
+
+/**
+ * 删除指定日期待办事项
+ * @param {array} todos 需要删除的待办日期数组
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function deleteTodoLabels(todos, componentId) {
+  bindCurrentComponent(componentId)
+  Todo(Component).deleteTodoLabels(todos)
+}
+
+/**
+ * 清空所有待办事项
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function clearTodoLabels(componentId) {
+  bindCurrentComponent(componentId)
+  Todo(Component).clearTodoLabels()
+}
+
+/**
+ * 获取所有待办事项
+ * @param {object } options 日期配置选项 {lunar} 是否返回农历信息
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function getTodoLabels(options = {}, componentId) {
+  bindCurrentComponent(componentId)
+  const config = getCalendarConfig()
+  const todoDates = Todo(Component).getTodoLabels() || []
+  if (options.lunar && !config.showLunar) {
+    const todoDatesWithLunar = getDate.convertLunar(todoDates)
+    return todoDatesWithLunar
+  } else {
+    return todoDates
+  }
+}
+
+/**
+ * 禁用指定日期
+ * @param {array} days 日期
+ * @param {number} [days.year]
+ * @param {number} [days.month]
+ * @param {number} [days.day]
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function disableDay(days = [], componentId) {
+  bindCurrentComponent(componentId)
+  Day(Component).disableDays(days)
+}
+
+/**
+ * 指定可选日期范围
+ * @param {array} area 日期访问数组
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function enableArea(area = [], componentId) {
+  bindCurrentComponent(componentId)
+  Day(Component).enableArea(area)
+}
+
+/**
+ * 指定特定日期可选
+ * @param {array} days 指定日期数组
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function enableDays(days = [], componentId) {
+  bindCurrentComponent(componentId)
+  Day(Component).enableDays(days)
+}
+
+/**
+ * 设置选中日期(多选模式下)
+ * @param {array} selected 需选中日期
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function setSelectedDays(selected, componentId) {
+  bindCurrentComponent(componentId)
+  Day(Component).setSelectedDays(selected)
+}
+
+/**
+ * 获取当前日历配置
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function getCalendarConfig(componentId) {
+  bindCurrentComponent(componentId)
+  return CalendarConfig(Component).getCalendarConfig()
+}
+
+/**
+ * 设置日历配置
+ * @param {object} config
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function setCalendarConfig(config, componentId) {
+  bindCurrentComponent(componentId)
+  if (!config || Object.keys(config).length === 0) {
+    return logger.warn('setCalendarConfig 参数必须为非空对象')
+  }
+  const existConfig = getCalendarConfig()
+  return new Promise((resolve, reject) => {
+    CalendarConfig(Component)
+      .setCalendarConfig(config)
+      .then(conf => {
+        resolve(conf)
+        const { date, type } = existConfig.disableMode || {}
+        const { _date, _type } = config.disableMode || {}
+        if (type !== _type || date !== _date) {
+          const { year, month } = getCurrentYM()
+          jump(year, month)
+        }
+      })
+      .catch(err => {
+        reject(err)
+      })
+  })
+}
+
+/**
+ * 获取当前日历面板日期
+ * @param {object } options 日期配置选项 {lunar} 是否返回农历信息
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function getCalendarDates(options = {}, componentId) {
+  bindCurrentComponent(componentId)
+  const config = getCalendarConfig()
+  const dates = getData('calendar.days', componentId) || []
+  if (options.lunar && !config.showLunar) {
+    const datesWithLunar = getDate.convertLunar(dates)
+    return datesWithLunar
+  } else {
+    return dates
+  }
+  
+}
+
+/**
+ * 选择连续日期范围
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function chooseDateArea(dateArea, componentId) {
+  bindCurrentComponent(componentId)
+  return Day(Component).chooseArea(dateArea)
+}
+
+/**
+ * 设置指定日期样式
+ * @param {array} dates 待设置特殊样式的日期
+ * @param {string} componentId 要操作的日历组件ID
+ */
+export function setDateStyle(dates, componentId) {
+  if (!dates) return
+  bindCurrentComponent(componentId)
+  Day(Component).setDateStyle(dates)
+}
+
+/**
+ * 切换周月视图
+ * 切换视图时可传入指定日期,如: {year: 2019, month: 1, day: 3}
+ * args[0] view 视图模式[week, month]
+ * args[1]|args[2]为day object或者 componentId
+ */
+export function switchView(...args) {
+  return new Promise((resolve, reject) => {
+    const view = args[0]
+    if (!args[1]) {
+      return Week(Component)
+        .switchWeek(view)
+        .then(resolve)
+        .catch(reject)
+    }
+    if (typeof args[1] === 'string') {
+      bindCurrentComponent(args[1], this)
+      Week(Component)
+        .switchWeek(view, args[2])
+        .then(resolve)
+        .catch(reject)
+    } else if (typeof args[1] === 'object') {
+      if (typeof args[2] === 'string') {
+        bindCurrentComponent(args[1], this)
+      }
+      Week(Component)
+        .switchWeek(view, args[1])
+        .then(resolve)
+        .catch(reject)
+    }
+  })
+}
+
+/**
+ * 绑定日历事件至当前页面实例
+ * @param {object} page 当前页面实例
+ */
+function mountEventsOnPage(page) {
+  page.calendar = {
+    jump,
+    switchView,
+    disableDay,
+    enableArea,
+    enableDays,
+    chooseDateArea,
+    getCurrentYM,
+    getSelectedDay,
+    cancelSelectedDates,
+    setDateStyle,
+    setTodoLabels,
+    getTodoLabels,
+    deleteTodoLabels,
+    clearTodoLabels,
+    setSelectedDays,
+    getCalendarConfig,
+    setCalendarConfig,
+    getCalendarDates
+  }
+}
+
+function setWeekHeader(firstDayOfWeek) {
+  let weeksCh = ['日', '一', '二', '三', '四', '五', '六']
+  if (firstDayOfWeek === 'Mon') {
+    weeksCh = ['一', '二', '三', '四', '五', '六', '日']
+  }
+  setData({
+    'calendar.weeksCh': weeksCh
+  })
+}
+
+function autoSelectDay(defaultDay) {
+  Component.firstRenderWeekMode = true
+  if (defaultDay && typeof defaultDay === 'string') {
+    const day = defaultDay.split('-')
+    if (day.length < 3) {
+      return logger.warn('配置 jumpTo 格式应为: 2018-4-2 或 2018-04-02')
+    }
+    jump(+day[0], +day[1], +day[2])
+  } else {
+    if (!defaultDay) {
+      Component.config.noDefault = true
+      setData({
+        'config.noDefault': true
+      })
+    }
+    jump()
+  }
+}
+
+function init(component, config) {
+  initialTasks.flag = 'process'
+  Component = component
+  Component.config = config
+  setWeekHeader(config.firstDayOfWeek)
+  autoSelectDay(config.defaultDay)
+  logger.tips(
+    '使用中若遇问题请反馈至 https://github.com/treadpit/wx_calendar/issues ✍️'
+  )
+}
+
+export default (component, config = {}) => {
+  if (initialTasks.flag === 'process') {
+    return initialTasks.tasks.push(function() {
+      init(component, config)
+    })
+  }
+  init(component, config)
+}

Разница между файлами не показана из-за своего большого размера
+ 2 - 0
miniprogram/components/calendar/theme/iconfont.wxss


+ 52 - 0
miniprogram/components/calendar/theme/theme-default.wxss

@@ -0,0 +1,52 @@
+
+/* 日历主要颜色相关样式 */
+
+.default_color,
+.default_weekend-color,
+.default_handle-color,
+.default_week-color {
+    color: #ff629a;
+}
+
+.default_today {
+    color: #fff;
+    background-color: #874fb4;
+}
+
+.default_choosed {
+    color: #fff;
+    background-color: #ff629a;
+}
+
+.default_date-disable {
+    color: #c7c7c7;
+}
+
+.default_prev-month-date,
+.default_next-month-date {
+    color: #e2e2e2;
+}
+
+.default_normal-date {
+    color: #88d2ac;
+}
+
+.default_todo-circle {
+    border-color: #88d2ac;
+}
+
+.default_todo-dot {
+    background-color: #e54d42;
+}
+
+.default_date-desc {
+    color: #c2c2c2;
+}
+
+.default_date-desc-lunar {
+    color: #e54d42;
+}
+
+.default_date-desc-disable {
+    color: #e2e2e2;
+}

+ 49 - 0
miniprogram/components/calendar/theme/theme-elegant.wxss

@@ -0,0 +1,49 @@
+.elegant_color,
+.elegant_weekend-color,
+.elegant_handle-color,
+.elegant_week-color {
+    color: #333;
+}
+
+.elegant_today {
+    color: #000;
+    background-color: #e1e7f5;
+}
+
+.elegant_choosed {
+    color: #000;
+    background-color: #e2e2e2;
+}
+
+.elegant_date-disable {
+    color: #c7c7c7;
+}
+
+.elegant_prev-month-date,
+.elegant_next-month-date {
+    color: #e2e2e2;
+}
+
+.elegant_normal-date {
+    color: #333;
+}
+
+.elegant_todo-circle {
+    border-color: #161035;
+}
+
+.elegant_todo-dot {
+    background-color: #161035;
+}
+
+.elegant_date-desc {
+    color: #c2c2c2;
+}
+
+.elegant_date-desc-lunar {
+    color: #161035;
+}
+
+.elegant_date-desc-disable {
+    color: #e2e2e2;
+}

+ 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"
+  }
+}

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

@@ -0,0 +1,95 @@
+/* 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);
+        color: #1D6FF6;
+      }
+    }
+  }
+
+  &__content {
+    padding: 0 12px;
+    box-sizing: border-box;
+  }
+}
+
+
+.card {
+  position: relative;
+  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);
+  background: white;
+  border: 1px solid #EEEEEE;
+  color: #191919;
+  &--active {
+    border: 1px solid #1D6FF6;
+    &::after {
+      content: '';
+      display: block;
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 0;
+      height: 0;
+      border-width: 28px 28px 28px 0;
+      border-style: solid;
+      border: 14px solid #1D6FF6;
+      border-bottom-color: transparent;
+      border-right-color: transparent;
+    }
+  }
+
+  &--disabled {
+    opacity: 0.5;
+  }
+
+  &__icon {
+    color: white;
+    position: absolute;
+    left: 1.5px;
+    top: 1.5px;
+    z-index: 1;
+  }
+}
+
+.input-wrapper {
+  box-sizing: border-box;
+
+  input {
+    padding: 0 8px;
+    height: 100%;
+    text-align: left;
+    border: 1px solid #2A2A2A;
+    border-radius: 4px;
+  }
+}

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

@@ -0,0 +1,171 @@
+// 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() {
+
+    }
+  },
+  /**
+   * 组件的属性列表
+   */
+  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[],
+    replenish: {} as AnyObject,
+    showReplenish: false,
+    replenishValue: '',
+
+    offset: 0,
+  },
+  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;
+
+      const handle = () => {
+        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] });
+        }
+      }
+
+      if (option.label === '其他') {
+        if (this.data.selected.includes(option.value)) {
+          handle();
+          this.setData({ [`replenish.${option.value}`]: '' })
+        } else {
+          this.setData({ showReplenish: true, replenishValue: '' });
+          this.onSubConfirm = () => {
+            if (this.data.replenishValue) {
+              handle();
+              this.setData({ [`replenish.${option.value}`]: this.data.replenishValue })
+            }
+            this.onSubCancel();
+          }
+        }
+      } else {
+        handle()
+      }
+    },
+    onConfirm() {
+      const get = (option: Option) => {
+        if (!option) return null;
+        const replenish = this.data.replenish[option.value];
+        return {
+          label: replenish ? replenish : option.label,
+          value: replenish ? `${option.value}:${replenish}` : option.value
+        }
+      };
+      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' });
+    },
+    onSubConfirm() { },
+    onSubCancel() {
+      this.setData({ showReplenish: false, offset: 0 });
+    },
+    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),
+            offset: 0
+          });
+        }, 100)
+      };
+    },
+    onBlur() {
+      this.setData({ offset: 0 })
+    },
+
+    onKeyboardheightchange(event: any) {
+      const _height = event?.detail?.height;
+      if (_height !== this.data.offset) this.setData({ offset: _height });
+    }
+  }
+})

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

@@ -0,0 +1,34 @@
+<!--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">
+      <block wx:if="{{showReplenish}}">
+        <view class="btn btn--cancel" aria-role="button" bind:tap="onSubCancel">返回</view>
+        <view class="title">{{title}}: 其他</view>
+        <view class="btn btn--confirm" aria-role="button" bind:tap="onSubConfirm">完成</view>
+      </block>
+      <block wx:else>
+        <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>
+      </block>
+    </view>
+    <view class="form-picker__content">
+      <view class="input-wrapper" wx:if="{{showReplenish}}" style="height: {{itemHeight}}px; padding: {{(itemHeight - 40) / 2}}px;">
+        <input placeholder="请输入" focus="{{true}}" model:value="{{replenishValue}}" adjust-position="{{false}}" bind:keyboardheightchange="onKeyboardheightchange" bind:blur="onBlur" bind:confirm="onSubConfirm" />
+      </view>
+      <block wx:else>
+        <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">{{replenish[item.value] || item.label}}</text>
+            </view>
+          </grid-builder>
+        </scroll-view>
+      </block>
+    </view>
+    <view style="height: {{offset}}px;"></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": {}
+}

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

@@ -0,0 +1,112 @@
+/* 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;
+  --active-color:#1D6FF6;
+  --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;
+    }
+  }
+}

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

@@ -0,0 +1,107 @@
+// 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: String, value: '' },
+    min: { type: Number, value: 0 },
+    max: { type: Number, value: 1 },
+    precision: { type: Number, value: 0.1 },
+    defaultValue: { type: Number, value: Number.NaN },
+    line: { type: Array, value: [] },
+  },
+  observers: {
+    'precision'(precision: number) {
+      const scale = Math.pow(10, Math.ceil(Math.log10(1 / precision)));
+      this.setData({ scale });
+    },
+    'min, max, precision'(...args: [number, number, number]) {
+      this._updateAxis(args);
+    },
+    'defaultValue, min, max, precision, line'(value, min, max, precision, line) {
+      const offset = precision >= 1 ? precision : 0
+      if (!value || Number.isNaN(+value)) {
+        if (Array.isArray(line)) {
+          min = line[0]?.value ?? min;
+          max = line[1]?.value ?? max;
+        }
+        value = Math.floor((max - min - offset) / 2 + min)
+      }
+      this._scrollValue(value);
+    }
+  },
+  lifetimes: {
+    attached() {
+      this._updateRect();
+    },
+  },
+  data: {
+    prefix,
+    initialValue: '', scale: 0,
+    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: value.toFixed(Math.log10(this.data.scale)) })
+    },
+    _scrollValue(value: number | string) {
+      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>

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

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

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

@@ -0,0 +1,22 @@
+// components/form/form.ts
+Component({
+  properties: {
+    _loading: { type: Boolean, value: false },
+    delay: { type: Number, value: 0 }
+  },
+  data: {
+    _model: {}
+  },
+  methods: {
+    onChange(event) {
+      const model = { ...this.data._model, ...event.detail };
+      this.setData({ _model: model });
+    },
+    onSubmit(event) {
+      setTimeout(() => {
+        if (this.data._loading) return;
+        this.triggerEvent('submit', { target: event.target, value: this.data._model }, { bubbles: true, composed: true });
+      }, this.data.delay);
+    },
+  }
+})

+ 3 - 0
miniprogram/components/form/form.wxml

@@ -0,0 +1,3 @@
+<view bind:change="onChange" mut-bind:submit="onSubmit">
+  <slot></slot>
+</view>

+ 6 - 0
miniprogram/components/media-carousel/media-carousel.json

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

+ 103 - 0
miniprogram/components/media-carousel/media-carousel.scss

@@ -0,0 +1,103 @@
+/* components/media-carousel/media-carousel.wxss */
+.media-carousel {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 16rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+}
+
+/* 自适应高度模式 */
+.media-carousel.adaptive-height {
+  /* height: auto; */
+  min-height: 400rpx;
+}
+
+.carousel-swiper {
+  width: 100%;
+  height: 100%;
+}
+
+
+
+.carousel-item {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.adaptive-height .carousel-item {
+  height: auto;
+}
+
+/* 图片容器样式 */
+.image-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  background-color: #f5f5f5; /* 添加背景色,避免空白区域突兀 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.adaptive-height .image-container {
+  height: auto;
+  background-color: transparent;
+}
+
+.carousel-image {
+  width: 100%;
+  height: 100%;
+  display: block;
+  /* 确保图片在aspectFit模式下居中显示 */
+  object-position: center;
+}
+
+.adaptive-height .carousel-image {
+  height: auto;
+  max-height: none;
+}
+
+.image-title {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+  color: #fff;
+  padding: 20rpx;
+  font-size: 28rpx;
+  line-height: 1.4;
+  z-index: 2;
+}
+
+/* 视频容器样式 */
+.video-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+.carousel-video {
+  width: 100%;
+  height: 100%;
+  display: block;
+}
+
+.video-title {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+  color: #fff;
+  padding: 20rpx;
+  font-size: 28rpx;
+  line-height: 1.4;
+  z-index: 2;
+}
+

+ 120 - 0
miniprogram/components/media-carousel/media-carousel.ts

@@ -0,0 +1,120 @@
+// components/media-carousel/media-carousel.ts
+
+// 定义微信小程序组件的类型
+interface IComponentInstance {
+  setData: (data: any) => void;
+  triggerEvent: (name: string, detail: any) => void;
+  data: any;
+  properties: any;
+}
+
+interface IComponentOptions {
+  properties: Record<string, any>;
+  data: any;
+  methods: Record<string, Function>;
+}
+
+// 声明全局的 Component 函数
+declare function Component(options: IComponentOptions): void;
+
+Component({
+  /**
+   * 组件的属性列表
+   */
+  properties: {
+    // 媒体列表
+    mediaList: {
+      type: Array,
+      value: [],
+      observer: function(this: IComponentInstance, newVal: any[]) {
+        this.setData({
+          totalCount: newVal.length
+        });
+      }
+    },
+    // 项目ID,用于标识轮播图属于哪个项目
+    itemId: {
+      type: String,
+      value: ''
+    },
+    // 是否显示指示器
+    showIndicator: {
+      type: Boolean,
+      value: true
+    },
+    // 指示器颜色
+    indicatorColor: {
+      type: String,
+      value: 'rgba(255, 255, 255, 0.6)'
+    },
+    // 指示器激活颜色
+    indicatorActiveColor: {
+      type: String,
+      value: '#fff'
+    },
+    // 是否自动播放
+    autoplay: {
+      type: Boolean,
+      value: true
+    },
+    // 自动播放间隔时间
+    interval: {
+      type: Number,
+      value: 3000
+    },
+    // 滑动动画时长
+    duration: {
+      type: Number,
+      value: 500
+    },
+    // 是否循环播放
+    circular: {
+      type: Boolean,
+      value: true
+    }
+  },
+
+  /**
+   * 组件的初始数据
+   */
+  data: {
+    currentIndex: 0,
+    totalCount: 0
+  },
+
+  /**
+   * 组件的方法列表
+   */
+  methods: {
+    // 图片加载成功
+    onImageLoad(this: IComponentInstance, e: any) {
+      const { index } = e.currentTarget.dataset;
+      this.triggerEvent('imageload', {
+        index: index,
+        item: this.data.mediaList[index],
+        itemId: this.properties.itemId
+      });
+    },
+
+    // 图片加载失败
+    onImageError(this: IComponentInstance, e: any) {
+      const { index } = e.currentTarget.dataset;
+  
+      this.triggerEvent('imageerror', {
+        index: index,
+        item: this.data.mediaList[index],
+        itemId: this.properties.itemId
+      });
+    },
+
+    // 视频播放错误
+    onVideoError(this: IComponentInstance, e: any) {
+      const { index } = e.currentTarget.dataset;
+      this.triggerEvent('videoerror', {
+        index: index,
+        item: this.data.mediaList[index],
+        itemId: this.properties.itemId
+      });
+    }
+  }
+}); 

+ 39 - 0
miniprogram/components/media-carousel/media-carousel.wxml

@@ -0,0 +1,39 @@
+<!--components/media-carousel/media-carousel.wxml-->
+<view class="media-carousel">
+  <swiper 
+    class="carousel-swiper" 
+    indicator-dots="{{showIndicator}}" 
+    indicator-color="{{indicatorColor}}" 
+    indicator-active-color="{{indicatorActiveColor}}"
+    autoplay="{{autoplay}}" 
+    interval="{{interval}}" 
+    duration="{{duration}}"
+    circular="{{circular}}"
+  >
+    <swiper-item wx:for="{{mediaList}}" wx:key="index" class="carousel-item">
+      <!-- 图片轮播 -->
+      <view wx:if="{{item.type === 'image'}}" class="image-container">
+        <image
+          src="{{item.src}}" 
+          mode="aspectFit"
+          class="carousel-image"
+          bind:load="onImageLoad"
+          bind:error="onImageError"
+          data-index="{{index}}"
+        />
+        <view wx:if="{{item.title}}" class="image-title">{{item.title}}</view>
+      </view>
+      
+      <!-- 视频轮播 -->
+      <view wx:if="{{item.type === 'video'}}" class="video-container">
+        <video 
+          src="{{item.src}}" 
+          class="carousel-video"
+          bind:error="onVideoError"
+          data-index="{{index}}"
+        />
+        <view wx:if="{{item.title}}" class="video-title">{{item.title}}</view>
+      </view>
+    </swiper-item>
+  </swiper>
+</view> 

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

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

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

@@ -0,0 +1,36 @@
+/* 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);
+  color: #1D6FF6;
+}
+
+.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>

+ 54 - 0
miniprogram/components/record-index/record-index.js

@@ -0,0 +1,54 @@
+// components/record-index/record-index.js
+Component({
+  properties: {
+    id: {
+      type: String,
+      value: ''
+    }
+  },
+
+  data: {
+    loading: true,
+    error: false,
+    charts: [],
+    rect: {
+      width: 0,
+      height: 0
+    }
+  },
+
+  lifetimes: {
+    attached() {
+      this.init();
+    }
+  },
+
+  methods: {
+    async init() {
+      try {
+        const systemInfo = wx.getSystemInfoSync();
+        const rect = {
+          width: systemInfo.windowWidth,
+          height: 300
+        };
+        
+        this.setData({ rect });
+        await this.loadCharts();
+      } catch (error) {
+        console.error('Failed to initialize record-index:', error);
+        this.setData({ error: true, loading: false });
+      }
+    },
+
+    async loadCharts() {
+      try {
+        // TODO: Implement your chart data loading logic here
+        // This should be similar to what you have in the page version
+        this.setData({ loading: false });
+      } catch (error) {
+        console.error('Failed to load charts:', error);
+        this.setData({ error: true, loading: false });
+      }
+    }
+  }
+}); 

+ 6 - 0
miniprogram/components/record-index/record-index.json

@@ -0,0 +1,6 @@
+{
+  "component": true,
+  "usingComponents": {
+    "ec-canvas": "../../ec-canvas/ec-canvas"
+  }
+} 

+ 22 - 0
miniprogram/components/record-index/record-index.wxml

@@ -0,0 +1,22 @@
+<!--components/record-index/record-index.wxml-->
+<view class="chart-container">
+  <block wx:if="{{loading}}">
+    <view class="loading">加载中...</view>
+  </block>
+  <block wx:elif="{{error}}">
+    <view class="error">加载失败,请重试</view>
+  </block>
+  <block wx:else>
+    <view class="chart">
+      <view wx:for="{{charts}}" wx:key="*this" style="width: {{rect.width}}px; height: {{rect.height}}px;">
+        <ec-canvas 
+          id="dom-{{item.id}}"
+          canvas-id="canvas-{{item.id}}"
+          ec="{{item}}" 
+          force-use-old-canvas="true"
+          disable-scroll="true"
+        ></ec-canvas>
+      </view>
+    </view>
+  </block>
+</view> 

+ 21 - 0
miniprogram/components/record-index/record-index.wxss

@@ -0,0 +1,21 @@
+.chart-container {
+  width: 100%;
+  background: #fff;
+  padding: 20rpx;
+  box-sizing: border-box;
+}
+
+.loading, .error {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+}
+
+.chart {
+  width: 100%;
+}
+
+ec-canvas {
+  width: 100%;
+  height: 100%;
+} 

+ 7 - 0
miniprogram/components/tabbar/tabbar.json

@@ -0,0 +1,7 @@
+{
+  "component": true,
+  "usingComponents": {
+    "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
+    "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
+  }
+}

+ 0 - 0
miniprogram/components/tabbar/tabbar.scss


+ 95 - 0
miniprogram/components/tabbar/tabbar.ts

@@ -0,0 +1,95 @@
+import props from "../../miniprogram_npm/tdesign-miniprogram/action-sheet/props";
+import Tabbar from "../../miniprogram_npm/tdesign-miniprogram/tab-bar/tab-bar";
+
+Component({
+  data: {
+    tabbarHeight: 0,
+    pageHeight: "100vh",
+    value: "/pages/home/home",
+    list: [
+      {
+        value: "/pages/home/home",
+        label: "首页",
+        icon: "home",
+        path: "/pages/home/home",
+      },
+      {
+        value: "/module/chats/pages/index/index",
+        label: "健康管家",
+        icon: "app",
+        path: "/module/chats/pages/index/index",
+      },
+      {
+        value: "/pages/mine/mine",
+        label: "我的",
+        icon: "chat",
+        path: "/pages/mine/mine",
+      },
+    ],
+  },
+  properties: {
+    tabbarValue: {
+      type: String,
+      value: "",
+    },
+    patientId: {
+      type: Number,
+      value: 0,
+    },
+  },
+  methods: {
+    calculatePageHeight() {
+      const systemInfo = wx.getSystemInfoSync();
+      const windowHeight = systemInfo.windowHeight; // 屏幕可用高度
+
+      // 获取 tabbar 高度
+      const query = wx.createSelectorQuery();
+      query
+        .select(".t-tabbar")
+        .boundingClientRect((rect) => {
+          if (rect) {
+            const tabbarHeight = rect.height;
+            const contentHeight = windowHeight - tabbarHeight;
+            this.setData({
+              pageHeight: `${contentHeight}px`,
+            });
+          }
+        })
+        .exec();
+    },
+    toChatsPage(page: string) {
+      wx.redirectTo({
+        url: `${page}?component=guide&isShowGuide=true`,
+      });
+      wx.setStorageSync("isAnalysis", 3);
+    },
+    onChange(e:any) {
+      this.setData({
+        value: e.detail.value,
+      });
+      if (e.detail.value === "/module/chats/pages/index/index") {
+        this.toChatsPage(e.detail.value);
+      } else {
+        wx.redirectTo({
+          url: `${e.detail.value}`,
+          fail: (error) => {
+            console.error('Navigation failed:', error);
+            wx.showToast({
+              title: '页面跳转失败,请重试',
+              icon: 'none'
+            });
+          }
+        });
+      }
+    },
+  },
+  lifetimes: {
+    attached() {
+      // 赋值
+      this.setData({
+        value: this.data.tabbarValue,
+      });
+      this.calculatePageHeight();
+    },
+  },
+});

+ 5 - 0
miniprogram/components/tabbar/tabbar.wxml

@@ -0,0 +1,5 @@
+<t-tab-bar t-class="my-tabbar" value="{{value}}" bindchange="onChange" theme="tag" split="{{false}}">
+  <t-tab-bar-item wx:for="{{list}}" wx:key="value" value="{{item.value}}" icon="{{item.icon}}" data-id="{{item.id}}">
+    {{item.label}}
+  </t-tab-bar-item>
+</t-tab-bar>

+ 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;
+}

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

@@ -0,0 +1,24 @@
+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 v_l = arguments[2] ? arguments[2].split(':') : [];
+  var index = keys.indexOf(v_l[0]);
+
+  return index !== -1 ? v_l[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;

+ 42 - 0
miniprogram/lib/logic.ts

@@ -0,0 +1,42 @@
+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, title = '登录中') {
+  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) {
+    wx.showToast({ title: error?.errMsg ?? error?.message ?? `登录错误`, icon: 'none' });
+    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 };
+}

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

@@ -0,0 +1,94 @@
+import { login } from "../logic";
+import { request as _request } from "../wx/network";
+import { getAccountInfoSync } from "../wx/open-api";
+
+const shareRequestCache = new Map<string, IRequestData<any>>();
+
+const miniProgram = getAccountInfoSync();
+
+export function createRequest(option: IRequestCreateConfig) {
+  const { baseURL } = option;
+
+  return async function request<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['patientId'] = '783';
+    header["doctorId"] = wx.getStorageSync("doctorId") ?? "";
+    header["appId"] = miniProgram.appId ?? "";
+    header["version"] = miniProgram.version ?? "";
+    header["env"] = miniProgram.envVersion ?? "";
+
+    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) {
+          const data =
+            Array.isArray(response.data.rows) && response.data.total != null
+              ? { data: response.data.rows, total: response.data.total }
+              : response.data.data;
+          return transform({ data: data as any, 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 as any).catch(async (error: { errno: string }) => {
+      shareRequestCache.delete(key);
+      if (error.errno === "060201401") {
+        await login(true, "重新登录中");
+        wx.showLoading({ title: "重新加载中" });
+        try {
+          return await request(config);
+        } catch (error) {
+          throw error;
+        } finally {
+          wx.hideLoading();
+        }
+      }
+      throw error;
+    });
+  };
+}

+ 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)
+}
+
+

+ 32 - 0
miniprogram/lib/use/use-location.ts

@@ -0,0 +1,32 @@
+import type { LocationType } from "../wx/location";
+import { getFuzzyLocation } from "../wx/location";
+import request from "../request/method";
+
+
+export async function useLocation(type?: LocationType) {
+  const scope = 'scope.userFuzzyLocation';
+  const { authSetting } = await wx.getSetting();
+  if (!authSetting?.[scope]) {
+    await wx.authorize({ scope })
+  }
+
+  const location = await getFuzzyLocation(type);
+
+  const {prov, city} = await request({
+    url: `/surplus/getAddrByLatLong`,
+    method: 'GET',
+    params: {
+      longitude: location.longitude,
+      latitude: location.latitude,
+      type,
+    },
+    transform({ data }) { return data; }
+  });
+  
+
+  return {
+    longitude: location.longitude,
+    latitude: location.latitude,
+    description: `${prov}${city}`.replace(/省|市|自治区|特别行政区|壮族|回族|维吾尔/g,''),
+  }
+}

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

@@ -0,0 +1,53 @@
+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) {
+      console.log('getPhoneNumber-->', event);
+      const code = event.detail.code;
+      console.log('getPhoneNumber-->code', code);
+      if (!code) return;
+      console.log('getPhoneNumber-->');
+      _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() {
+  
+}

+ 9 - 0
miniprogram/lib/wx/location.ts

@@ -0,0 +1,9 @@
+import { withResolvers } from "../promise";
+
+export type LocationType = 'wgs84' | 'gcj02';
+
+export function getFuzzyLocation(type?: LocationType) {
+  const { promise, resolve, reject } = withResolvers<WechatMiniprogram.GetFuzzyLocationSuccessCallbackResult>();
+  wx.getFuzzyLocation({ type: type ?? 'gcj02', success: resolve, fail: reject });
+  return promise;
+}

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

@@ -0,0 +1,42 @@
+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) {
+      if (res.statusCode !== 200) reject({ errMsg: `上传失败(${res.statusCode})` })
+      else 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;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов