hurixing 1 год назад
Родитель
Сommit
aa399804f7

+ 7 - 0
src/api/comboMeal/index.js

@@ -34,4 +34,11 @@ export function del(query) {
     data: query
   })
   }
+
+export function tencentLearnPackage() {
+  return request({
+    url: `/learn/package/tencent/learnPackage`,
+    method: 'post'
+  })
+}
   

+ 17 - 0
src/api/patient/index.js

@@ -233,4 +233,21 @@ export function printReviewResultDetail(query) {
     method: 'post',
     data: query
   })
+}
+
+export function wXTencentSave(query) {
+  return request({
+    url: `/wx/tencent/save`,
+    method: 'post',
+    data: query
+  })
+}
+
+
+export function resetPasswordSys(query) {
+  return request({
+    url: `/patient/reset/password`,
+    method: 'post',
+    data: query
+  })
 }

BIN
src/assets/images/packages1.png


BIN
src/assets/images/packages2.png


BIN
src/assets/images/packages3.png


BIN
src/assets/images/payStatus_fail.png


BIN
src/assets/images/payStatus_succeed.png


+ 172 - 0
src/components/OfficialAccounts/DatePicker.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="date-picker-container">
+    <!-- 使用van-field作为输入框,优化移动端点击体验 -->
+    <van-field
+      v-model="displayDate"
+      :label="label"
+      readonly
+      :required="required"
+      clickable
+      :placeholder="placeholder"
+      @click="showPicker = true"
+      :error-message="error"
+    >
+      <template #right-icon>
+        <van-icon name="calendar-o" class="calendar-icon" />
+      </template>
+    </van-field>
+
+    <!-- 日期选择弹窗 -->
+    <van-popup
+      v-model="showPicker"
+      position="bottom"
+      round
+    >
+      <van-datetime-picker
+        v-model="currentDate"
+        type="date"
+        :min-date="minDate"
+        :max-date="maxDate"
+        :formatter="formatter"
+        @confirm="onConfirm"
+        @cancel="showPicker = false"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: {
+      type: [String, Date],
+      default: ''
+    },
+    label: String,
+    required: {
+      type: Boolean,
+      default: false
+    },
+    placeholder: {
+      type: String,
+      default: '请选择日期'
+    },
+    // 新增日期格式配置
+    dateFormat: {
+      type: String,
+      default: 'yyyy-MM-dd'
+    },
+    error: {
+      type: String,
+      default: ''
+    }
+  },
+
+  data() {
+    return {
+      showPicker: false,
+      currentDate: this.value ? new Date(this.value) : new Date(),
+      minDate: new Date(1990, 0, 1),
+      maxDate: new Date()
+    }
+  },
+
+  computed: {
+    // 显示格式化后的日期
+    displayDate: {
+      get() {
+        return this.formatDate(this.currentDate)
+      },
+      set() {} // 避免直接修改警告
+    }
+  },
+
+  watch: {
+    value: {
+      handler(newVal) {
+        if (newVal) {
+          this.currentDate = this.parseDate(newVal)
+        }
+      },
+      immediate: true
+    }
+  },
+
+  methods: {
+    // 日期确认处理
+    onConfirm(value) {
+      this.currentDate = value
+      this.$emit('input', this.formatDate(value))
+      this.showPicker = false
+    },
+
+    // 日期格式化方法
+    formatDate(date) {
+      if (!(date instanceof Date)) return ''
+      const pad = n => n.toString().padStart(2, '0')
+      return this.dateFormat
+        .replace('yyyy', date.getFullYear())
+        .replace('MM', pad(date.getMonth() + 1))
+        .replace('dd', pad(date.getDate()))
+    },
+
+    // 字符串转Date对象
+    parseDate(dateStr) {
+      if (dateStr instanceof Date) return dateStr
+      return new Date(dateStr.replace(/-/g, '/'))
+    },
+
+    // 自定义日期显示格式
+    formatter(type, value) {
+      if (type === 'year') return `${value}年`
+      if (type === 'month') return `${value}月`
+      if (type === 'day') return `${value}日`
+      return value
+    }
+  }
+}
+</script>
+
+<style scoped>
+.date-picker-container {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+  padding: 12px 0;
+  box-sizing: border-box;
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+  .date-picker-container {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .custom-label {
+    width: 100% !important;
+    margin-bottom: 8px;
+  }
+}
+
+/* Vant组件样式覆盖 */
+.calendar-icon {
+  color: #1989fa;
+  font-size: 18px;
+}
+
+/* 必填星号样式 */
+.required-star {
+  color: #ff4444;
+  margin-left: 4px;
+}
+
+/* 标签样式优化 */
+.custom-label {
+  flex: 0 0 auto;
+  font-size: 14px;
+  color: #333;
+  min-width: 6em;
+}
+</style>

+ 136 - 0
src/components/OfficialAccounts/InputField.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="input-wrapper">
+    <div class="form-row">
+      <label 
+        class="label"
+        :style="{ width: labelWidth }"
+      >
+        {{ label }}
+        <span v-if="required" class="required-star">*</span>
+      </label>
+      <div class="input-container">
+        <input
+          :type="type"
+          :value="value"
+          @input="$emit('input', $event.target.value)"
+          :placeholder="placeholder"
+          :pattern="pattern"
+          :required="required"
+          class="input-field"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MobileInput',
+  props: {
+    value: String,
+    label: String,
+    type: { type: String, default: 'text' },
+    placeholder: String,
+    pattern: String,
+    required: Boolean,
+    // 新增label宽度配置
+    labelWidth: {
+      type: [String, Number],
+      default: '5em', // 默认5个中文宽度
+      validator: value => typeof value === 'string' || typeof value === 'number'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.input-wrapper {
+  width: 100%;
+  margin-bottom: 1rem;
+  box-sizing: border-box;
+  background-color: #ffffff;
+}
+
+.form-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 0.8rem;
+  width: 100%;
+  min-height: 48px;
+}
+
+.label {
+  flex: 0 0 auto; /* 取消固定flex比例 */
+  font-size: 1rem;
+  color: #333;
+  font-weight: 600;
+  line-height: 1.5;
+  white-space: normal;
+  word-break: break-word;
+  text-align: right;
+  box-sizing: border-box;
+  max-width: 60%; /* 移动端最大宽度限制 */
+}
+
+.required-star {
+  color: #ff4d4f;
+  font-size: 1.1em;
+  margin-left: 0.2rem;
+}
+
+.input-container {
+  flex: 1;
+  position: relative;
+}
+
+.input-field {
+  width: 100%;
+  padding: 12px 16px;
+  background: #ffffff;
+  font-size: 1rem;
+  line-height: 1.5;
+  /* 移除边框 */
+  border: none !important;
+  transition: all 0.3s ease;
+  box-sizing: border-box;
+  transition: all 0.3s ease;
+}
+
+.input-field::placeholder {
+  text-align: right; /* placeholder右对齐 */
+  padding-right: 16px; /* 保留右侧间距 */
+  direction: rtl; /* 强制右对齐(备用方案) */
+  color: rgba(153, 153, 153, 1);
+}
+
+/* 移动端优化 */
+@media (max-width: 768px) {
+  .form-row {
+    flex-wrap: wrap;
+    gap: 0.6rem;
+  }
+
+  .label {
+    max-width: 70%; /* 移动端更宽松的限制 */
+    margin-bottom: 0.3rem;
+  }
+
+  .input-field {
+    min-height: 44px;
+    padding: 10px 16px;
+  }
+
+  .input-field:focus {
+    outline: none;
+    /* box-shadow: 0 0 0 1px rgb(8, 9, 10); */
+  }
+}
+
+/* 响应式宽度控制 */
+@media (min-width: 769px) {
+  .label {
+    max-width: 40%; /* PC端更紧凑的布局 */
+  }
+}
+</style>

+ 162 - 0
src/components/OfficialAccounts/MultiSelect.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="multi-select">
+    <div v-if="label" class="multi-select__label">{{ label }}</div>
+    <div class="multi-select__tags">
+      <div
+        v-for="(option, index) in options"
+        :key="index"
+        class="tag"
+        :class="{ 'tag--active': isSelected(option.value) }"
+        @click="toggleSelection(option.value)"
+        @keydown.enter.space.prevent="toggleSelection(option.value)"
+        role="checkbox"
+        :aria-checked="isSelected(option.value)"
+        tabindex="0"
+      >
+        {{ option.label }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MultiSelect',
+  props: {
+    value: {  // Vue2 中 v-model 默认对应 value 属性
+      type: Array,
+      default: () => []
+    },
+    label: String,
+    options: {
+      type: Array,
+      required: true,
+      validator: opts => opts.every(option => 
+        typeof option === 'object' && 
+        'value' in option && 
+        'label' in option
+      )
+    }
+  },
+  methods: {
+    isSelected(value) {
+      return this.value.some(item => this.deepEqual(item, value))
+    },
+    toggleSelection(value) {
+      const newSelection = [...this.value]
+      const index = newSelection.findIndex(item => this.deepEqual(item, value))
+
+      if (index > -1) {
+        newSelection.splice(index, 1)
+      } else {
+        newSelection.push(value)
+      }
+      this.$emit('input', newSelection)  // Vue2 中通过 input 事件更新
+    },
+    deepEqual(a, b) {
+      if (typeof a !== typeof b) return false
+      if (typeof a === 'object') {
+        return JSON.stringify(a) === JSON.stringify(b)
+      }
+      return a === b
+    }
+  }
+}
+</script>
+
+<style scoped>
+.multi-select {
+  --primary-color: #409eff;
+  --hover-color: #ecf5ff;
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+  background-color: #ffffff;
+}
+
+/* 标签文字放大 */
+.multi-select__label {
+  margin: 1rem;
+  font-size: 15px;
+  color: #333;
+  font-weight: 400;
+}
+
+/* 弹性布局优化 */
+.multi-select__tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-bottom: 1rem;
+}
+
+.tag {
+  margin-left: 1rem;
+  margin-bottom: 1rem;
+  /* 触控优化 */
+  padding: 8px 16px;
+  min-width: 60px;
+  min-height: 36px;
+  -webkit-tap-highlight-color: transparent;
+  touch-action: manipulation;
+  
+  /* 视觉优化 */
+  border: 1px solid #e0e0e0;  /* 加粗边框 */
+  border-radius: 8px;  /* 更圆润的圆角 */
+  font-size: 16px;  /* 放大文字 */
+  color: #333;
+  background: #f8f8f8;  /* 更明显的背景 */
+  box-shadow: 0 1px 1px rgba(0,0,0,0.05);  /* 添加轻微阴影 */
+  
+  /* 布局优化 */
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  transition: all 0.15s ease-in-out;
+}
+
+/* 移除 hover 效果(移动端不适用) */
+/* 添加点击反馈 */
+.tag:active {
+  transform: scale(0.98);
+  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
+}
+
+.tag--active {
+  background: var(--primary-color);
+  color: white;
+  border-color: var(--primary-color);
+  box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);  /* 激活态阴影 */
+}
+
+/* 焦点状态优化 */
+.tag:focus-visible {
+  outline: none;
+  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
+}
+
+/* 移动端横屏适配 */
+@media screen and (max-width: 480px) {
+  .multi-select__tags {
+    gap: 8px;
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+    
+  }
+  
+  .tag {
+    padding: 8px 12px;
+    font-size: 14px;
+    min-width: 54px;
+    min-height: 32px;
+  }
+}
+
+/* 超小屏幕优化 */
+@media screen and (max-width: 320px) {
+  .tag {
+    padding: 6px 10px;
+    font-size: 13px;
+    border-radius: 6px;
+  }
+}
+</style>

