yuanmingze 3 долоо хоног өмнө
parent
commit
0ae6d5f1ea

+ 523 - 0
src/pages-mine/record-info/index.vue

@@ -0,0 +1,523 @@
+<template>
+  <view class="filing-page">
+    <view v-if="loading" class="page-loading">
+      <wd-loading size="48rpx" />
+      <text>备案信息加载中...</text>
+    </view>
+
+    <template v-else>
+      <view class="invitation-card">
+        <view class="invitation-card__title">备案邀请</view>
+        <view class="invitation-card__content">
+          <text v-if="entName" class="company-name">【{{ entName }}】</text>
+          <text>向您发起了备案邀请,请提交您的备案信息。</text>
+        </view>
+      </view>
+
+      <view class="form-card">
+        <view class="form-row form-row--record">
+          <view class="form-label">医药代表资质备案号:</view>
+          <view class="form-control form-control--column">
+            <view class="optional-tag">没有备案号可以不填</view>
+            <input
+              v-model="form.filingNo"
+              class="record-input"
+              :maxlength="50"
+              placeholder="填写备案号"
+              placeholder-class="form-placeholder"
+            />
+          </view>
+        </view>
+
+        <view class="form-row" @click="openEducationPicker">
+          <view class="form-label">学历:</view>
+          <view class="form-control">
+            <text :class="form.degreeName ? 'form-value' : 'form-placeholder'">
+              {{ form.degreeName || '请选择学历' }}
+            </text>
+            <wd-icon name="right" size="32rpx" color="#a9afb8" />
+          </view>
+        </view>
+
+        <view class="form-row" @click="openMajorPicker">
+          <view class="form-label">所学专业类别:</view>
+          <view class="form-control">
+            <text :class="form.majorName ? 'form-value' : 'form-placeholder'">
+              {{ form.majorName || '请选择专业类别' }}
+            </text>
+            <wd-icon name="right" size="32rpx" color="#a9afb8" />
+          </view>
+        </view>
+
+        <view class="form-row" @click="openAreaPicker">
+          <view class="form-label">所在区域:</view>
+          <view class="form-control">
+            <text :class="form.areaName ? 'form-value' : 'form-placeholder'">
+              {{ form.areaName || '请选择区域' }}
+            </text>
+            <wd-icon name="right" size="32rpx" color="#a9afb8" />
+          </view>
+        </view>
+
+        <view class="form-row form-row--last" @click="openDrugPicker">
+          <view class="form-label">推广药品:</view>
+          <view class="form-control">
+            <text class="form-value">{{ selectedDrugText }}</text>
+            <wd-icon name="right" size="32rpx" color="#a9afb8" />
+          </view>
+        </view>
+      </view>
+
+      <view class="submit-section">
+        <button class="submit-button" :disabled="submitting" @click="submit">
+          {{ submitting ? '提交中...' : '提交备案' }}
+        </button>
+      </view>
+    </template>
+
+    <wd-picker
+      v-model:visible="educationPickerVisible"
+      title="选择学历"
+      :columns="educationList"
+      label-key="label"
+      value-key="value"
+      root-portal
+      @confirm="confirmEducation"
+    />
+
+    <wd-picker
+      v-model:visible="majorPickerVisible"
+      title="选择专业类别"
+      :columns="majorList"
+      label-key="label"
+      value-key="value"
+      root-portal
+      @confirm="confirmMajor"
+    />
+
+    <wd-cascader
+      v-model="areaPickerValue"
+      v-model:visible="areaPickerVisible"
+      title="选择所在区域"
+      :options="areaOptions"
+      root-portal
+      @confirm="confirmArea"
+    />
+
+    <wd-select-picker
+      v-model="selectedDrugIds"
+      v-model:visible="drugPickerVisible"
+      title="选择推广药品"
+      :columns="drugOptions"
+      filterable
+      filter-placeholder="搜索药品"
+      root-portal
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+
+import { onLoad } from '@dcloudio/uni-app'
+import { useCascaderAreaData } from '@vant/area-data'
+
+import { getDictTypeApi } from '@/services/modules/common'
+import type { DictItem } from '@/services/modules/common/type'
+import {
+  getUserFilingPendingDetailApi,
+  submitUserFilingApi,
+} from '@/services/modules/userInfo'
+import type {
+  FilingDrugId,
+  FilingDrugItem,
+  UserFilingSubmitRequest,
+} from '@/services/modules/userInfo/type'
+
+interface PageOptions {
+  entTaxCode?: string
+  entName?: string
+}
+
+interface AreaOption {
+  value: string
+  text: string
+  children?: AreaOption[]
+  isLeaf?: boolean
+}
+
+interface AreaConfirmEvent {
+  selectedOptions?: AreaOption[]
+}
+
+const entTaxCode = ref('')
+const entName = ref('')
+const loading = ref(true)
+const submitting = ref(false)
+
+const educationPickerVisible = ref(false)
+const majorPickerVisible = ref(false)
+const areaPickerVisible = ref(false)
+const drugPickerVisible = ref(false)
+const areaPickerValue = ref<string | number>()
+
+const educationList = ref<DictItem[]>([])
+const majorList = ref<DictItem[]>([])
+const drugList = ref<FilingDrugItem[]>([])
+const selectedDrugIds = ref<string[]>([])
+
+const form = reactive({
+  filingNo: '',
+  degree: '',
+  degreeName: '',
+  majorCategory: '',
+  majorName: '',
+  province: '',
+  provinceCode: '',
+  city: '',
+  cityCode: '',
+  areaName: '',
+})
+
+const rawAreaOptions = useCascaderAreaData() as AreaOption[]
+const areaOptions = convertCityLeaf(rawAreaOptions)
+
+const drugOptions = computed(() =>
+  drugList.value.map((item) => ({
+    label: item.drug_name,
+    value: String(item.drug_id),
+  }))
+)
+
+const selectedDrugText = computed(() => {
+  if (!selectedDrugIds.value.length) return '默认选择全部'
+  return `已选择${selectedDrugIds.value.length}个药品`
+})
+
+onLoad((options?: PageOptions) => {
+  entTaxCode.value = decodeQueryValue(options?.entTaxCode)
+  entName.value = decodeQueryValue(options?.entName)
+
+  if (!entTaxCode.value) {
+    loading.value = false
+    uni.showModal({
+      title: '提示',
+      content: '缺少企业税号,无法加载备案信息',
+      showCancel: false,
+      success() {
+        uni.navigateBack()
+      },
+    })
+    return
+  }
+
+  void initPage()
+})
+
+const initPage = async () => {
+  loading.value = true
+
+  try {
+    const [detailRes, degreeRes, majorRes] = await Promise.all([
+      getUserFilingPendingDetailApi(entTaxCode.value),
+      getDictTypeApi('user_degree'),
+      getDictTypeApi('user_filing_major'),
+    ])
+
+    educationList.value = degreeRes.data ?? []
+    majorList.value = majorRes.data ?? []
+
+    const detail = detailRes.data
+    const record = detail.record
+
+    entName.value = detail.drugEntName || record.drugEntName || record.deptName || entName.value
+    drugList.value = detail.optionalDrugs ?? []
+
+    // 详情仅回填备案号,其余备案字段由用户本次重新选择。
+    form.filingNo = record.filingNo ?? ''
+  } catch (error) {
+    console.error('获取待提交备案信息失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const openEducationPicker = () => {
+  if (showEmptyOptions(educationList.value, '暂无可选学历')) return
+  educationPickerVisible.value = true
+}
+
+const openMajorPicker = () => {
+  if (showEmptyOptions(majorList.value, '暂无可选专业类别')) return
+  majorPickerVisible.value = true
+}
+
+const openAreaPicker = () => {
+  areaPickerVisible.value = true
+}
+
+const openDrugPicker = () => {
+  if (showEmptyOptions(drugOptions.value, '暂无可选推广药品')) return
+  drugPickerVisible.value = true
+}
+
+const confirmEducation = (event: PickerConfirmEvent) => {
+  const item = event.selectedItems[0] as DictItem | undefined
+  if (!item) return
+
+  form.degree = item.value
+  form.degreeName = item.label
+}
+
+const confirmMajor = (event: PickerConfirmEvent) => {
+  const item = event.selectedItems[0] as DictItem | undefined
+  if (!item) return
+
+  form.majorCategory = item.value
+  form.majorName = item.label
+}
+
+const confirmArea = (event: AreaConfirmEvent) => {
+  const [province, city] = event.selectedOptions ?? []
+  if (!province || !city) return
+
+  form.province = province.text
+  form.provinceCode = String(province.value)
+  form.city = city.text
+  form.cityCode = String(city.value)
+  form.areaName = `${province.text}/${city.text}`
+  areaPickerValue.value = city.value
+}
+
+const submit = async () => {
+  if (submitting.value) return
+
+  const payload: UserFilingSubmitRequest = {
+    entTaxCode: entTaxCode.value,
+    filingNo: form.filingNo.trim(),
+    degree: form.degree,
+    majorCategory: form.majorCategory,
+    province: form.province,
+    provinceCode: form.provinceCode,
+    city: form.city,
+    cityCode: form.cityCode,
+    promotionDrugIds: resolvePromotionDrugIds(),
+  }
+
+  try {
+    submitting.value = true
+    await submitUserFilingApi(payload)
+    uni.showToast({
+      title: '提交成功',
+      icon: 'success',
+    })
+
+    setTimeout(() => {
+      uni.navigateBack()
+    }, 800)
+  } catch (error) {
+    console.error('提交备案失败:', error)
+    submitting.value = false
+  }
+}
+
+const resolvePromotionDrugIds = (): FilingDrugId[] => {
+  if (!selectedDrugIds.value.length) {
+    return drugList.value.map((item) => item.drug_id)
+  }
+
+  return [...selectedDrugIds.value]
+}
+
+function convertCityLeaf(list: AreaOption[]): AreaOption[] {
+  return list.map((province) => ({
+    ...province,
+    children: province.children?.map((city) => ({
+      value: city.value,
+      text: city.text,
+      isLeaf: true,
+    })),
+  }))
+}
+
+const showEmptyOptions = (options: unknown[], message: string): boolean => {
+  if (options.length) return false
+
+  uni.showToast({
+    title: message,
+    icon: 'none',
+  })
+  return true
+}
+
+const decodeQueryValue = (value?: string): string => {
+  if (!value) return ''
+
+  try {
+    return decodeURIComponent(value)
+  } catch {
+    return value
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.filing-page {
+  min-height: 100vh;
+  padding: 28rpx 24rpx calc(150rpx + env(safe-area-inset-bottom));
+  box-sizing: border-box;
+  background: #f5f7fa;
+}
+
+.page-loading {
+  min-height: 60vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 20rpx;
+  color: #8a93a0;
+  font-size: 26rpx;
+}
+
+.invitation-card,
+.form-card {
+  background: #ffffff;
+  border-radius: 24rpx;
+  box-shadow: 0 10rpx 30rpx rgba(31, 45, 61, 0.06);
+}
+
+.invitation-card {
+  padding: 30rpx;
+  margin-bottom: 24rpx;
+}
+
+.invitation-card__title {
+  margin-bottom: 14rpx;
+  color: #1f2329;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+
+.invitation-card__content {
+  color: #68707c;
+  font-size: 28rpx;
+  line-height: 44rpx;
+}
+
+.company-name {
+  color: #30343b;
+  font-weight: 600;
+}
+
+.form-card {
+  padding: 0 30rpx;
+  overflow: hidden;
+}
+
+.form-row {
+  min-height: 104rpx;
+  display: flex;
+  align-items: center;
+  border-bottom: 1rpx solid #eeeeee;
+}
+
+.form-row--record {
+  min-height: 140rpx;
+}
+
+.form-row--last {
+  border-bottom: none;
+}
+
+.form-label {
+  width: 230rpx;
+  flex-shrink: 0;
+  color: #4f5661;
+  font-size: 28rpx;
+}
+
+.form-control {
+  min-width: 0;
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10rpx;
+  text-align: right;
+}
+
+.form-control--column {
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+}
+
+.optional-tag {
+  padding: 5rpx 14rpx;
+  border-radius: 30rpx;
+  background: #edf3ff;
+  color: #537cf3;
+  font-size: 22rpx;
+  line-height: 30rpx;
+}
+
+.record-input {
+  width: 100%;
+  height: 54rpx;
+  color: #30343b;
+  font-size: 28rpx;
+  line-height: 54rpx;
+  text-align: right;
+}
+
+.form-value,
+.form-placeholder {
+  max-width: 370rpx;
+  overflow: hidden;
+  font-size: 28rpx;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.form-value {
+  color: #30343b;
+}
+
+.form-placeholder {
+  color: #a9afb8;
+}
+
+.submit-section {
+  position: fixed;
+  z-index: 20;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 20rpx 36rpx calc(20rpx + env(safe-area-inset-bottom));
+  background: rgba(255, 255, 255, 0.96);
+  box-shadow: 0 -8rpx 24rpx rgba(31, 45, 61, 0.08);
+}
+
+.submit-button {
+  width: 100%;
+  height: 88rpx;
+  padding: 0;
+  border: none;
+  border-radius: 44rpx;
+  background: #537cf3;
+  color: #ffffff;
+  font-size: 32rpx;
+  font-weight: 600;
+  line-height: 88rpx;
+
+  &::after {
+    border: none;
+  }
+
+  &[disabled] {
+    opacity: 0.65;
+    color: #ffffff;
+  }
+}
+</style>

+ 7 - 0
src/pages.json

@@ -103,6 +103,13 @@
             "navigationStyle": "default"
           }
         },
+        {
+          "path": "record-info/index",
+          "style": {
+            "navigationBarTitleText": "提交备案",
+            "navigationStyle": "default"
+          }
+        },
         {
           "path": "points-rule/index",
           "style": {

+ 197 - 19
src/pages/index/index.vue

@@ -43,10 +43,13 @@ import { onShow } from '@dcloudio/uni-app'
 import dayjs from 'dayjs'
 
 import {
+  getPopupNoticesApi,
   getQuizPltTestResultApi,
+  readPopupNoticeApi,
   resetPasswordApi,
   stopSixtyReminderApi,
 } from '@/services/modules/userInfo'
+import type { PopupNoticeItem, PopupNoticeType } from '@/services/modules/userInfo/type'
 
 import { useUserStore } from '@/stores/modules/user'
 
@@ -62,6 +65,14 @@ const isLoggedIn = computed(() => userStore.isLoggedIn)
 
 const waitApprove = ref('0')
 const onTheWay = ref('0')
+const isShowingPopupNotice = ref(false)
+
+const SUPPORTED_POPUP_NOTICE_TYPES: PopupNoticeType[] = [
+  'PENDING_SUBMIT',
+  'AUDIT_PASS',
+  'REVOKED',
+  'AUDIT_REJECT',
+]
 
 onShow(async () => {
   // 每次进入页面时刷新用户状态
@@ -104,11 +115,159 @@ onShow(async () => {
   }
 
   // 密码修改提醒
-  checkLastChangePassword()
+  const hasNavigatedToResetPassword = await checkLastChangePassword()
+  if (hasNavigatedToResetPassword) return
+
+  await requestPopupNoticesOnce()
 
   // 兜底处理
   uni.showTabBar()
 })
+
+const requestPopupNoticesOnce = async () => {
+  if (!userStore.hasRequestedPopupNotices) {
+    // 发起请求前即标记,避免首页 onShow 并发触发重复请求。
+    userStore.markPopupNoticesRequested()
+
+    try {
+      const res = await getPopupNoticesApi()
+      const notices = Array.isArray(res.data)
+        ? res.data.filter((item) => SUPPORTED_POPUP_NOTICE_TYPES.includes(item.noticeType))
+        : []
+
+      userStore.setPopupNotices(notices)
+    } catch (error) {
+      console.error('获取首页弹窗公告失败:', error)
+    }
+  }
+
+  showNextPopupNotice()
+}
+
+const showNextPopupNotice = () => {
+  if (isShowingPopupNotice.value) return
+
+  const notice = userStore.currentPopupNotice
+  if (!notice) return
+
+  isShowingPopupNotice.value = true
+
+  if (notice.noticeType === 'PENDING_SUBMIT') {
+    void showPendingSubmitNotice(notice)
+    return
+  }
+
+  void showTextNotice(notice)
+}
+
+const showPendingSubmitNotice = async (notice: PopupNoticeItem) => {
+  const confirmed = await openPopupNoticeModal({
+    title: '提示',
+    content: `【${notice.entName}】向您发起了备案邀请,请提交您的备案信息。`,
+    cancelText: '暂不处理',
+    confirmText: '去备案',
+  })
+
+  isShowingPopupNotice.value = false
+
+  if (confirmed === null) return
+
+  userStore.shiftPopupNotice()
+
+  if (confirmed) {
+    const entTaxCode = encodeURIComponent(notice.entTaxCode)
+    const entName = encodeURIComponent(notice.entName)
+
+    uni.navigateTo({
+      url: `/pages-mine/record-info/index?entTaxCode=${entTaxCode}&entName=${entName}`,
+      fail() {
+        scheduleNextPopupNotice()
+      },
+    })
+    return
+  }
+
+  scheduleNextPopupNotice()
+}
+
+const showTextNotice = async (notice: PopupNoticeItem) => {
+  const confirmed = await openPopupNoticeModal({
+    title: getTextNoticeTitle(notice),
+    content: getTextNoticeContent(notice),
+    showCancel: false,
+    confirmText: '确认',
+  })
+
+  if (!confirmed) {
+    isShowingPopupNotice.value = false
+    return
+  }
+
+  try {
+    if (notice.noticeId !== null) {
+      await readPopupNoticeApi(notice.noticeId)
+    }
+
+    userStore.shiftPopupNotice()
+    isShowingPopupNotice.value = false
+    scheduleNextPopupNotice()
+  } catch (error) {
+    isShowingPopupNotice.value = false
+    console.error('备案弹窗公告标记已读失败:', error)
+  }
+}
+
+const getTextNoticeTitle = (notice: PopupNoticeItem): string => {
+  if (notice.noticeType === 'AUDIT_REJECT') {
+    return `您在【${notice.entName}】的备案申请已被拒绝。`
+  }
+
+  return '提示'
+}
+
+const getTextNoticeContent = (notice: PopupNoticeItem): string => {
+  switch (notice.noticeType) {
+    case 'AUDIT_PASS':
+      return `您在【${notice.entName}】的备案申请已被通过。您将可以提交该药企需要备案资质的任务类型。`
+    case 'REVOKED':
+      return `您在【${notice.entName}】的备案已被撤销。您将无法继续提交需要备案资质的任务类型。`
+    case 'AUDIT_REJECT':
+      return `拒绝原因:${notice.rejectReason || '暂无'}`
+    default:
+      return ''
+  }
+}
+
+interface PopupNoticeModalOptions {
+  title: string
+  content: string
+  showCancel?: boolean
+  cancelText?: string
+  confirmText: string
+}
+
+const openPopupNoticeModal = (
+  options: PopupNoticeModalOptions
+): Promise<boolean | null> => {
+  return new Promise((resolve) => {
+    let result: boolean | null = null
+
+    uni.showModal({
+      ...options,
+      success(res) {
+        result = res.confirm
+      },
+      complete() {
+        resolve(result)
+      },
+    })
+  })
+}
+
+const scheduleNextPopupNotice = () => {
+  setTimeout(showNextPopupNotice, 0)
+}
+
 const ensureCanOperate = (): boolean => {
   if (!userStore.isLoggedIn) {
     uni.showToast({
@@ -244,32 +403,51 @@ const getPltQuizResult = async (): Promise<boolean> => {
 }
 
 // 是否修改过密码
-const checkLastChangePassword = async () => {
+const checkLastChangePassword = async (): Promise<boolean> => {
   const userInfo = userStore.currentUserInfo
-  if (!userInfo || !userInfo.latestChangePwdTime) return
+  if (!userInfo || !userInfo.latestChangePwdTime) return false
 
   const lastChangeTime = new Date(userInfo.latestChangePwdTime.replace(/-/g, '/')).getTime()
 
-  if (Number.isNaN(lastChangeTime)) return
+  if (Number.isNaN(lastChangeTime)) return false
 
   const isExpired = Date.now() - lastChangeTime >= 90 * 24 * 60 * 60 * 1000
 
-  if (!isExpired) return
+  if (!isExpired) return false
 
-  uni.showModal({
-    title: '提示',
-    content: '您的登录密码长时间未修改,为保障账号安全,建议您及时修改密码!',
-    confirmText: '去修改',
-    cancelText: '暂不修改',
-    success: async ({ confirm, cancel }) => {
-      if (confirm) {
-        uni.navigateTo({
-          url: '/pages/reset-password/index',
-        })
-      } else if (cancel) {
-        resetPasswordApi(userInfo.userId)
-      }
-    },
+  return new Promise((resolve) => {
+    uni.showModal({
+      title: '提示',
+      content: '您的登录密码长时间未修改,为保障账号安全,建议您及时修改密码!',
+      confirmText: '去修改',
+      cancelText: '暂不修改',
+      success: async ({ confirm, cancel }) => {
+        if (confirm) {
+          uni.navigateTo({
+            url: '/pages/reset-password/index',
+            success() {
+              resolve(true)
+            },
+            fail() {
+              resolve(false)
+            },
+          })
+        } else if (cancel) {
+          try {
+            await resetPasswordApi(userInfo.userId)
+          } catch (error) {
+            console.error('重置密码提醒状态更新失败:', error)
+          } finally {
+            resolve(false)
+          }
+        } else {
+          resolve(false)
+        }
+      },
+      fail() {
+        resolve(false)
+      },
+    })
   })
 }
 

+ 32 - 1
src/services/modules/userInfo/index.ts

@@ -1,5 +1,10 @@
 import http from '../../index'
-import type { QuizPltTestResultResponse } from './type'
+import type {
+  PopupNoticeItem,
+  QuizPltTestResultResponse,
+  UserFilingPendingDetailResponse,
+  UserFilingSubmitRequest,
+} from './type'
 
 export const stopSixtyReminderApi = () => {
   return http.post<boolean>('/admin/api/stop-sixtyyearsold-reminder')
@@ -9,6 +14,32 @@ export const getQuizPltTestResultApi = () => {
   return http.get<QuizPltTestResultResponse>('/admin/api/quiz/plt/test/result')
 }
 
+export const getPopupNoticesApi = () => {
+  return http.get<PopupNoticeItem[]>('/admin/user-filing/popup-notices', {
+    terminal: 'MINI',
+  })
+}
+
+export const readPopupNoticeApi = (id: number) => {
+  return http.put<unknown>(`/admin/user-filing/popup-notices/${id}/read`, undefined, {
+    loading: true,
+    loadingText: '处理中...',
+  })
+}
+
+export const getUserFilingPendingDetailApi = (entTaxCode: string) => {
+  return http.get<UserFilingPendingDetailResponse>('/admin/user-filing/pending-detail', {
+    entTaxCode,
+  })
+}
+
+export const submitUserFilingApi = (data: UserFilingSubmitRequest) => {
+  return http.post<unknown>('/admin/user-filing/submit', data, {
+    loading: true,
+    loadingText: '提交中...',
+  })
+}
+
 export const resetPasswordApi = (userId: string) => {
   return http.post<QuizPltTestResultResponse>(`/admin/user/reset-password?userId=${userId}`)
 }

+ 61 - 0
src/services/modules/userInfo/type.d.ts

@@ -3,6 +3,67 @@ export interface QuizPltTestResultResponse {
   testResults: TestResultItem[]
 }
 
+export type PopupNoticeType = 'PENDING_SUBMIT' | 'AUDIT_PASS' | 'AUDIT_REJECT' | 'REVOKED'
+
+export interface PopupNoticeItem {
+  entName: string
+  entTaxCode: string
+  filingRecordId: number
+  noticeId: number | null
+  noticeType: PopupNoticeType
+  rejectReason: string | null
+}
+
+export type FilingDrugId = string
+
+export interface FilingDrugItem {
+  drug_id: FilingDrugId
+  drug_name: string
+}
+
+export interface UserFilingRecord {
+  id: number
+  userId: number
+  realName: string
+  deptName: string
+  phone: string
+  idCardNumber: string
+  drugEntName: string
+  entTaxCode: string
+  filingNo: string | null
+  degree: string | null
+  majorCategory: string | null
+  province: string | null
+  provinceCode: string | null
+  city: string | null
+  cityCode: string | null
+  promotionDrugIds: FilingDrugId[] | null
+  status: string
+  rejectReason: string | null
+  submitTime: string | null
+  confirmTime: string | null
+  revokeTime: string | null
+}
+
+export interface UserFilingPendingDetailResponse {
+  drugEntId: string
+  drugEntName: string
+  optionalDrugs: FilingDrugItem[]
+  record: UserFilingRecord
+}
+
+export interface UserFilingSubmitRequest {
+  entTaxCode: string
+  filingNo: string
+  degree: string
+  majorCategory: string
+  province: string
+  provinceCode: string
+  city: string
+  cityCode: string
+  promotionDrugIds: FilingDrugId[]
+}
+
 export interface TestResultItem {
   expiryDate: string | null
   finalMark: number | null

+ 4 - 0
src/services/request/index.ts

@@ -108,6 +108,10 @@ const http = {
     return send<T>({ url, method: 'POST', data, ...options })
   },
 
+  put<T = any>(url: string, data?: any, options?: RequestOptions) {
+    return send<T>({ url, method: 'PUT', data, ...options })
+  },
+
   getRaw<T = any>(url: string, params?: any, options?: RequestOptions) {
     return send<T>({ url, method: 'GET', data: params, ...options }, true)
   },

+ 33 - 0
src/stores/modules/user.ts

@@ -9,6 +9,7 @@ import type {
   LoginRequest,
 } from '@/services/modules/login/type'
 import type { UserInfoItem } from '@/services/modules/login/userInfo'
+import type { PopupNoticeItem } from '@/services/modules/userInfo/type'
 
 import { pinia } from '@/stores/index'
 
@@ -25,6 +26,13 @@ interface UserState {
    * 不持久化,避免变成永久只展示一次
    */
   hasShownLivenessReminder: boolean
+
+  /**
+   * 首页弹窗公告:本次登录、当前企业是否已经请求过
+   * 不持久化,重新登录或切换企业后可再次请求
+   */
+  hasRequestedPopupNotices: boolean
+  popupNotices: PopupNoticeItem[]
 }
 
 export const useUserStore = defineStore('user', {
@@ -34,6 +42,8 @@ export const useUserStore = defineStore('user', {
     userInfoList: [],
     isBlacklisted: false,
     hasShownLivenessReminder: false,
+    hasRequestedPopupNotices: false,
+    popupNotices: [],
   }),
 
   getters: {
@@ -50,6 +60,10 @@ export const useUserStore = defineStore('user', {
       if (index == null) return undefined
       return state.userInfoList[index]
     },
+
+    currentPopupNotice(state): PopupNoticeItem | undefined {
+      return state.popupNotices[0]
+    },
   },
 
   actions: {
@@ -68,6 +82,7 @@ export const useUserStore = defineStore('user', {
 
       // 登录成功后立即重置,保证本次登录重新具备弹窗资格
       this.resetLivenessReminderShown()
+      this.resetPopupNotices()
 
       await this.getUserInfoByCode()
 
@@ -121,6 +136,7 @@ export const useUserStore = defineStore('user', {
 
       // 切换身份后,活体状态需要重新判断
       this.resetLivenessReminderShown()
+      this.resetPopupNotices()
     },
 
     markLivenessReminderShown() {
@@ -131,6 +147,23 @@ export const useUserStore = defineStore('user', {
       this.hasShownLivenessReminder = false
     },
 
+    markPopupNoticesRequested() {
+      this.hasRequestedPopupNotices = true
+    },
+
+    setPopupNotices(notices: PopupNoticeItem[]) {
+      this.popupNotices = notices
+    },
+
+    shiftPopupNotice() {
+      this.popupNotices.shift()
+    },
+
+    resetPopupNotices() {
+      this.hasRequestedPopupNotices = false
+      this.popupNotices = []
+    },
+
     logout() {
       this.$reset()
     },