detail.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <template>
  2. <view class="package-detail">
  3. <view class="package-detail__header">
  4. <view class="package-detail__title">
  5. {{ packageDetail?.scorePackageName || '积分包详情' }}
  6. </view>
  7. <view class="package-detail__sub">编号:{{ packageDetail?.packageSn || '-' }}</view>
  8. </view>
  9. <view class="detail-card">
  10. <view class="detail-card__title">基础信息</view>
  11. <view
  12. v-for="item in detailRows"
  13. :key="item.label"
  14. class="detail-row"
  15. :class="{ 'detail-row--long': item.long }"
  16. >
  17. <text class="detail-row__label">{{ item.label }}</text>
  18. <text class="detail-row__value">{{ item.value }}</text>
  19. </view>
  20. </view>
  21. <view v-if="ruleSections.length" class="rule-card">
  22. <view class="rule-card__title">积分规则</view>
  23. <view v-for="section in ruleSections" :key="section.title" class="rule-section">
  24. <view class="rule-section__title">{{ section.title }}</view>
  25. <view v-for="item in section.items" :key="item.id" class="rule-item">
  26. <text class="rule-item__name">{{ item.taskTypeName }}</text>
  27. <text class="rule-item__score">+{{ item.score }} 积分</text>
  28. </view>
  29. </view>
  30. </view>
  31. <view v-if="!loading && !packageDetail" class="empty">暂无详情数据</view>
  32. <view v-if="showExportButton" class="bottom-action">
  33. <wd-button block type="primary" @click="openExportPopup">发送报告</wd-button>
  34. </view>
  35. <wd-popup
  36. v-model="exportPopupVisible"
  37. position="bottom"
  38. custom-style="border-radius: 32rpx 32rpx 0 0;"
  39. @close="resetExportError"
  40. >
  41. <view class="export-popup">
  42. <view class="export-popup__close" @click="exportPopupVisible = false">×</view>
  43. <view class="export-popup__header">
  44. <view class="export-popup__title">报告发送</view>
  45. <view class="export-popup__desc">报告将发送至填写的邮箱,请确认邮箱地址有效。</view>
  46. </view>
  47. <view class="export-popup__body">
  48. <view
  49. class="export-popup__input-wrap"
  50. :class="{ 'export-popup__input-wrap--error': emailError }"
  51. >
  52. <input
  53. v-model="email"
  54. class="export-popup__input"
  55. placeholder="请输入邮箱"
  56. placeholder-class="export-popup__placeholder"
  57. type="text"
  58. confirm-type="send"
  59. cursor-spacing="120"
  60. :focus="emailInputFocus"
  61. :adjust-position="true"
  62. @focus="emailError = ''"
  63. @confirm="submitExportReport"
  64. />
  65. </view>
  66. <view v-if="emailError" class="export-popup__error">
  67. {{ emailError }}
  68. </view>
  69. </view>
  70. <view class="export-popup__footer">
  71. <wd-button block type="primary" :loading="exportLoading" @click="submitExportReport">
  72. 提交
  73. </wd-button>
  74. </view>
  75. </view>
  76. </wd-popup>
  77. </view>
  78. </template>
  79. <script setup lang="ts">
  80. import { computed, nextTick, ref } from 'vue'
  81. import { onLoad } from '@dcloudio/uni-app'
  82. import {
  83. exportZbInfoFormWeiNewApi,
  84. getPackageCanTaskListApi,
  85. getScorePackagePageByIdApi,
  86. } from '@/services/modules/task/package/index'
  87. import { useUserStore } from '@/stores/modules/user'
  88. const userStore = useUserStore()
  89. const id = ref('')
  90. const loading = ref(false)
  91. const exportLoading = ref(false)
  92. const exportPopupVisible = ref(false)
  93. const emailInputFocus = ref(false)
  94. const emailError = ref('')
  95. const email = ref('')
  96. const packageDetail = ref(null)
  97. const ruleSections = ref([])
  98. const EMPTY_TEXT = '无'
  99. const roles = computed(() => userStore.currentUserInfo?.roles || [])
  100. const showExportButton = computed(() => {
  101. return roles.value.includes(6) && packageDetail.value?.exportFlag === '1'
  102. })
  103. const formatDrugList = (val) => {
  104. if (!val) return EMPTY_TEXT
  105. if (Array.isArray(val)) return val.join('、') || EMPTY_TEXT
  106. return (
  107. String(val)
  108. .replace(/^\[|\]$/g, '')
  109. .replace(/"/g, '')
  110. .split(',')
  111. .filter(Boolean)
  112. .join('、') || EMPTY_TEXT
  113. )
  114. }
  115. const areaName = computed(() => {
  116. const area = packageDetail.value?.area
  117. if (!Array.isArray(area) || area.length === 0) return EMPTY_TEXT
  118. return (
  119. area
  120. .map((item) => `${item.province ?? ''}${item.city ?? ''}${item.area ?? ''}`)
  121. .filter(Boolean)
  122. .join(',') || EMPTY_TEXT
  123. )
  124. })
  125. const quizNames = computed(() => {
  126. const list = packageDetail.value?.quizRelations
  127. if (!Array.isArray(list) || list.length === 0) return EMPTY_TEXT
  128. return (
  129. list
  130. .map((item) => item.title)
  131. .filter(Boolean)
  132. .join(',') || EMPTY_TEXT
  133. )
  134. })
  135. const detailRows = computed(() => {
  136. const d = packageDetail.value
  137. return [
  138. { label: '截止日期', value: d?.endTimeValue || EMPTY_TEXT },
  139. { label: '接单对象类型', value: d?.typeidName || EMPTY_TEXT },
  140. {
  141. label: '接单对象',
  142. value: Array.isArray(d?.usernameList)
  143. ? d.usernameList.join(',')
  144. : d?.usernameList || EMPTY_TEXT,
  145. },
  146. { label: '结算渠道', value: d?.subjectLocationName || EMPTY_TEXT },
  147. { label: '关联服务企业', value: d?.relatedServiceName || EMPTY_TEXT },
  148. { label: '关联任务积分包', value: d?.relationScoreName || EMPTY_TEXT },
  149. { label: '可分配积分值', value: d?.kfpjf || EMPTY_TEXT },
  150. { label: '积分包值', value: d?.score || EMPTY_TEXT },
  151. { label: '接单对象范围', value: d?.packageUserScopeName || EMPTY_TEXT },
  152. { label: '推广药品', value: formatDrugList(d?.drugtableName), long: true },
  153. { label: '推广区域', value: areaName.value },
  154. {
  155. label: '派工范围',
  156. value: Array.isArray(d?.dispatchUserScopeName)
  157. ? d.dispatchUserScopeName.join(',')
  158. : d?.dispatchUserScopeName || EMPTY_TEXT,
  159. },
  160. { label: '测试试卷', value: quizNames.value },
  161. { label: '描述', value: d?.description || EMPTY_TEXT, long: true },
  162. ]
  163. })
  164. onLoad((query) => {
  165. const packageId = String(query?.id || '')
  166. if (!packageId) {
  167. uni.showToast({ title: '缺少ID', icon: 'none' })
  168. return
  169. }
  170. id.value = packageId
  171. initPage()
  172. })
  173. const initPage = async () => {
  174. loading.value = true
  175. uni.showLoading({ title: '加载中' })
  176. try {
  177. await Promise.all([getScorePackagePageById(), getPackageCanTaskList()])
  178. } finally {
  179. loading.value = false
  180. uni.hideLoading()
  181. }
  182. }
  183. const getScorePackagePageById = async () => {
  184. const res = await getScorePackagePageByIdApi(id.value)
  185. packageDetail.value = res.data || null
  186. email.value = res.data?.email || ''
  187. }
  188. const getPackageCanTaskList = async () => {
  189. const res = await getPackageCanTaskListApi(id.value)
  190. ruleSections.value = normalizeRuleSections(res.data)
  191. }
  192. const openExportPopup = async () => {
  193. resetExportError()
  194. exportPopupVisible.value = true
  195. await nextTick()
  196. setTimeout(() => {
  197. emailInputFocus.value = true
  198. }, 300)
  199. }
  200. const resetExportError = () => {
  201. emailError.value = ''
  202. emailInputFocus.value = false
  203. }
  204. const submitExportReport = async () => {
  205. const value = email.value.trim()
  206. if (!validateEmail(value)) {
  207. emailError.value = '请输入正确的邮箱地址'
  208. emailInputFocus.value = true
  209. return
  210. }
  211. exportLoading.value = true
  212. try {
  213. await exportZbInfoFormWeiNewApi({
  214. id: id.value,
  215. email: value,
  216. })
  217. exportPopupVisible.value = false
  218. uni.showToast({
  219. title: '报告已发送',
  220. icon: 'none',
  221. })
  222. } finally {
  223. exportLoading.value = false
  224. }
  225. }
  226. const validateEmail = (value) => {
  227. return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
  228. }
  229. const normalizeRuleSections = (raw) => {
  230. if (!raw) return []
  231. if (Array.isArray(raw)) {
  232. return raw.length ? [{ title: '积分规则', items: raw }] : []
  233. }
  234. if (Object.prototype.toString.call(raw) === '[object Object]') {
  235. return Object.entries(raw)
  236. .filter(([, v]) => Array.isArray(v) && v.length > 0)
  237. .map(([title, items]) => ({ title, items }))
  238. }
  239. return []
  240. }
  241. </script>
  242. <style lang="scss" scoped>
  243. .package-detail {
  244. min-height: 100vh;
  245. padding: 24rpx 24rpx 164rpx;
  246. background: #f5f7fa;
  247. .package-detail__header {
  248. padding: 36rpx 32rpx;
  249. margin-bottom: 24rpx;
  250. color: #ffffff;
  251. background: linear-gradient(135deg, #3fa3f1 0%, #2176e8 100%);
  252. border-radius: 24rpx;
  253. box-shadow: 0 12rpx 28rpx rgba(47, 134, 230, 0.24);
  254. }
  255. .package-detail__title {
  256. font-size: 38rpx;
  257. font-weight: 700;
  258. line-height: 52rpx;
  259. }
  260. .package-detail__sub {
  261. margin-top: 10rpx;
  262. font-size: 26rpx;
  263. line-height: 36rpx;
  264. opacity: 0.8;
  265. }
  266. .detail-card,
  267. .rule-card {
  268. margin-bottom: 24rpx;
  269. overflow: hidden;
  270. background: #ffffff;
  271. border-radius: 20rpx;
  272. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
  273. }
  274. .detail-card__title,
  275. .rule-card__title {
  276. padding: 28rpx 32rpx 18rpx;
  277. color: #1f2937;
  278. font-size: 32rpx;
  279. font-weight: 700;
  280. line-height: 44rpx;
  281. border-bottom: 1rpx solid #eef0f3;
  282. }
  283. .detail-row {
  284. display: flex;
  285. align-items: flex-start;
  286. gap: 24rpx;
  287. padding: 26rpx 32rpx;
  288. border-bottom: 1rpx solid #f1f2f4;
  289. }
  290. .detail-row:last-child {
  291. border-bottom: none;
  292. }
  293. .detail-row__label {
  294. flex-shrink: 0;
  295. width: 260rpx;
  296. color: #6b7280;
  297. font-size: 28rpx;
  298. line-height: 40rpx;
  299. white-space: nowrap;
  300. }
  301. .detail-row__value {
  302. flex: 1;
  303. min-width: 0;
  304. color: #111827;
  305. font-size: 28rpx;
  306. line-height: 40rpx;
  307. text-align: right;
  308. word-break: break-all;
  309. }
  310. .detail-row--long {
  311. display: block;
  312. }
  313. .detail-row--long .detail-row__label {
  314. display: block;
  315. width: auto;
  316. margin-bottom: 16rpx;
  317. white-space: nowrap;
  318. }
  319. .detail-row--long .detail-row__value {
  320. display: block;
  321. padding: 20rpx 24rpx;
  322. color: #374151;
  323. font-size: 28rpx;
  324. line-height: 44rpx;
  325. text-align: left;
  326. word-break: break-all;
  327. background: #f8fafc;
  328. border-radius: 12rpx;
  329. }
  330. .rule-section {
  331. padding: 28rpx 32rpx;
  332. }
  333. .rule-section + .rule-section {
  334. border-top: 1rpx dashed #e5e7eb;
  335. }
  336. .rule-section__title {
  337. position: relative;
  338. padding-left: 18rpx;
  339. margin-bottom: 20rpx;
  340. color: #111827;
  341. font-size: 30rpx;
  342. font-weight: 700;
  343. line-height: 42rpx;
  344. }
  345. .rule-section__title::before {
  346. content: '';
  347. position: absolute;
  348. top: 8rpx;
  349. left: 0;
  350. width: 6rpx;
  351. height: 28rpx;
  352. background: #3fa3f1;
  353. border-radius: 999rpx;
  354. }
  355. .rule-item {
  356. display: flex;
  357. align-items: center;
  358. justify-content: space-between;
  359. gap: 24rpx;
  360. padding: 18rpx 0;
  361. }
  362. .rule-item__name {
  363. flex: 1;
  364. min-width: 0;
  365. color: #374151;
  366. font-size: 28rpx;
  367. line-height: 40rpx;
  368. word-break: break-all;
  369. }
  370. .rule-item__score {
  371. flex-shrink: 0;
  372. padding: 8rpx 18rpx;
  373. color: #2f86e6;
  374. font-size: 26rpx;
  375. font-weight: 600;
  376. line-height: 34rpx;
  377. background: rgba(63, 163, 241, 0.1);
  378. border-radius: 999rpx;
  379. }
  380. .bottom-action {
  381. position: fixed;
  382. right: 0;
  383. bottom: 0;
  384. left: 0;
  385. z-index: 0;
  386. padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
  387. background: #ffffff;
  388. box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.08);
  389. }
  390. .export-popup {
  391. position: relative;
  392. padding: 40rpx 32rpx 24rpx;
  393. background: #ffffff;
  394. }
  395. .export-popup__close {
  396. position: absolute;
  397. top: 28rpx;
  398. right: 32rpx;
  399. width: 48rpx;
  400. height: 48rpx;
  401. color: #9ca3af;
  402. font-size: 44rpx;
  403. line-height: 44rpx;
  404. text-align: center;
  405. }
  406. .export-popup__header {
  407. padding-right: 64rpx;
  408. margin-bottom: 34rpx;
  409. }
  410. .export-popup__title {
  411. color: #111827;
  412. font-size: 38rpx;
  413. font-weight: 700;
  414. line-height: 52rpx;
  415. }
  416. .export-popup__desc {
  417. margin-top: 10rpx;
  418. color: #6b7280;
  419. font-size: 26rpx;
  420. line-height: 38rpx;
  421. }
  422. .export-popup__body {
  423. margin-bottom: 40rpx;
  424. }
  425. .export-popup__input-wrap {
  426. display: flex;
  427. align-items: center;
  428. height: 88rpx;
  429. padding: 0 24rpx;
  430. background: #ffffff;
  431. border: 2rpx solid #e5e7eb;
  432. border-radius: 16rpx;
  433. box-sizing: border-box;
  434. }
  435. .export-popup__input-wrap--error {
  436. border-color: #ef4444;
  437. background: #fff7f7;
  438. }
  439. .export-popup__input {
  440. width: 100%;
  441. height: 88rpx;
  442. color: #111827;
  443. font-size: 30rpx;
  444. line-height: 88rpx;
  445. }
  446. .export-popup__placeholder {
  447. color: #9ca3af;
  448. font-size: 30rpx;
  449. }
  450. .export-popup__error {
  451. margin-top: 12rpx;
  452. color: #ef4444;
  453. font-size: 24rpx;
  454. line-height: 34rpx;
  455. }
  456. .export-popup__footer {
  457. padding-bottom: max(20rpx, env(safe-area-inset-bottom));
  458. }
  459. .empty {
  460. padding: 80rpx 24rpx;
  461. color: #9ca3af;
  462. font-size: 28rpx;
  463. line-height: 40rpx;
  464. text-align: center;
  465. }
  466. }
  467. </style>