+ 158 - 0
src/components/OfficialAccounts/SingleSelect.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="select-wrapper">
+    <!-- 使用van-field作为选择器触发入口 -->
+    <van-field
+      v-model="displayText"
+      :label="label"
+      readonly
+      :required="required"
+      clickable
+      :placeholder="placeholder"
+      @click="showPickerclick"
+      :error-message="errorMessage"
+      @touchstart="pickerError()"
+      @focus="pickerError()"
+    >
+      <template #right-icon>
+        <van-icon name="arrow-down" class="arrow-icon" />
+      </template>
+    </van-field>
+
+    <!-- 选项选择弹窗 -->
+    <van-popup
+      v-model="showPicker"
+      position="bottom"
+      round
+      :style="{ height: '40%' }"
+    >
+      <van-picker
+        show-toolbar
+        :columns="formattedOptions"
+        @confirm="onConfirm"
+        @cancel="showPicker = false"
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    value: [String, Number],
+    label: {
+      type: String,
+      default: ''
+    },
+    options: {
+      type: Array,
+      required: true,
+      validator: opts => opts.every(option => 
+        typeof option === 'object' && 
+        'value' in option && 
+        'label' in option
+      )
+    },
+    placeholder: {
+      type: String,
+      default: '请选择'
+    },
+    required: Boolean,
+    errorMessage: {
+      type: String,
+    }
+  },
+
+  data() {
+    return {
+      showPicker: false,
+      selectedValue: this.value,
+    }
+  },
+
+  computed: {
+    // 格式化选项为Vant Picker需要的结构
+    formattedOptions() {
+      return this.options.map(option => ({
+        text: option.label,
+        value: option.value
+      }))
+    },
+
+    // 显示选中的文本
+    displayText() {
+      if (!this.selectedValue) return ''
+      const selected = this.options.find(
+        option => option.value === this.selectedValue
+      )
+      return selected ? selected.label : ''
+    }
+  },
+
+  watch: {
+    value(newVal) {
+      this.selectedValue = newVal
+    }
+  },
+
+  methods: {
+    showPickerclick() {
+      this.showPicker = true
+      this.pickerError()
+    },
+    pickerError(){
+      this.$emit('error-message');
+    },
+    // 确认选择
+    onConfirm(value) {
+      this.selectedValue = value.value
+      this.$emit('input', value.value)
+      this.showPicker = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.select-wrapper {
+  position: relative;
+  margin-top: 1rem;
+  margin-bottom: 1rem;
+  background-color: #ffffff;
+}
+
+/* 下拉箭头样式 */
+.arrow-icon {
+  color: #969799;
+  font-size: 16px;
+  transition: transform 0.3s;
+}
+
+.van-field__control:active + .arrow-icon {
+  transform: rotate(180deg);
+}
+
+/* 弹窗样式优化 */
+.van-picker {
+  --picker-option-font-size: 16px;
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+  
+  .van-field__control {
+    font-size: 14px !important;
+  }
+
+  .van-picker {
+    --picker-option-font-size: 14px;
+  }
+}
+
+/* 必填星号样式 */
+.required-star {
+  color: #ff4d4f;
+  margin-left: 4px;
+  font-size: 12px;
+  vertical-align: top;
+}
+</style>

+ 1 - 1
src/permission.js

@@ -8,7 +8,7 @@ import { isRelogin } from '@/utils/request'
 
 NProgress.configure({ showSpinner: false })
 
-const whiteList = ['/login', '/register']
+const whiteList = ['/login', '/register', '/officialAccounts/patientData','/officialAccounts/pay','/officialAccounts/payStatus']
 
 router.beforeEach((to, from, next) => {
   NProgress.start()

+ 26 - 3
src/router/index.js

@@ -85,7 +85,29 @@ export const constantRoutes = [
         meta: { title: '个人中心', icon: 'user' }
       }
     ]
-  }
+  },
+  {
+    path: '/officialAccounts/patientData',
+    component: () => import('@/views/officialAccounts/patientData'),
+    hidden: true,
+    props: true
+  },
+  {
+    path: '/officialAccounts/pay',
+    component: () => import('@/views/officialAccounts/pay'),
+    hidden: true,
+    meta: {
+      title: '支付页面',
+      requiresAuth: true
+    },
+    props: true
+  },
+  {
+    path: '/officialAccounts/payStatus',
+    component: () => import('@/views/officialAccounts/payStatus'),
+    hidden: true,
+    props: true
+  },
 ]
 
 // 动态路由,基于用户权限动态去加载
