index.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <template>
  2. <view class="agreement-container">
  3. <view class="agreement-list">
  4. <view
  5. v-for="item in staticAgreementList"
  6. :key="item.path"
  7. class="agreement-card"
  8. hover-class="agreement-card--active"
  9. @click="navigateToAgreementPage(item.path)"
  10. >
  11. <text class="agreement-card__title">《{{ item.name }}》</text>
  12. <text class="agreement-card__arrow">›</text>
  13. </view>
  14. <view
  15. v-for="item in certSuccessList"
  16. :key="item.templateUrl"
  17. class="agreement-card"
  18. hover-class="agreement-card--active"
  19. @click="openPdfFile(item)"
  20. >
  21. <text class="agreement-card__title">《{{ item.name }}》</text>
  22. <text class="agreement-card__arrow">›</text>
  23. </view>
  24. </view>
  25. </view>
  26. </template>
  27. <script setup lang="ts">
  28. import { computed, ref } from 'vue'
  29. import { onLoad } from '@dcloudio/uni-app'
  30. import { getSubjectLocationAgreementApi } from '@/services/modules/mine/common'
  31. import type { agreementInfosItem } from '@/services/modules/mine/type'
  32. import { useUserStore } from '@/stores/modules/user'
  33. const AGREEMENT_PAGE_BASE = '/pages-mine/agreement'
  34. const CERT_SUCCESS_STATUS = 'CERT'
  35. const userStore = useUserStore()
  36. const currentUserInfo = computed(() => userStore.currentUserInfo)
  37. const certSuccessList = ref<agreementInfosItem[]>([])
  38. const showHonestAgreement = computed(() => hasSignedAgreement('HONEST_AGREEMENT'))
  39. const showHonestAgreementV2 = computed(() => hasSignedAgreement('HONEST_AGREEMENT_V2'))
  40. const staticAgreementList = computed(() => {
  41. const list = [
  42. {
  43. name: '要易云平台用户协议',
  44. path: 'platform-user-agreement',
  45. },
  46. {
  47. name: '隐私权政策',
  48. path: 'privacy-policy',
  49. },
  50. {
  51. name: '个人信息使用授权书',
  52. path: 'personal-info-authorization',
  53. },
  54. ]
  55. if (showHonestAgreement.value) {
  56. list.push({
  57. name: '廉洁承诺书',
  58. path: 'integrity-commitment',
  59. })
  60. }
  61. if (showHonestAgreementV2.value) {
  62. list.push({
  63. name: '承诺书2.0',
  64. path: 'integrity-commitment-v2',
  65. })
  66. }
  67. return list
  68. })
  69. onLoad(() => {
  70. void getCertSuccessList()
  71. })
  72. async function getCertSuccessList(): Promise<void> {
  73. const subjectLocations = getCertifiedSubjectLocations()
  74. if (!subjectLocations.length) {
  75. certSuccessList.value = []
  76. return
  77. }
  78. uni.showLoading({
  79. title: '加载中...',
  80. mask: true,
  81. })
  82. let hasError = false
  83. try {
  84. const agreementInfoGroups = await Promise.all(
  85. subjectLocations.map(async (subjectLocation) => {
  86. try {
  87. const res = await getSubjectLocationAgreementApi({
  88. subjectLocation,
  89. })
  90. return normalizeAgreementInfos(res.data?.agreementInfos)
  91. } catch (error) {
  92. hasError = true
  93. console.log('getSubjectLocationAgreementApi error', error)
  94. return []
  95. }
  96. })
  97. )
  98. certSuccessList.value = dedupeAgreementInfos(agreementInfoGroups.flat())
  99. if (hasError) {
  100. showToast('部分协议加载失败')
  101. }
  102. } finally {
  103. uni.hideLoading()
  104. }
  105. }
  106. function getCertifiedSubjectLocations(): string[] {
  107. const certList = currentUserInfo.value?.certList ?? []
  108. const subjectLocations = certList
  109. .filter((item) => item?.commonCertStatus === CERT_SUCCESS_STATUS)
  110. .map((item) => item?.subjectLocation?.trim())
  111. .filter((subjectLocation): subjectLocation is string => Boolean(subjectLocation))
  112. return Array.from(new Set(subjectLocations))
  113. }
  114. function normalizeAgreementInfos(list: agreementInfosItem[] = []): agreementInfosItem[] {
  115. return list
  116. .filter((item) => Boolean(item?.templateUrl?.trim()))
  117. .map((item) => ({
  118. name: item.name?.trim() || '协议',
  119. templateUrl: item.templateUrl.trim(),
  120. }))
  121. }
  122. function dedupeAgreementInfos(list: agreementInfosItem[]): agreementInfosItem[] {
  123. const map = new Map<string, agreementInfosItem>()
  124. list.forEach((item) => {
  125. if (!map.has(item.templateUrl)) {
  126. map.set(item.templateUrl, item)
  127. }
  128. })
  129. return Array.from(map.values())
  130. }
  131. function navigateToAgreementPage(path: string): void {
  132. uni.navigateTo({
  133. url: `${AGREEMENT_PAGE_BASE}/${path}`,
  134. })
  135. }
  136. async function openPdfFile(item: agreementInfosItem): Promise<void> {
  137. const fileUrl = item.templateUrl?.trim()
  138. if (!fileUrl) {
  139. showToast('文件地址不存在')
  140. return
  141. }
  142. uni.showLoading({
  143. title: '文件加载中',
  144. mask: true,
  145. })
  146. try {
  147. const filePath = await downloadFile(fileUrl)
  148. await openDocument(filePath)
  149. } catch (error) {
  150. console.log('openPdfFile error', error)
  151. showToast('文件打开失败')
  152. } finally {
  153. uni.hideLoading()
  154. }
  155. }
  156. function downloadFile(url: string): Promise<string> {
  157. return new Promise((resolve, reject) => {
  158. uni.downloadFile({
  159. url,
  160. success: (res) => {
  161. if (res.statusCode === 200 && res.tempFilePath) {
  162. resolve(res.tempFilePath)
  163. return
  164. }
  165. reject(new Error('download file failed'))
  166. },
  167. fail: reject,
  168. })
  169. })
  170. }
  171. function openDocument(filePath: string): Promise<void> {
  172. return new Promise((resolve, reject) => {
  173. uni.openDocument({
  174. filePath,
  175. fileType: 'pdf',
  176. showMenu: true,
  177. success: () => {
  178. resolve()
  179. },
  180. fail: reject,
  181. })
  182. })
  183. }
  184. function hasSignedAgreement(agreementCode: string): boolean {
  185. const signedAgreement = currentUserInfo.value?.signedAgreement
  186. if (Array.isArray(signedAgreement)) {
  187. return signedAgreement.includes(agreementCode)
  188. }
  189. if (typeof signedAgreement === 'string') {
  190. return signedAgreement.includes(agreementCode)
  191. }
  192. return false
  193. }
  194. function showToast(title: string): void {
  195. uni.showToast({
  196. title,
  197. icon: 'none',
  198. })
  199. }
  200. </script>
  201. <style lang="scss" scoped>
  202. .agreement-container {
  203. min-height: 100vh;
  204. padding: 28rpx 24rpx 40rpx;
  205. box-sizing: border-box;
  206. background:
  207. radial-gradient(
  208. circle at 50% -120rpx,
  209. rgba(47, 140, 255, 0.14) 0%,
  210. rgba(47, 140, 255, 0) 420rpx
  211. ),
  212. linear-gradient(180deg, #f6f8fc 0%, #f7f8fa 100%);
  213. }
  214. .agreement-list {
  215. display: flex;
  216. flex-direction: column;
  217. gap: 18rpx;
  218. }
  219. .agreement-card {
  220. position: relative;
  221. display: flex;
  222. align-items: center;
  223. min-height: 112rpx;
  224. padding: 0 28rpx 0 32rpx;
  225. box-sizing: border-box;
  226. border: 1rpx solid rgba(31, 41, 55, 0.06);
  227. border-radius: 24rpx;
  228. background:
  229. linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0.9) 100%), #ffffff;
  230. box-shadow:
  231. 0 16rpx 40rpx rgba(15, 23, 42, 0.05),
  232. 0 2rpx 8rpx rgba(15, 23, 42, 0.03);
  233. overflow: hidden;
  234. transition:
  235. transform 0.18s ease,
  236. opacity 0.18s ease;
  237. }
  238. .agreement-card--active {
  239. opacity: 0.86;
  240. transform: scale(0.985);
  241. }
  242. .agreement-card__title {
  243. flex: 1;
  244. min-width: 0;
  245. padding-right: 24rpx;
  246. font-size: 31rpx;
  247. font-weight: 600;
  248. line-height: 44rpx;
  249. color: #1f2937;
  250. letter-spacing: 0.2rpx;
  251. overflow: hidden;
  252. text-overflow: ellipsis;
  253. white-space: nowrap;
  254. }
  255. .agreement-card__arrow {
  256. flex-shrink: 0;
  257. font-size: 48rpx;
  258. font-weight: 300;
  259. line-height: 1;
  260. color: #aeb8c6;
  261. }
  262. </style>