|
@@ -15,9 +15,49 @@
|
|
|
placeholder="请输入备案号"
|
|
placeholder="请输入备案号"
|
|
|
align-right
|
|
align-right
|
|
|
clearable
|
|
clearable
|
|
|
|
|
+ :readonly="disabled"
|
|
|
/>
|
|
/>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
|
|
+ <view class="filing-section">
|
|
|
|
|
+ <text class="section-label">备案状态</text>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="filingList.length" class="filing-list">
|
|
|
|
|
+ <view v-for="item in filingList" :key="getFilingItemKey(item)" class="filing-item">
|
|
|
|
|
+ <text class="enterprise-name">{{ item.drugEntName || '--' }}</text>
|
|
|
|
|
+ <text class="filing-status" :class="getFilingStatusClass(item.status)">
|
|
|
|
|
+ {{ item.statusDesc || '--' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text v-else class="empty-text">暂无备案信息</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="promotion-section">
|
|
|
|
|
+ <text class="section-label">推广产品</text>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="filingList.length" class="promotion-list">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(item, index) in filingList"
|
|
|
|
|
+ :key="`promotion-${getFilingItemKey(item)}`"
|
|
|
|
|
+ class="promotion-item"
|
|
|
|
|
+ @click="openPromotionDrugPicker(index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="enterprise-name">{{ item.drugEntName || '--' }}</text>
|
|
|
|
|
+ <view class="promotion-value-wrapper">
|
|
|
|
|
+ <text
|
|
|
|
|
+ class="promotion-value"
|
|
|
|
|
+ :class="{ 'promotion-value--placeholder': !item.promotionDrugs?.length }"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ getPromotionDrugDisplayText(item) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <wd-icon name="arrow-right" size="30rpx" color="#a9afb8" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text v-else class="empty-text">暂无可选择的推广产品</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
<view class="certificate-section">
|
|
<view class="certificate-section">
|
|
|
<text class="section-label">资质证书</text>
|
|
<text class="section-label">资质证书</text>
|
|
|
|
|
|
|
@@ -46,13 +86,33 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <wd-select-picker
|
|
|
|
|
+ v-model="selectedPromotionDrugIds"
|
|
|
|
|
+ v-model:visible="promotionDrugPickerVisible"
|
|
|
|
|
+ :title="promotionDrugPickerTitle"
|
|
|
|
|
+ :columns="currentPromotionDrugOptions"
|
|
|
|
|
+ :loading="promotionDrugLoading"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ filter-placeholder="搜索药品"
|
|
|
|
|
+ root-portal
|
|
|
|
|
+ @confirm="confirmPromotionDrugs"
|
|
|
|
|
+ @close="resetPromotionDrugPicker"
|
|
|
|
|
+ />
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { computed, ref } from 'vue'
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
-import type { PersonalBusinessCardResponse } from '@/services/modules/mine/personalCard/type'
|
|
|
|
|
|
|
+import { listPromotionDrugByEntApi } from '@/services/modules/mine/personalCard'
|
|
|
|
|
+import type {
|
|
|
|
|
+ PersonalBusinessCardResponse,
|
|
|
|
|
+ PersonalCardFilingItem,
|
|
|
|
|
+ PromotionDrugApiItem,
|
|
|
|
|
+ PromotionDrugItem,
|
|
|
|
|
+ PromotionDrugListResponse,
|
|
|
|
|
+} from '@/services/modules/mine/personalCard/type'
|
|
|
|
|
|
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
import { useUserStore } from '@/stores/modules/user'
|
|
|
|
|
|
|
@@ -65,23 +125,51 @@ interface UploadResponse {
|
|
|
} | null
|
|
} | null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface ProfessionalSkillFields {
|
|
|
|
|
- qualificationRecordNumber?: string | null
|
|
|
|
|
- qualificationsUrl?: string[] | null
|
|
|
|
|
|
|
+interface PromotionDrugOption extends PromotionDrugItem {
|
|
|
|
|
+ label: string
|
|
|
|
|
+ value: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type Model = PersonalBusinessCardResponse & ProfessionalSkillFields
|
|
|
|
|
|
|
+interface SelectPickerConfirmEvent {
|
|
|
|
|
+ value: Array<string | number | boolean>
|
|
|
|
|
+ selectedItems: PromotionDrugOption[]
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-withDefaults(defineProps<{ title?: string; maxCount?: number }>(), {
|
|
|
|
|
|
|
+withDefaults(defineProps<{ title?: string; maxCount?: number; disabled?: boolean }>(), {
|
|
|
title: '专业技能',
|
|
title: '专业技能',
|
|
|
maxCount: 9,
|
|
maxCount: 9,
|
|
|
|
|
+ disabled: false,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const model = defineModel<Model>({ required: true })
|
|
|
|
|
|
|
+const model = defineModel<PersonalBusinessCardResponse>({ required: true })
|
|
|
|
|
|
|
|
const userStore = useUserStore()
|
|
const userStore = useUserStore()
|
|
|
const uploading = ref(false)
|
|
const uploading = ref(false)
|
|
|
const baseUrl = (import.meta.env.VITE_BASE_API as string).replace(/\/$/, '')
|
|
const baseUrl = (import.meta.env.VITE_BASE_API as string).replace(/\/$/, '')
|
|
|
|
|
+const promotionDrugPickerVisible = ref(false)
|
|
|
|
|
+const promotionDrugLoading = ref(false)
|
|
|
|
|
+const currentFilingIndex = ref(-1)
|
|
|
|
|
+const currentDrugEnterpriseName = ref('')
|
|
|
|
|
+const currentPromotionDrugOptions = ref<PromotionDrugOption[]>([])
|
|
|
|
|
+const selectedPromotionDrugIds = ref<string[]>([])
|
|
|
|
|
+const promotionDrugOptionCache = new Map<string, PromotionDrugItem[]>()
|
|
|
|
|
+let promotionDrugLoadToken = 0
|
|
|
|
|
+
|
|
|
|
|
+const filingStatusClassMap: Record<string, string> = {
|
|
|
|
|
+ NOT_FILED: 'filing-status--not-filed',
|
|
|
|
|
+ APPLIED: 'filing-status--applied',
|
|
|
|
|
+ SUBMITTED: 'filing-status--submitted',
|
|
|
|
|
+ CONFIRMED: 'filing-status--confirmed',
|
|
|
|
|
+ REJECTED: 'filing-status--rejected',
|
|
|
|
|
+ REVOKED: 'filing-status--revoked',
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const filingList = computed<PersonalCardFilingItem[]>(() => model.value.filingList ?? [])
|
|
|
|
|
+const promotionDrugPickerTitle = computed(() =>
|
|
|
|
|
+ currentDrugEnterpriseName.value
|
|
|
|
|
+ ? `选择推广药品(${currentDrugEnterpriseName.value})`
|
|
|
|
|
+ : '选择推广药品'
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
const certificateList = computed<string[]>({
|
|
const certificateList = computed<string[]>({
|
|
|
get: () => model.value.qualificationsUrl ?? [],
|
|
get: () => model.value.qualificationsUrl ?? [],
|
|
@@ -186,6 +274,161 @@ function previewCertificate(index: number) {
|
|
|
function removeCertificate(index: number) {
|
|
function removeCertificate(index: number) {
|
|
|
certificateList.value = certificateList.value.filter((_, i) => i !== index)
|
|
certificateList.value = certificateList.value.filter((_, i) => i !== index)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+async function openPromotionDrugPicker(index: number) {
|
|
|
|
|
+ const filingItem = filingList.value[index]
|
|
|
|
|
+ if (!filingItem) {
|
|
|
|
|
+ uni.showToast({ title: '当前备案数据不存在', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const drugEntId = getDrugEntId(filingItem)
|
|
|
|
|
+ if (!drugEntId) {
|
|
|
|
|
+ uni.showToast({ title: '当前备案数据缺少药企ID', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectedDrugs = normalizePromotionDrugs(filingItem.promotionDrugs)
|
|
|
|
|
+ const currentToken = ++promotionDrugLoadToken
|
|
|
|
|
+
|
|
|
|
|
+ currentFilingIndex.value = index
|
|
|
|
|
+ currentDrugEnterpriseName.value = filingItem.drugEntName ?? ''
|
|
|
|
|
+ selectedPromotionDrugIds.value = selectedDrugs.map((item) => item.drugId)
|
|
|
|
|
+ currentPromotionDrugOptions.value = toPromotionDrugOptions(selectedDrugs)
|
|
|
|
|
+ promotionDrugPickerVisible.value = true
|
|
|
|
|
+ promotionDrugLoading.value = true
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const cacheKey = String(drugEntId)
|
|
|
|
|
+ let drugOptions = promotionDrugOptionCache.get(cacheKey)
|
|
|
|
|
+
|
|
|
|
|
+ if (!drugOptions) {
|
|
|
|
|
+ const response = await listPromotionDrugByEntApi(drugEntId)
|
|
|
|
|
+ if (currentToken !== promotionDrugLoadToken) return
|
|
|
|
|
+
|
|
|
|
|
+ drugOptions = normalizePromotionDrugs(extractResponseList(response.data))
|
|
|
|
|
+ promotionDrugOptionCache.set(cacheKey, drugOptions)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ currentPromotionDrugOptions.value = toPromotionDrugOptions(
|
|
|
|
|
+ mergePromotionDrugOptions(drugOptions, selectedDrugs)
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[ProfessionalSkillCard] load promotion drugs failed:', error)
|
|
|
|
|
+ if (currentToken === promotionDrugLoadToken) {
|
|
|
|
|
+ uni.showToast({ title: '获取推广药品失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (currentToken === promotionDrugLoadToken) {
|
|
|
|
|
+ promotionDrugLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function confirmPromotionDrugs(event: SelectPickerConfirmEvent) {
|
|
|
|
|
+ const filingItem = filingList.value[currentFilingIndex.value]
|
|
|
|
|
+ if (!filingItem) return
|
|
|
|
|
+
|
|
|
|
|
+ const selectedIdSet = new Set(event.value.map(String))
|
|
|
|
|
+ const promotionDrugs = currentPromotionDrugOptions.value
|
|
|
|
|
+ .filter((item) => selectedIdSet.has(item.value))
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ drugId: item.drugId,
|
|
|
|
|
+ drugName: item.drugName,
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ const updatedFilingList = [...filingList.value]
|
|
|
|
|
+ updatedFilingList[currentFilingIndex.value] = {
|
|
|
|
|
+ ...filingItem,
|
|
|
|
|
+ promotionDrugs,
|
|
|
|
|
+ }
|
|
|
|
|
+ model.value.filingList = updatedFilingList
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetPromotionDrugPicker() {
|
|
|
|
|
+ promotionDrugLoadToken += 1
|
|
|
|
|
+ promotionDrugLoading.value = false
|
|
|
|
|
+ currentFilingIndex.value = -1
|
|
|
|
|
+ currentDrugEnterpriseName.value = ''
|
|
|
|
|
+ currentPromotionDrugOptions.value = []
|
|
|
|
|
+ selectedPromotionDrugIds.value = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getDrugEntId(item: PersonalCardFilingItem): string {
|
|
|
|
|
+ const value = item.drugEntId ?? item.entId ?? item.drugEnterpriseId
|
|
|
|
|
+ return value === undefined || value === null ? '' : String(value)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractResponseList(data: PromotionDrugListResponse): PromotionDrugApiItem[] {
|
|
|
|
|
+ if (Array.isArray(data)) return data
|
|
|
|
|
+ if (Array.isArray(data.records)) return data.records
|
|
|
|
|
+ if (Array.isArray(data.list)) return data.list
|
|
|
|
|
+ return []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function normalizePromotionDrugs(
|
|
|
|
|
+ list: Array<PromotionDrugApiItem | PromotionDrugItem> | null | undefined
|
|
|
|
|
+): PromotionDrugItem[] {
|
|
|
|
|
+ if (!Array.isArray(list)) return []
|
|
|
|
|
+
|
|
|
|
|
+ const seen = new Set<string>()
|
|
|
|
|
+ const result: PromotionDrugItem[] = []
|
|
|
|
|
+
|
|
|
|
|
+ list.forEach((item) => {
|
|
|
|
|
+ if (!item) return
|
|
|
|
|
+
|
|
|
|
|
+ const apiItem = item as PromotionDrugApiItem
|
|
|
|
|
+ const sourceDrugId = apiItem.drugId ?? apiItem.drug_id ?? apiItem.id
|
|
|
|
|
+ if (sourceDrugId === undefined || sourceDrugId === null || sourceDrugId === '') return
|
|
|
|
|
+
|
|
|
|
|
+ const drugId = String(sourceDrugId)
|
|
|
|
|
+ if (seen.has(drugId)) return
|
|
|
|
|
+
|
|
|
|
|
+ seen.add(drugId)
|
|
|
|
|
+ result.push({
|
|
|
|
|
+ drugId,
|
|
|
|
|
+ drugName: apiItem.drugName ?? apiItem.drug_name ?? apiItem.name ?? '',
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function mergePromotionDrugOptions(
|
|
|
|
|
+ apiOptions: PromotionDrugItem[],
|
|
|
|
|
+ selectedOptions: PromotionDrugItem[]
|
|
|
|
|
+): PromotionDrugItem[] {
|
|
|
|
|
+ return normalizePromotionDrugs([...apiOptions, ...selectedOptions])
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toPromotionDrugOptions(list: PromotionDrugItem[]): PromotionDrugOption[] {
|
|
|
|
|
+ return list.map((item) => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ label: item.drugName || `药品 ${item.drugId}`,
|
|
|
|
|
+ value: item.drugId,
|
|
|
|
|
+ }))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getPromotionDrugDisplayText(item: PersonalCardFilingItem): string {
|
|
|
|
|
+ const drugs = normalizePromotionDrugs(item.promotionDrugs)
|
|
|
|
|
+ if (!drugs.length) return '请选择推广产品'
|
|
|
|
|
+
|
|
|
|
|
+ const drugNames = drugs.map((drug) => drug.drugName).filter(Boolean)
|
|
|
|
|
+ if (!drugNames.length) return `已选择${drugs.length}个药品`
|
|
|
|
|
+ if (drugNames.length <= 2) return drugNames.join('、')
|
|
|
|
|
+
|
|
|
|
|
+ return `${drugNames.slice(0, 2).join('、')} 等${drugNames.length}个`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFilingItemKey(item: PersonalCardFilingItem): string {
|
|
|
|
|
+ return String(item.recordId ?? item.entTaxCode ?? item.drugEntId ?? item.drugEntName ?? '')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFilingStatusClass(status?: string | null): string {
|
|
|
|
|
+ return status
|
|
|
|
|
+ ? filingStatusClassMap[status] || 'filing-status--default'
|
|
|
|
|
+ : 'filing-status--default'
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
@@ -250,6 +493,96 @@ function removeCertificate(index: number) {
|
|
|
padding: 22rpx 24rpx 28rpx;
|
|
padding: 22rpx 24rpx 28rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.filing-section,
|
|
|
|
|
+.promotion-section {
|
|
|
|
|
+ padding: 22rpx 24rpx;
|
|
|
|
|
+ border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-list,
|
|
|
|
|
+.promotion-list {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-item,
|
|
|
|
|
+.promotion-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ min-height: 76rpx;
|
|
|
|
|
+ gap: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.enterprise-name {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #4f5661;
|
|
|
|
|
+ font-size: 27rpx;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-status {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding: 7rpx 16rpx;
|
|
|
|
|
+ border-radius: 999rpx;
|
|
|
|
|
+ background: #f2f4f7;
|
|
|
|
|
+ color: #7c8491;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-status--not-filed,
|
|
|
|
|
+.filing-status--rejected,
|
|
|
|
|
+.filing-status--revoked {
|
|
|
|
|
+ background: #fff1f0;
|
|
|
|
|
+ color: #e85c55;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-status--applied,
|
|
|
|
|
+.filing-status--submitted {
|
|
|
|
|
+ background: #fff7e8;
|
|
|
|
|
+ color: #e99928;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filing-status--confirmed {
|
|
|
|
|
+ background: #eaf8ef;
|
|
|
|
|
+ color: #35a765;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.promotion-item {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.promotion-value-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.promotion-value {
|
|
|
|
|
+ max-width: 360rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #30343b;
|
|
|
|
|
+ font-size: 27rpx;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.promotion-value--placeholder,
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ color: #a9afb8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ padding: 16rpx 0 6rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.section-label {
|
|
.section-label {
|
|
|
display: block;
|
|
display: block;
|
|
|
margin-bottom: 24rpx;
|
|
margin-bottom: 24rpx;
|