@@ -170,8 +192,9 @@ Router.prototype.replace = function push(location) {
 }
 
 export default new Router({
-  // mode: 'history', // 去掉url中的#
-  mode: 'hash', // 去掉url中的
+  mode: 'history', // 去掉url中的#
+  // mode: 'hash', // 去掉url中的
+  base: '/hcp/',
   scrollBehavior: () => ({ y: 0 }),
   routes: constantRoutes
 })

+ 39 - 0
src/utils/secret.js

@@ -32,4 +32,43 @@ export function symmetryDecrypt(word) {
     });
     const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
     return decryptedStr;
+  }
+
+
+  /**
+   * 加密  微信公众号
+   * @param {*} word 
+   * @returns 
+   */
+  export function symmetryEncryptWX(word) {
+    let srcs = CryptoJS.enc.Utf8.parse(word);
+    let encrypted = CryptoJS.AES.encrypt(srcs, SECRET_KEY, {
+      iv: SECRET_IV,
+      mode: CryptoJS.mode.CBC,
+      padding: CryptoJS.pad.ZeroPadding
+    });
+    
+    // 获取Base64字符串并替换加号
+    let base64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
+    return base64.replace(/\+/g, '-'); // 将+替换为-
+  }
+
+/**
+ * 解密 微信公众号
+ * @param {} word 
+ * @returns 
+ */
+  export function symmetryDecryptWX(word) {
+    // 将-替换回+
+    let base64Str = word.replace(/-/g, '+');
+    
+    let base64 = CryptoJS.enc.Base64.parse(base64Str);
+    let srcs = CryptoJS.enc.Base64.stringify(base64);
+    const decrypt = CryptoJS.AES.decrypt(srcs, SECRET_KEY, {
+      iv: SECRET_IV,
+      mode: CryptoJS.mode.CBC,
+      padding: CryptoJS.pad.ZeroPadding
+    });
+    const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
+    return decryptedStr;
   }

+ 1 - 1
src/views/doctor/physician/index.vue

@@ -350,7 +350,7 @@ export default {
       })
     },
     submit() {
-       this.$refs["formPass"].validate(valid => {
+      this.$refs["formPass"].validate(valid => {
          if (valid) {
            let newPassword = symmetryEncrypt(this.formPass.newPassword);
           violentResetPwd(this.formPass.id, newPassword).then(response => {

+ 406 - 0
src/views/officialAccounts/patientData/index.vue

@@ -0,0 +1,406 @@
+<template>
+  <div class="app-officialAccounts">
+
+    <div style="margin-bottom: 1rem;">
+        <!-- 导航栏(标题加粗) -->
+        <van-nav-bar
+        title="基础信息"
+        
+        >
+        <!-- :style="{ backgroundColor: '#ffffff', marginTop: '1rem' }" -->
+        <!-- 使用 title 插槽并添加加粗样式 -->
+        <!-- <template #title>
+            <span class="bold-title">基础信息</span>
+        </template> -->
+        </van-nav-bar>
+    </div>
+
+    <!-- 主表单区域 -->
+    <form @submit.prevent="handleSubmit">
+      
+
+      <!-- 基本信息组 -->
+      <!-- <fieldset class="form-group">
+        <legend>基本信息</legend> -->
+        <van-field
+            v-model="formData.name"
+            label="姓名"
+            placeholder="请输入姓名"
+            maxlength="20"
+            required
+            clearable
+            :label-width="100"
+            class="right-placeholder-field"
+            :error-message="errorform.name"
+            @input="nameError"
+            @blur="nameError"
+        />
+        
+        <single-select
+          v-model="formData.gender"
+          label="性别"
+          :options="getLabelForValue(dict.type.dict_sex)"
+          required
+           placeholder="请输入性别"
+          labelWidth="3em"
+          :errorMessage="errorform.gender"
+          @error-message="genderError"
+        />
+
+        <van-field
+            v-model="formData.mobile"
+            label="联系电话"
+            placeholder="请输入手机号"
+            required
+            clearable
+            :label-width="100"
+            :error-message="errorform.mobile"
+            @input="mobileError"
+            @blur="mobileError"
+        />
+
+        <div style="margin-top: 1rem;">
+            <van-field
+            v-model="formData.idCard"
+            label="身份证号码"
+            placeholder="请输入身份证号码"
+            required
+            clearable
+            :label-width="100"
+            :error-message="errorform.idCard"
+            @input="idCardError"
+            @blur="idCardError"
+            />    
+        </div>
+
+        <date-picker
+          v-model="formData.birthDate"
+          label="出生年月"
+          required
+        />
+
+        <van-field
+            v-model="formData.age"
+            label="年龄"
+            placeholder="请输入年龄"
+            maxlength="20"
+            required
+            clearable
+            :label-width="100"
+            :error-message="errorform.age"
+            @input="ageError"
+            @blur="ageError"
+        />
+        
+
+        <!-- <single-select
+          v-model="formData.profession"
+          label="职业"
+          placeholder="请输入职业"
+          :options="professionOptions"
+          required
+          labelWidth="3em"
+        /> -->
+
+        <single-select
+          v-model="formData.education"
+          label="教育程度"
+          :options="getLabelForValue(dict.type.education_type)"
+          placeholder="请输入教育程度"
+          required
+          labelWidth="5em"
+          :errorMessage="errorform.education"
+          @error-message="educationError"
+        />
+        <van-field
+            v-model="formData.doctorName"
+            label="推荐医生"
+            placeholder="请输入推荐医生"
+            clearable
+            maxlength="20"
+            :label-width="100"
+            class="right-placeholder-field"
+        />
+
+        <!-- 病史多选题组 -->
+        <MultiSelect
+          v-model="formData.medicalCurrentTypes"
+          label="现病史(多选)"
+          :options="getLabelForValue(dict.type.medical_current_type)"
+        />
+
+        <MultiSelect
+          v-model="formData.medicalHisTypes"
+          label="既往史(多选)"
+          :options="getLabelForValue(dict.type.medical_his_type)"
+        />
+        
+        <MultiSelect
+          v-model="formData.geneticDiseases"
+          label="家族史(多选)"
+          :options="getLabelForValue(dict.type.genetic_disease)"
+        />
+
+      <!-- 诊断结果多选题组 -->
+    <MultiSelect
+      v-model="formData.diagnoseResult"
+      label="诊疗结果(多选)"
+      :options="getLabelForValue(dict.type.diagnose_result_type)"
+    />
+
+    <MultiSelect
+      v-model="formData.tags"
+      label="认知障碍(多选)"
+      :options="getLabelForValue(dict.type.game_type)"
+    />
+      <!-- 提交按钮 -->
+      <button type="submit" class="submit-btn">提交</button>
+    </form>
+  </div>
+</template>
+
+<script>
+
+import InputField from '@/components/OfficialAccounts/InputField.vue';
+import SingleSelect from '@/components/OfficialAccounts/SingleSelect.vue';
+import MultiSelect from '@/components/OfficialAccounts/MultiSelect.vue';
+import DatePicker from '@/components/OfficialAccounts/DatePicker.vue';
+import { symmetryEncryptWX } from '@/utils/secret.js'
+import { Dialog } from 'vant';
+
+export default {
+  dicts: ['equipment_class', 'education_type', 'dict_sex', 'medical_his_type', 'medical_current_type', 'diagnose_result_type', 'career', 'game_type', 'genetic_disease', 'department', 'patient_source', 'pay_status', 'pay_method'],
+  components: {
+    InputField,
+    SingleSelect,
+    MultiSelect,
+    DatePicker
+  },
+  data() {
+    return {
+      minDate: new Date(1990,1,1),
+      formData: {
+        name: '',
+        gender: '',
+        mobile: '',
+        birthDate: '',
+        age: '',
+        idCard: '',
+        education: '',
+        doctorName: '',
+        medicalCurrentTypes:[],
+        medicalHisTypes: [],
+        geneticDiseases:[],
+        diagnoseResult: [],
+        tags:[]
+      },
+      errorform: {
+        name: '',
+        gender: '',
+        mobile: '',
+        birthDate: '',
+        age: '',
+        idCard: '',
+        education: '',
+        medicalHistory: [],
+        familyHistory: [],
+        diagnosis: []
+      },
+    };
+  },
+  computed: {
+    
+  },
+  created(){
+    if(Object.keys(this.$route.query).length > 0){
+        this.formData = JSON.parse(this.$route.query.payUserInfo);
+    }
+  },
+  methods: {
+    getLabelForValue(educationData) {
+        return educationData.map(item => ({
+                label: item.label,
+                value: item.value
+                }))
+    },
+    handleSubmit() {
+      // 表单验证
+      if (!this.validateForm()){
+        Dialog.alert({
+          title: '提示',
+          message: '请完善基础信息'
+        }).then(() => {
+          // on close
+        });
+        return;
+      }
+      this.$router.push({
+        path: '/officialAccounts/pay',
+        query: {
+          userInfo: symmetryEncryptWX(JSON.stringify(this.formData))
+        }
+      })
+    },
+    validateForm() {
+      this.nameError()
+      this.genderError()
+      this.mobileError()
+      this.birthDateError()
+      this.ageError()
+      this.idCardError()
+      this.educationError()
+      if(this.errorform.name != "" ){
+        return false
+      }
+      if(this.errorform.gender != "" ){
+        return false
+      }
+      if(this.errorform.mobile != "" ){
+        return false
+      }
+      if(this.errorform.birthDate != "" ){
+        return false
+      }
+      if(this.errorform.age != "" ){
+        return false
+      }
+      if(this.errorform.idCard != "" ){
+        return false
+      }
+      if(this.errorform.education != "" ){
+        return false
+      }
+      return true
+    },
+    //校验
+    nameError() {
+        this.errorform.name = this.formData.name.trim().length === 0 
+        ? '姓名不能为空' 
+        : '';
+    },
+    genderError() {
+        this.errorform.gender = this.formData.gender.trim().length === 0 
+        ? '性别不能为空' 
+        : '';
+    },
+    mobileError() {
+      const mobile = this.formData.mobile.trim()
+      const mobilePattern = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/
+      if(!mobile) {
+        this.errorform.mobile = "手机号不能为空";
+        return;
+      }
+      if(!mobilePattern.test(mobile)) {
+        this.errorform.mobile = "请输入正确的手机号码"
+        return;
+      }
+      this.errorform.mobile = ''
+    },
+    birthDateError() {
+        this.errorform.birthDate = this.formData.birthDate.trim().length === 0 
+        ? '出生年月不能为空' 
+        : '';
+    },
+    ageError() {
+        this.errorform.age = this.formData.age.trim().length === 0 
+        ? '年龄不能为空' 
+        : '';
+    },
+    idCardError() {
+        const idCard = this.formData.idCard.trim();
+    
+        // 基础格式校验
+        const idCardPattern = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
+        
+        if (!idCard) {
+            this.errorform.idCard = '身份证号码不能为空';
+            return;
+        }
+        
+        if (!idCardPattern.test(idCard)) {
+            this.errorform.idCard = '身份证号码格式不正确';
+            return;
+        }
+
+        if (this.formData.idCard.length == 18) {
+            this.formData.birthDate = this.formData.idCard.substring(6, 10) + "-" + this.formData.idCard.substring(10, 12) + "-" + this.formData.idCard.substring(12, 14)
+            this.formData.age = this.calculateAge(this.formData.idCard)
+        }
+        this.errorform.idCard = '';
+    },
+    // 获取年龄
+    calculateAge(iden) {
+      if (iden.length == 18) {
+        let ye = iden.substring(6, 10)
+        let date = new Date();
+        let year = date.getFullYear();
+        return year - ye
+      }
+    },
+    educationError() {
+        this.errorform.education = this.formData.education.trim().length === 0 
+        ? '教育程度不能为空' 
+        : '';
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.app-officialAccounts {
+  width: 100vw;
+  background-color:#f8f9fa;
+  box-sizing: border-box;
+}
+
+/* 添加加粗样式 */
+.bold-title {
+  font-weight: 600; /* 或者使用 bold 关键字 */
+  font-size: 20px;
+  color: #333;
+}
+
+.status-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30px;
+  color: #666;
+  font-family: Arial;
+
+  .time, .battery {
+    font-size: 1.2em;
+  }
+}
+
+form {
+  .submit-btn {
+    background-color: #007bff;
+    color: white;
+    padding: 12px 12px;
+    border: none;
+    border-radius: 5px;
+    font-size: 1.1em;
+    cursor: pointer;
+    width: calc(100% - 60px);
+    margin: 30px;
+
+    &:hover {
+      background-color: #0056b3;
+    }
+  }
+}
+
+/* 提示信息样式 */
+.toast {
+  position: fixed;
+  top: 20px;
+  right: -300px;
+  padding: 15px 25px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 5px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+  transition: right 0.3s ease;
+}
+</style>

+ 369 - 0
src/views/officialAccounts/pay/index.vue

@@ -0,0 +1,369 @@
+<template>
+  <div class="package-container">
+    <van-nav-bar
+      title="服务套餐"
+      left-text="返回"
+      left-arrow
+      @click-left="goBack()"
+    />
+
+    <van-cell
+    title="设备押金 1200(元)"
+    label="设备押金在服务到期后,若不再进行续费,设备完好情况下,联系工作人员退款。"
+    >
+    <!-- <van-stepper
+        slot="right-icon"
+        v-model="WxTencentForm.equipmentPledge"
+        button-size="24px"
+        input-width="58px"
+        class="absolute right14"
+        disabled  
+    /> -->
+    </van-cell>
+
+    <van-loading
+      v-if="loading"
+      type="spinner"
+      color="#1989fa"
+      size="40px"
+      vertical
+    >
+      加载中...
+    </van-loading>
+
+    <van-overlay v-if="loading" :show="loading" />
+
+    <!-- 修改为单列布局 -->
+    <div class="package-list">
+      <div 
+        v-for="(packageItem, index) in packages" 
+        :key="index" 
+        class="package-card"
+      >
+        <div class="image-wrapper" @click="showDialog(packageItem)">
+          <van-image
+            :src="packageItem.icon"
+            fit="cover"
+            class="package-image"
+          />
+          <div class="overlay-text">
+            <h3 class="card-title">{{ packageItem.type }}</h3>
+            <p class="card-price">{{ formatPrice(packageItem.price) }}元</p>
+          </div>
+        </div>
+      </div>
+    </div>
+
+
+    <van-dialog v-model="payShow" title="支付确认" show-cancel-button :lazy-render="false" @confirm="handleWechatAuth">
+    
+    </van-dialog>
+  </div>
+</template>
+
+<script>
+import {tencentLearnPackage} from '@/api/comboMeal/index'
+import { symmetryDecryptWX } from '@/utils/secret.js'
+import {wXTencentSave} from '@/api/patient/index'
+
+export default {
+  data() {
+    return {
+      packages: [
+      ],
+      WxTencentForm: {},
+      payShow: false,
+      learningPackageId: "",
+      loading: false
+    }
+  },
+  created() {
+    this.getLearnPackage()
+  },
+  methods: {
+    getLearnPackage(){
+        tencentLearnPackage().then(resp=>{
+            this.packages = resp.data.map(item => {
+                const newItem = { ...item }; // 创建新对象
+                switch (newItem.timeType) {
+                    case "B":
+                        newItem.icon = require('@/assets/images/packages3.png');
+                        newItem.type = "季卡"
+                        break;
+                    case "C":
+                        newItem.icon = require('@/assets/images/packages2.png');
+                        newItem.type = "半年卡"
+                        break;
+                    case "D":
+                        newItem.icon = require('@/assets/images/packages1.png');
+                        newItem.type = "年卡"
+                        break;
+                }
+                return newItem;
+            });
+            if(Object.keys(this.$route.query).length > 0){
+                const query = this.$route.query;
+                this.WxTencentForm = JSON.parse(symmetryDecryptWX(decodeURIComponent(this.$route.query.userInfo)));
+                if(query.code && query.packagesId && query.state){
+                    this.handleSubmit(query.code,query.packagesId,query.state);
+                }
+                this.WxTencentForm.equipmentPledge = 1
+            }else {
+                this.$router.push({
+                      path: '/officialAccounts/patientData',
+                })
+            }
+        })
+    },
+    goBack(){
+        this.$router.push({
+            path: '/officialAccounts/patientData',
+            query: {
+                payUserInfo: symmetryDecryptWX(this.$route.query.userInfo)
+            }
+        })
+    },
+    formatPrice(price) {
+      return (price / 100).toFixed(2)
+    },
+    showDialog(row) {
+        this.payShow = true
+        this.learningPackageId= row.id
+    },
+    // 加载微信JS-SDK
+    loadWechatSDK() {
+      return new Promise((resolve, reject) => {
+        if (window.wx) {
+          resolve(window.wx);
+          return;
+        }
+
+        const script = document.createElement('script');
+        script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
+        script.onload = () => {
+          // SDK加载成功后初始化配置
+          this.initWxConfig().then(resolve).catch(reject);
+        };
+        script.onerror = () => {
+          reject(new Error('微信SDK加载失败'));
+        };
+        document.head.appendChild(script);
+      });
+    },
+    // 初始化微信配置
+    initWxConfig() {
+      return this.$http.get('/api/wechat/config', {
+        params: {
+          url: window.location.href.split('#')[0]
+        }
+      }).then(response => {
+        const { appId, timestamp, nonceStr, signature } = response.data;
+
+        wx.config({
+          debug: process.env.NODE_ENV === 'development',
+          appId,
+          timestamp,
+          nonceStr,
+          signature,
+          jsApiList: ['chooseWXPay']
+        });
+
+        return new Promise((resolve, reject) => {
+          wx.ready(() => resolve());
+          wx.error((res) => reject(new Error(`SDK配置失败: ${JSON.stringify(res)}`)));
+        });
+      });
+    },
+    handleSubmit(code,id,state) {
+        this.loading = true;
+        console.log(this.WxTencentForm)
+        if(this.WxTencentForm){
+            const params = {
+                state: state,
+                code: code,
+                packageId: id,
+                equipmentPledge: '1',
+                name: this.WxTencentForm.name,
+                gender: this.WxTencentForm.gender,
+                age: this.WxTencentForm.age,
+                mobile: this.WxTencentForm.mobile,
+                idCard: this.WxTencentForm.idCard,
+                birthDate: this.WxTencentForm.birthDate,
+                education: this.WxTencentForm.education,
+                medicalCurrentTypes: this.WxTencentForm.medicalCurrentTypes.toString(),
+                medicalHisTypes: this.WxTencentForm.medicalHisTypes.toString(),
+                geneticDiseases: this.WxTencentForm.geneticDiseases.toString(),
+                diagnoseResult: this.WxTencentForm.diagnoseResult.toString(),
+                tags: this.WxTencentForm.tags.toString(),
+                doctorName: this.WxTencentForm.doctorName
+            }
+            wXTencentSave(params).then(resp => {
+                if(resp.code == 201) {
+                  console.log("重复调用")
+                  this.$router.push({
+                      path: '/officialAccounts/patientData',
+                  })
+                  return
+                }
+                // 微信JSAPI调用参数
+                const payParams = {
+                    appId: resp.data.wxAppId,
+                    timeStamp: resp.data.wxTimeStamp.toString(),
+                    nonceStr: resp.data.wxNonceStr,
+                    package: resp.data.wxPackage,
+                    signType: resp.data.wxSignType, // 统一转换标准类型
+                    paySign: resp.data.wxPaySign
+                };
+                // // 调用微信支付接口
+                // wx.chooseWXPay({
+                //     ...payParams,
+                //     success: res => {
+                //         this.$toast.success('支付成功');
+                //         this.handlePaymentSuccess(res);
+                //     },
+                //     fail: err => {
+                //         this.$toast.error(`支付失败: ${err.errMsg || '系统错误'}`);
+                //         this.handlePaymentFailure(err);
+                //     },
+                //     complete: () => {
+                //         this.loading = false;
+                //     }
+                // });
+                this.invokeWechatPay(payParams)
+
+            });
+        }
+    },
+    invokeWechatPay(payParams) {
+      WeixinJSBridge.invoke('getBrandWCPayRequest', {
+        ...payParams
+      }, (res) => {
+        this.loading = false;
+        if (res.err_msg === 'get_brand_wcpay_request:ok') {
+          this.handlePaymentSuccess();
+        } else {
+          this.handlePaymentFailure(res);
+        }
+      });
+    },
+    // 支付成功处理
+    handlePaymentSuccess() {
+      console.log("成功")
+      this.$router.push({
+            path: '/officialAccounts/payStatus',
+            query: {
+                payStatus: '0'
+            }
+        })
+    },
+    // 支付失败处理
+    handlePaymentFailure(error) {
+      console.log("失败")
+      this.$router.push({
+            path: '/officialAccounts/payStatus',
+            query: {
+                payStatus: '1'
+            }
+        })
+    },
+    handleWechatAuth() {
+      const currentPath = window.location.pathname;
+      const currentQuery = { ...this.$route.query,
+        packagesId: this.learningPackageId,
+      };
+      let redirectPath = currentPath;
+      const queryString = Object.keys(currentQuery)
+        .map(key => `${key}=${currentQuery[key]}`)
+        .join('&');
+      if (queryString) {
+        redirectPath += `?${queryString}`;
+      }
+      const redirectUri = encodeURIComponent(
+        window.location.origin + redirectPath
+      );
+      const params = {
+        appId: 'wxe6dbe98e8c6d1a4b',
+        redirectUri: redirectUri,
+        scope: 'snsapi_base',
+        state: this.generateState()
+      }
+      
+      const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${params.appId}&redirect_uri=${params.redirectUri}&response_type=code&scope=${params.scope}&state=${params.state}#wechat_redirect`;
+      window.location.href = authUrl
+    },
+    generateState() {
+      return Math.random().toString(36).substr(2) + Date.now().toString(36)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+/* 修改图片容器为流式布局 */
+.image-wrapper {
+  position: relative;
+  width: 80%;
+  display: flex;
+  background: #f0f2f5;
+  margin: 2rem auto;
+  border-radius: 12px;
+  overflow: hidden;
+
+  .package-image {
+    width: 100%;
+    height: auto;
+    min-height: 150px;
+    object-fit: cover;
+    transition: height 0.3s ease;
+  }
+
+  .overlay-text {
+    position: absolute;
+    top: 0; // 定位到顶部
+    left: 0;
+    right: 0;
+    padding: 12px 16px;
+    background: transparent !important; // 透明背景
+    
+    .card-title {
+      font-size: clamp(16px, 4vw, 18px);
+      color: #fff;
+      margin-left: 7%;
+      margin-top: 5%;
+    }
+    
+    .card-price {
+      font-size: clamp(24px, 6vw, 32px);
+      color: #fff;
+      font-weight: 400;
+      margin-top: 2%;
+      margin-left: 7%;
+    }
+  }
+}
+
+// 响应式适配
+@media (max-width: 480px) {
+  .image-wrapper {
+    width: 85%;
+    margin: 1.5rem auto;
+    border-radius: 8px;
+    
+    .overlay-text {
+      padding: 8px 12px;
+    }
+  }
+}
+
+// 极端小屏适配
+@media (max-width: 320px) {
+  .image-wrapper {
+    width: 90%;
+    margin: 1rem auto;
+    
+    .overlay-text {
+      padding: 6px 8px;
+    }
+  }
+}
+</style>

+ 82 - 0
src/views/officialAccounts/payStatus/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <div class="pay_status">
+    <van-nav-bar title="支付结果" />
+    <div class="image-container">
+      <van-image 
+        width="120" 
+        height="120" 
+        :src="payStatusForm.imageUrl"
+      />
+      <h3 class="status-title">{{payStatusForm.statusTitle}}</h3>
+      <p class="status-desc">{{payStatusForm.statusDesc}}</p>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      payStatusForm:{
+        imageUrl: "",
+        statusTitle: "",
+        statusDesc: ""
+      }
+    }; 
+  },
+  created(){
+      this.initialize()  
+  },
+  methods:{
+    initialize() {
+        if(Object.keys(this.$route.query).length > 0){
+            if(this.$route.query.payStatus === '0'){
+                this.payStatusForm.imageUrl = require('@/assets/images/payStatus_succeed.png')
+                this.payStatusForm.statusTitle = "支付成功"
+                this.payStatusForm.statusDesc = "您已完成支付"
+            }else{
+                this.payStatusForm.imageUrl = require('@/assets/images/payStatus_fail.png')
+                this.payStatusForm.statusTitle = "支付失败"
+                this.payStatusForm.statusDesc = "您尚未完成支付"
+            }
+        }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.pay_status {
+  padding-top: 11px; /* 导航栏高度补偿 */
+}
+
+.image-container {
+  display: flex;
+  flex-direction: column; /* 垂直排列 */
+  align-items: center;    /* 水平居中 */
+  padding: 3rem 1rem 1rem; /* 上边距补偿,左右内边距 */
+  background: #fff;       /* 背景色 */
+  min-height: calc(100vh - 44px); /* 视口高度适配 */
+}
+
+.image-container van-image {
+  width: 120px;
+  height: 120px;
+  margin-bottom: 1.5rem; /* 图片与文字间距 */
+  object-fit: contain;    /* 图片比例保持 */
+}
+
+.status-title {
+  font-size: 1.2rem;
+  color: #030303;
+  margin-bottom: 0.8rem; /* 标题与描述间距 */
+  font-weight: 600;
+}
+
+.status-desc {
+  font-size: 1rem;
+  color: #666;
+  text-align: center;
+  line-height: 1.5;
+}
+</style>

+ 53 - 2
src/views/patient/list/index.vue

@@ -50,6 +50,7 @@
           <el-button size="mini" type="text" style="color: red;" @click="handelDel(scope.row.id)">删除</el-button>
           <el-button size="mini" type="text" @click="handelCePingInfo(scope.row)">测评报告</el-button>
           <el-button size="mini" type="text" @click="handelTrain(scope.row)">训练数据</el-button>
+          <el-button size="mini" type="text" @click="resetPassWord(scope.row)">重置密码</el-button>
           <!-- <router-link :to="'/train/list/index/' + scope.row.id" style="color: #1890ff; font-size: 12px;margin: 0 10px;">训练数据</router-link> -->
         </template>
       </el-table-column>
@@ -59,15 +60,28 @@
     <cePingInfo ref="cepingInfo" @refresh="handleQuery" />
     <!--  分页  -->
     <pagination v-show="total > 0" :total="total" :page.sync="queryParams.page"  :page-sizes="[10, 20, 30,50,100]" :limit.sync="queryParams.limit" />
+
+    <el-dialog title="重置患者密码" :visible="resetShow" width="700px" append-to-body destroy-on-close>
+       <el-form ref="resetForm" :model="resetForm" :rules="rulesPass" label-width="110px">
+          <el-form-item label="新密码" prop="newPwd">
+            <el-input v-model="resetForm.newPwd" placeholder="请输入新密码" type="password" show-password />
+          </el-form-item>
+          <el-form-item>
+        <el-button type="primary" size="mini" @click="submit">修改</el-button>
+          <el-button type="danger" size="mini" @click="resetShow = false">关闭</el-button> 
+        </el-form-item>
+       </el-form>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { listData, delpatient, patientDetail, learnSelectAll, queryMonthRecord, reviewTaskSave } from '@/api/patient/index'
+import { listData, delpatient, patientDetail, learnSelectAll, queryMonthRecord, reviewTaskSave,resetPasswordSys } from '@/api/patient/index'
 import { debounce } from 'lodash-es'
 import lookDetail from './lookDetail.vue';
 import trainDetatil from './trainDetatil.vue';
 import cePingInfo from './cePingInfo.vue';
+import {symmetryEncrypt} from '@/utils/secret'
 export default {
   name: "patient_list",
   dicts: ['education_type'],
@@ -92,7 +106,23 @@ export default {
         limit: 10,
         status: 0
       },
-
+      resetShow: false,
+      resetForm:{
+        id: "",
+        pwd: "",
+        newPwd: ""
+      },
+      rulesPass: {
+        newPwd: [
+          { required: true, message: "新密码不能为空", trigger: "blur" },
+          {
+            pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,11}$/,
+            message:
+            "密码长度必须8-11位数且包含大小写字母,数字",
+            trigger: "blur"
+          }
+        ]
+      }
     };
   },
   created() {
@@ -123,6 +153,27 @@ export default {
   },
 
   methods: {
+    submit() {
+      this.$refs["resetForm"].validate(valid => {
+        if(valid) {
+          this.resetForm.pwd = symmetryEncrypt(this.resetForm.newPwd)
+          this.resetForm.newPwd = ""
+          resetPasswordSys(this.resetForm).then(resp => {
+            if(resp.data == true) {
+              this.$modal.msgSuccess("修改成功");
+              this.resetShow = false
+            }
+          })
+        }
+      })
+    },
+    resetPassWord(row) {
+      this.resetShow = true
+      this.resetForm = {
+        id: row.id,
+        newPwd: ""
+      }
+    },
     /** 查询机构列表 */
     getList() {
       this.loading = true;

+ 1 - 1
src/views/patient/list/print/mmsePrint.vue

@@ -7,7 +7,7 @@
                 <hr style="margin: 0px 0;">
                 <div style="text-align: left; display: flex; gap: 20px; flex-wrap: wrap;">
                 <p style="flex: 1 1 100px;"><strong>姓名:</strong>{{defaultVal.name}}</p>
-                <p style="flex: 1 1 100px;"><strong>性别:</strong>{{defaultVal.gender === 1 ? '女' : '男'}}</p>
+                <p style="flex: 1 1 100px;"><strong>性别:</strong>{{defaultVal.gender === "1" ? '女' : '男'}}</p>
                 <p style="flex: 1 1 100px;"><strong>年龄:</strong>{{defaultVal.age}}</p>
                 <p style="flex: 1 1 100px;"><strong>教育年限:</strong>{{getLabelForValue(defaultVal.education,dict.type.education_type)}}</p>
                 </div>

+ 1 - 1
src/views/patient/list/print/mocaPrint.vue

@@ -6,7 +6,7 @@
         <hr style="margin: 0px 0;">
         <div style="text-align: left; display: flex; gap: 20px; flex-wrap: wrap;">
           <p style="flex: 1 1 100px;"><strong>姓名:</strong>{{defaultVal.name}}</p>
-                <p style="flex: 1 1 100px;"><strong>性别:</strong>{{defaultVal.gender === 1 ? '女' : '男'}}</p>
+                <p style="flex: 1 1 100px;"><strong>性别:</strong>{{defaultVal.gender === "1" ? '女' : '男'}}</p>
                 <p style="flex: 1 1 100px;"><strong>年龄:</strong>{{defaultVal.age}}</p>
                 <p style="flex: 1 1 100px;"><strong>教育年限:</strong>{{getLabelForValue(defaultVal.education,dict.type.education_type)}}</p>
         </div>