my-resources.vue 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777
  1. <template>
  2. <view class="my-resources">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <text class="header-title">我的资源</text>
  6. <view class="header-right">
  7. <text class="dropdown-arrow">▼</text>
  8. </view>
  9. </view>
  10. <!-- 搜索栏 -->
  11. <view class="search-bar">
  12. <view class="search-input-wrapper">
  13. <view class="search-icon-small"></view>
  14. <input type="text" class="search-input" placeholder="请输入姓名、手机号或标签(如:温柔)" v-model="searchKeyword" @input="handleSearch" />
  15. </view>
  16. </view>
  17. <!-- 资源/线索切换标签 -->
  18. <view class="tab-switcher">
  19. <view class="tab-item" :class="{ active: currentTab === 'resource' }" @click="switchToResource">
  20. <text class="tab-text">资源</text>
  21. <text class="tab-count">({{ registeredTotal }})</text>
  22. </view>
  23. <view class="tab-item" :class="{ active: currentTab === 'clue' }" @click="switchToClue">
  24. <text class="tab-text">线索</text>
  25. <text class="tab-count">({{ unregisteredTotal }})</text>
  26. </view>
  27. </view>
  28. <scroll-view scroll-y class="content" @scrolltolower="handleScrollToLower" :lower-threshold="50" :enable-back-to-top="true" :scroll-with-animation="true">
  29. <!-- 资源部分(已注册用户) -->
  30. <view class="resource-section" v-if="currentTab === 'resource'">
  31. <view class="section-header" v-if="registeredResources.length > 0">
  32. <text class="section-title">资源</text>
  33. <text class="section-count">({{ registeredTotal }})</text>
  34. </view>
  35. <view class="resource-list-container">
  36. <view class="resource-item" v-for="(item, index) in getRegisteredDisplayData" :key="getRegisteredItemKey(item, index)" @click="handleResourceClickSafe(item, index)">
  37. <!-- 右上角选中图标 -->
  38. <view class="select-icon">✓-</view>
  39. <view class="resource-header">
  40. <image
  41. :src="item.avatar"
  42. mode="aspectFill"
  43. class="resource-avatar"
  44. @error="handleImageErrorRegistered(index)"
  45. @load="handleImageLoadRegistered(index)"
  46. :lazy-load="true"
  47. ></image>
  48. <view class="resource-basic-info">
  49. <view class="name-gender">
  50. <text class="resource-name">{{ item.name }}</text>
  51. <text class="resource-gender gender-tag">{{ item.gender }}</text>
  52. </view>
  53. <view class="status-tag-wrapper">
  54. <text class="status-tag register-tag registered">已注册</text>
  55. <text class="status-tag match-tag" :class="{ 'matched': item.isMatch === 1, 'unmatched': item.isMatch === 0 || !item.isMatch }">
  56. {{ item.isMatch === 1 ? '已匹配' : '未匹配' }}
  57. </text>
  58. </view>
  59. </view>
  60. </view>
  61. <view class="resource-details">
  62. <view class="labels">
  63. <text class="label" v-for="(label, idx) in item.labels" :key="idx">{{ label }}</text>
  64. </view>
  65. <view class="requirement-box">
  66. <text class="requirement-label">择偶要求:</text>
  67. <text class="requirement-content">{{ item.requirement }}</text>
  68. </view>
  69. <view class="contact-box">
  70. <text class="contact-label">联系方式:</text>
  71. <text class="contact-number">{{ item.contact }}</text>
  72. <text class="copy-btn" @click.stop="handleCopyContact(item.originalPhone || item.contact)">复制</text>
  73. </view>
  74. <view class="action-buttons">
  75. <view class="delete-btn" @click.stop="handleDelete(item.id || item.resourceId || item.resource_id)">删除</view>
  76. <view class="match-btn" @click.stop="handleMatch(item.id || item.resourceId || item.resource_id)">精准匹配</view>
  77. </view>
  78. </view>
  79. <!-- 优质资源标签 -->
  80. <view class="quality-tag" v-if="item.isQuality">
  81. <text class="quality-star">★</text>
  82. <text class="quality-text">优质资源</text>
  83. </view>
  84. </view>
  85. </view>
  86. <!-- 加载更多提示 -->
  87. <view class="load-more-tip" v-if="registeredResources.length < registeredTotal && currentTab === 'resource'">
  88. <text class="load-more-text">下拉加载更多</text>
  89. </view>
  90. <!-- 没有更多数据提示 -->
  91. <view class="no-more-tip" v-if="registeredResources.length >= registeredTotal && registeredTotal > 0 && currentTab === 'resource'">
  92. <text class="no-more-text">已加载全部数据</text>
  93. </view>
  94. </view>
  95. <!-- 线索部分(未注册用户) -->
  96. <view class="resource-section" v-if="currentTab === 'clue'">
  97. <view class="section-header" v-if="unregisteredResources.length > 0">
  98. <text class="section-title">线索</text>
  99. <text class="section-count">({{ unregisteredTotal }})</text>
  100. </view>
  101. <view class="resource-list-container">
  102. <view class="resource-item" v-for="(item, index) in getUnregisteredDisplayData" :key="getUnregisteredItemKey(item, index)" @click="handleResourceClickSafe(item, index)">
  103. <!-- 右上角选中图标 -->
  104. <view class="select-icon">✓-</view>
  105. <view class="resource-header">
  106. <image
  107. :src="item.avatar"
  108. mode="aspectFill"
  109. class="resource-avatar"
  110. @error="handleImageErrorUnregistered(index)"
  111. @load="handleImageLoadUnregistered(index)"
  112. :lazy-load="true"
  113. ></image>
  114. <view class="resource-basic-info">
  115. <view class="name-gender">
  116. <text class="resource-name">{{ item.name }}</text>
  117. <text class="resource-gender gender-tag">{{ item.gender }}</text>
  118. </view>
  119. <view class="status-tag-wrapper">
  120. <text class="status-tag register-tag unregistered">未注册</text>
  121. <!-- 审核状态标签(仅线索列表显示) -->
  122. <text class="status-tag audit-tag" :class="{ 'audit-pending': item.status === 0 || item.status === null || item.status === undefined, 'audit-approved': item.status === 1, 'audit-rejected': item.status === 2 }">
  123. {{ item.status === 0 || item.status === null || item.status === undefined ? '待审核' : (item.status === 1 ? '审核通过' : (item.status === 2 ? '审核未通过' : '待审核')) }}
  124. </text>
  125. </view>
  126. </view>
  127. </view>
  128. <view class="resource-details">
  129. <view class="labels">
  130. <text class="label" v-for="(label, idx) in item.labels" :key="idx">{{ label }}</text>
  131. </view>
  132. <view class="requirement-box">
  133. <text class="requirement-label">择偶要求:</text>
  134. <text class="requirement-content">{{ item.requirement }}</text>
  135. </view>
  136. <view class="contact-box">
  137. <text class="contact-label">联系方式:</text>
  138. <text class="contact-number">{{ item.contact }}</text>
  139. <text class="copy-btn" @click.stop="handleCopyContact(item.originalPhone || item.contact)">复制</text>
  140. </view>
  141. <view class="action-buttons">
  142. <view class="delete-btn" @click.stop="handleDelete(item.id || item.resourceId || item.resource_id)">删除</view>
  143. <view class="match-btn" @click.stop="handleMatch(item.id || item.resourceId || item.resource_id)">精准匹配</view>
  144. </view>
  145. </view>
  146. <!-- 优质资源标签 -->
  147. <view class="quality-tag" v-if="item.isQuality">
  148. <text class="quality-star">★</text>
  149. <text class="quality-text">优质资源</text>
  150. </view>
  151. </view>
  152. </view>
  153. <!-- 加载更多提示 -->
  154. <view class="load-more-tip" v-if="unregisteredResources.length < unregisteredTotal && currentTab === 'clue'">
  155. <text class="load-more-text">下拉加载更多</text>
  156. </view>
  157. <!-- 没有更多数据提示 -->
  158. <view class="no-more-tip" v-if="unregisteredResources.length >= unregisteredTotal && unregisteredTotal > 0 && currentTab === 'clue'">
  159. <text class="no-more-text">已加载全部数据</text>
  160. </view>
  161. </view>
  162. <!-- 空状态 -->
  163. <view class="empty-state" v-if="(currentTab === 'resource' && registeredTotal === 0 && registeredResources.length === 0) || (currentTab === 'clue' && unregisteredTotal === 0 && unregisteredResources.length === 0)">
  164. <text class="empty-text">{{ currentTab === 'resource' ? '暂无资源' : '暂无线索' }}</text>
  165. </view>
  166. </scroll-view>
  167. <!-- 底部添加按钮 -->
  168. <view class="add-button" @click="handleAdd">
  169. <text class="add-button-icon">+</text>
  170. </view>
  171. <!-- 底部导航 -->
  172. <view class="tabbar">
  173. <view class="tabbar-item" @click="navigateToWorkbench">
  174. <view class="tabbar-icon home"></view>
  175. <text class="tabbar-text">工作台</text>
  176. </view>
  177. <view class="tabbar-item active" @click="navigateToMyResources">
  178. <view class="tabbar-icon resources"></view>
  179. <text class="tabbar-text">我的资源</text>
  180. </view>
  181. <view class="tabbar-item" @click="navigateToRanking">
  182. <view class="tabbar-icon trophy"></view>
  183. <text class="tabbar-text">排行榜</text>
  184. </view>
  185. <view class="tabbar-item" @click="navigateToMessage">
  186. <view class="tabbar-icon message">
  187. <view v-if="unreadCount > 0" class="badge">{{ unreadCount }}</view>
  188. </view>
  189. <text class="tabbar-text">消息</text>
  190. </view>
  191. <view class="tabbar-item" @click="navigateToMine">
  192. <view class="tabbar-icon mine"></view>
  193. <text class="tabbar-text">我的</text>
  194. </view>
  195. </view>
  196. </view>
  197. </template>
  198. <script>
  199. import api from '@/utils/api.js'
  200. export default {
  201. data() {
  202. return {
  203. searchKeyword: '',
  204. isFirstLoad: true, // 标记是否为首次加载
  205. currentTab: 'resource', // 当前选中的标签:'resource' 或 'clue'
  206. resources: [], // 保留用于兼容
  207. registeredResources: [], // 已注册用户(资源部分)- 已加载的数据
  208. unregisteredResources: [], // 未注册用户(线索部分)- 已加载的数据
  209. allRegisteredResources: [], // 已注册用户(资源部分)- 全部数据(从后端获取)
  210. allUnregisteredResources: [], // 未注册用户(线索部分)- 全部数据(从后端获取)
  211. refreshTimer: null, // 定时刷新器
  212. // 资源分页
  213. registeredPageNum: 1, // 资源当前页码
  214. registeredPageSize: 3, // 资源每页显示数量
  215. registeredTotal: 0, // 资源总数
  216. registeredLoading: false, // 资源加载中标志
  217. // 线索分页
  218. unregisteredPageNum: 1, // 线索当前页码
  219. unregisteredPageSize: 3, // 线索每页显示数量
  220. unregisteredTotal: 0, // 线索总数
  221. unregisteredLoading: false // 线索加载中标志
  222. }
  223. },
  224. computed: {
  225. // 全局未读消息数
  226. unreadCount() {
  227. return this.$store.getters.getTotalUnread || 0
  228. },
  229. // 获取资源已加载的数据(用于显示)
  230. getRegisteredDisplayData() {
  231. const data = (this.registeredResources || []).filter(item => {
  232. const isValid = item && item.id !== undefined && item.id !== null
  233. if (!isValid) {
  234. console.warn('getRegisteredDisplayData - 过滤掉无效数据:', item)
  235. }
  236. return isValid
  237. })
  238. console.log('getRegisteredDisplayData - 返回数据长度:', data.length, '数据:', data.map(item => ({ id: item.id, name: item.name })))
  239. return data
  240. },
  241. // 获取线索已加载的数据(用于显示)
  242. getUnregisteredDisplayData() {
  243. const data = (this.unregisteredResources || []).filter(item => {
  244. const isValid = item && item.id !== undefined && item.id !== null
  245. if (!isValid) {
  246. console.warn('getUnregisteredDisplayData - 过滤掉无效数据:', item)
  247. }
  248. return isValid
  249. })
  250. console.log('getUnregisteredDisplayData - 返回数据长度:', data.length, '数据:', data.map(item => ({ id: item.id, name: item.name })))
  251. return data
  252. }
  253. },
  254. onLoad() {
  255. // 加载我的资源数据
  256. this.loadMyResources()
  257. this.isFirstLoad = false
  258. // 监听刷新事件
  259. uni.$on('refreshResourceList', () => {
  260. console.log('收到刷新资源列表事件')
  261. this.loadMyResources()
  262. })
  263. // 启动定时刷新,每30秒检查一次用户注册状态变化
  264. this.startAutoRefresh()
  265. },
  266. onShow() {
  267. // 页面显示时刷新列表(从其他页面返回时,非首次加载)
  268. if (!this.isFirstLoad) {
  269. this.loadMyResources()
  270. }
  271. // 重新启动定时刷新
  272. this.startAutoRefresh()
  273. },
  274. onHide() {
  275. // 页面隐藏时停止定时刷新
  276. this.stopAutoRefresh()
  277. },
  278. onUnload() {
  279. // 页面卸载时移除事件监听和停止定时刷新
  280. uni.$off('refreshResourceList')
  281. this.stopAutoRefresh()
  282. },
  283. methods: {
  284. // 获取审核状态文本
  285. getAuditStatusText(status) {
  286. if (status === null || status === undefined) {
  287. return '待审核'
  288. }
  289. switch (status) {
  290. case 0:
  291. return '待审核'
  292. case 1:
  293. return '审核通过'
  294. case 2:
  295. return '审核未通过'
  296. default:
  297. return '待审核'
  298. }
  299. },
  300. // 获取审核状态样式类(返回对象,用于:class绑定)
  301. getAuditStatusClass(status) {
  302. if (status === null || status === undefined) {
  303. return { 'audit-pending': true }
  304. }
  305. switch (status) {
  306. case 0:
  307. return { 'audit-pending': true }
  308. case 1:
  309. return { 'audit-approved': true }
  310. case 2:
  311. return { 'audit-rejected': true }
  312. default:
  313. return { 'audit-pending': true }
  314. }
  315. },
  316. // 获取资源项的key
  317. getRegisteredItemKey(item, index) {
  318. if (item && item.id) {
  319. return 'registered-' + item.id
  320. }
  321. return 'registered-placeholder-' + index
  322. },
  323. // 获取线索项的key
  324. getUnregisteredItemKey(item, index) {
  325. if (item && item.id) {
  326. return 'unregistered-' + item.id
  327. }
  328. return 'unregistered-placeholder-' + index
  329. },
  330. // 切换到资源标签
  331. switchToResource() {
  332. this.currentTab = 'resource'
  333. // 切换标签时重置已加载数据
  334. this.registeredPageNum = 1
  335. this.loadRegisteredPage(1)
  336. },
  337. // 切换到线索标签
  338. switchToClue() {
  339. this.currentTab = 'clue'
  340. // 切换标签时重置已加载数据
  341. this.unregisteredPageNum = 1
  342. this.loadUnregisteredPage(1)
  343. },
  344. // 滚动到底部触发加载更多
  345. handleScrollToLower(e) {
  346. console.log('=== 触发滚动到底部事件 ===', e)
  347. if (this.currentTab === 'resource') {
  348. // 资源列表加载更多
  349. console.log('资源列表状态:', {
  350. registeredLoading: this.registeredLoading,
  351. registeredResourcesLength: this.registeredResources.length,
  352. registeredTotal: this.registeredTotal,
  353. canLoadMore: this.registeredResources.length < this.registeredTotal
  354. })
  355. if (!this.registeredLoading) {
  356. // 如果已加载数量小于总数,继续加载
  357. if (this.registeredTotal > 0 && this.registeredResources.length < this.registeredTotal) {
  358. console.log('✅ 开始加载更多资源...')
  359. this.loadMoreRegistered()
  360. } else if (this.registeredTotal === 0) {
  361. console.log('⚠️ 资源总数未知,尝试加载...')
  362. // 如果总数未知,也尝试加载(可能是第一页加载失败)
  363. this.loadMoreRegistered()
  364. } else {
  365. console.log('❌ 资源列表已加载全部数据,不再加载')
  366. }
  367. } else {
  368. console.log('⏳ 资源列表正在加载中,跳过...')
  369. }
  370. } else {
  371. // 线索列表加载更多
  372. console.log('线索列表状态:', {
  373. unregisteredLoading: this.unregisteredLoading,
  374. unregisteredResourcesLength: this.unregisteredResources.length,
  375. unregisteredTotal: this.unregisteredTotal,
  376. canLoadMore: this.unregisteredResources.length < this.unregisteredTotal
  377. })
  378. if (!this.unregisteredLoading) {
  379. // 如果已加载数量小于总数,继续加载
  380. if (this.unregisteredTotal > 0 && this.unregisteredResources.length < this.unregisteredTotal) {
  381. console.log('✅ 开始加载更多线索...')
  382. this.loadMoreUnregistered()
  383. } else if (this.unregisteredTotal === 0) {
  384. console.log('⚠️ 线索总数未知,尝试加载...')
  385. // 如果总数未知,也尝试加载(可能是第一页加载失败)
  386. this.loadMoreUnregistered()
  387. } else {
  388. console.log('❌ 线索列表已加载全部数据,不再加载')
  389. }
  390. } else {
  391. console.log('⏳ 线索列表正在加载中,跳过...')
  392. }
  393. }
  394. },
  395. // 加载资源指定页数据(从后端加载,使用isUser参数过滤已注册用户)
  396. async loadRegisteredPage(pageNum) {
  397. if (this.registeredLoading) return
  398. this.registeredLoading = true
  399. this.registeredPageNum = pageNum
  400. try {
  401. // 获取当前登录用户ID
  402. const userInfo = uni.getStorageSync('userInfo') || {}
  403. const userId = uni.getStorageSync('userId')
  404. let currentUserId = userInfo.userId || userId || null
  405. if (currentUserId !== null && currentUserId !== undefined) {
  406. currentUserId = parseInt(currentUserId)
  407. if (isNaN(currentUserId) || currentUserId <= 0) {
  408. currentUserId = null
  409. }
  410. }
  411. if (!currentUserId) {
  412. console.error('无法获取当前登录用户ID')
  413. this.registeredResources = []
  414. this.registeredLoading = false
  415. return
  416. }
  417. const baseUrl = process.env.NODE_ENV === 'development'
  418. ? 'http://localhost:8083/api'
  419. : 'https://your-domain.com/api'
  420. // 构建查询参数,使用isUser=1参数只获取已注册用户
  421. let url = `${baseUrl}/my-resource/list?currentUserId=${currentUserId}&pageNum=${pageNum}&pageSize=${this.registeredPageSize}&isUser=1`
  422. if (this.searchKeyword && this.searchKeyword.trim()) {
  423. url += `&keyword=${encodeURIComponent(this.searchKeyword.trim())}`
  424. }
  425. const [error, res] = await uni.request({
  426. url: url,
  427. method: 'GET'
  428. })
  429. if (error) {
  430. console.error('加载资源数据失败:', error)
  431. this.registeredResources = []
  432. this.registeredLoading = false
  433. return
  434. }
  435. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  436. const pageData = res.data.data
  437. if (pageData && pageData.records) {
  438. // 转换数据格式,并过滤掉无效数据(null值)
  439. const resources = this.convertResourceData(pageData.records).filter(item => item !== null && item !== undefined)
  440. // 如果是第一页,直接替换;否则追加
  441. if (pageNum === 1) {
  442. this.registeredResources = resources
  443. } else {
  444. this.registeredResources = [...this.registeredResources, ...resources]
  445. }
  446. // 更新总数(使用后端返回的total)
  447. this.registeredTotal = pageData.total || 0
  448. console.log('loadRegisteredPage - pageNum:', pageNum, 'loaded:', this.registeredResources.length, 'total:', this.registeredTotal, 'pageData:', {
  449. total: pageData.total,
  450. size: pageData.size,
  451. current: pageData.current,
  452. pages: pageData.pages,
  453. recordsLength: pageData.records.length
  454. })
  455. } else {
  456. if (pageNum === 1) {
  457. this.registeredResources = []
  458. this.registeredTotal = 0
  459. }
  460. }
  461. } else {
  462. console.error('加载资源数据失败,响应:', res.data)
  463. }
  464. } catch (e) {
  465. console.error('加载资源数据异常:', e)
  466. if (this.registeredPageNum === 1) {
  467. this.registeredResources = []
  468. }
  469. } finally {
  470. this.registeredLoading = false
  471. }
  472. },
  473. // 加载线索指定页数据(从后端加载,使用isUser参数过滤未注册用户)
  474. async loadUnregisteredPage(pageNum) {
  475. if (this.unregisteredLoading) return
  476. this.unregisteredLoading = true
  477. this.unregisteredPageNum = pageNum
  478. try {
  479. // 获取当前登录用户ID
  480. const userInfo = uni.getStorageSync('userInfo') || {}
  481. const userId = uni.getStorageSync('userId')
  482. let currentUserId = userInfo.userId || userId || null
  483. if (currentUserId !== null && currentUserId !== undefined) {
  484. currentUserId = parseInt(currentUserId)
  485. if (isNaN(currentUserId) || currentUserId <= 0) {
  486. currentUserId = null
  487. }
  488. }
  489. if (!currentUserId) {
  490. console.error('无法获取当前登录用户ID')
  491. this.unregisteredResources = []
  492. this.unregisteredLoading = false
  493. return
  494. }
  495. const baseUrl = process.env.NODE_ENV === 'development'
  496. ? 'http://localhost:8083/api'
  497. : 'https://your-domain.com/api'
  498. // 构建查询参数,使用isUser=0参数只获取未注册用户(线索)
  499. let url = `${baseUrl}/my-resource/list?currentUserId=${currentUserId}&pageNum=${pageNum}&pageSize=${this.unregisteredPageSize}&isUser=0`
  500. if (this.searchKeyword && this.searchKeyword.trim()) {
  501. url += `&keyword=${encodeURIComponent(this.searchKeyword.trim())}`
  502. }
  503. const [error, res] = await uni.request({
  504. url: url,
  505. method: 'GET'
  506. })
  507. if (error) {
  508. console.error('加载线索数据失败:', error)
  509. this.unregisteredResources = []
  510. this.unregisteredLoading = false
  511. return
  512. }
  513. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  514. const pageData = res.data.data
  515. if (pageData && pageData.records) {
  516. // 转换数据格式,并过滤掉无效数据(null值)
  517. const resources = this.convertResourceData(pageData.records).filter(item => item !== null && item !== undefined)
  518. // 如果是第一页,直接替换;否则追加
  519. if (pageNum === 1) {
  520. this.unregisteredResources = resources
  521. } else {
  522. this.unregisteredResources = [...this.unregisteredResources, ...resources]
  523. }
  524. // 更新总数(使用后端返回的total)
  525. this.unregisteredTotal = pageData.total || 0
  526. console.log('loadUnregisteredPage - pageNum:', pageNum, 'loaded:', this.unregisteredResources.length, 'total:', this.unregisteredTotal, 'pageData:', {
  527. total: pageData.total,
  528. size: pageData.size,
  529. current: pageData.current,
  530. pages: pageData.pages,
  531. recordsLength: pageData.records.length
  532. })
  533. } else {
  534. if (pageNum === 1) {
  535. this.unregisteredResources = []
  536. this.unregisteredTotal = 0
  537. }
  538. }
  539. } else {
  540. console.error('加载线索数据失败,响应:', res.data)
  541. }
  542. } catch (e) {
  543. console.error('加载线索数据异常:', e)
  544. if (this.unregisteredPageNum === 1) {
  545. this.unregisteredResources = []
  546. }
  547. } finally {
  548. this.unregisteredLoading = false
  549. }
  550. },
  551. // 加载更多资源(从后端分页加载)
  552. async loadMoreRegistered() {
  553. if (this.registeredLoading) return
  554. // 如果已加载的数据已经达到或超过估算的总数,不再加载
  555. if (this.registeredResources.length >= this.registeredTotal && this.registeredTotal > 0) return
  556. this.registeredPageNum++
  557. await this.loadRegisteredPage(this.registeredPageNum)
  558. },
  559. // 加载更多线索(从后端分页加载)
  560. async loadMoreUnregistered() {
  561. if (this.unregisteredLoading) return
  562. // 如果已加载的数据已经达到或超过估算的总数,不再加载
  563. if (this.unregisteredResources.length >= this.unregisteredTotal && this.unregisteredTotal > 0) return
  564. this.unregisteredPageNum++
  565. await this.loadUnregisteredPage(this.unregisteredPageNum)
  566. },
  567. // 转换资源数据格式(提取公共方法)
  568. convertResourceData(records) {
  569. return records.map(item => {
  570. let requirement = item.mateSelectionCriteria || item.mate_selection_criteria || ''
  571. if (requirement) {
  572. requirement = requirement.trim()
  573. }
  574. const labels = []
  575. if (item.constellation) {
  576. labels.push(item.constellation)
  577. }
  578. if (item.occupation) {
  579. labels.push(item.occupation)
  580. }
  581. let avatarUrl = item.avatarUrl || item.avatar_url || item.avatar || ''
  582. if (!avatarUrl || avatarUrl.trim() === '' || avatarUrl === 'null' || avatarUrl === null || avatarUrl === undefined) {
  583. avatarUrl = ''
  584. } else {
  585. avatarUrl = avatarUrl.trim()
  586. }
  587. let resourceId = item.resourceId || item.resource_id
  588. if (resourceId === null || resourceId === undefined || resourceId === 'undefined' || resourceId === 'null') {
  589. return null
  590. }
  591. resourceId = parseInt(resourceId)
  592. if (isNaN(resourceId) || resourceId <= 0) {
  593. return null
  594. }
  595. let isUser = item.isUser !== null && item.isUser !== undefined ? item.isUser :
  596. (item.is_user !== null && item.is_user !== undefined ? item.is_user : 0)
  597. isUser = parseInt(isUser) || 0
  598. let userId = item.userId !== null && item.userId !== undefined ? item.userId :
  599. (item.user_id !== null && item.user_id !== undefined ? item.user_id : null)
  600. if (userId !== null && userId !== undefined) {
  601. userId = parseInt(userId)
  602. if (isNaN(userId) || userId <= 0) {
  603. userId = null
  604. }
  605. }
  606. let isMatch = item.isMatch !== null && item.isMatch !== undefined ? item.isMatch :
  607. (item.is_match !== null && item.is_match !== undefined ? item.is_match : 0)
  608. isMatch = parseInt(isMatch) || 0
  609. let resourceTags = []
  610. if (item.tags && Array.isArray(item.tags) && item.tags.length > 0) {
  611. resourceTags = item.tags
  612. } else if (labels && labels.length > 0) {
  613. resourceTags = labels
  614. }
  615. return {
  616. id: resourceId,
  617. resourceId: resourceId,
  618. resource_id: resourceId,
  619. avatar: avatarUrl,
  620. name: item.name || '',
  621. gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
  622. status: item.status !== undefined && item.status !== null ? item.status : 0,
  623. isUser: isUser,
  624. userId: userId,
  625. isMatch: isMatch,
  626. isPlus: false,
  627. labels: resourceTags,
  628. requirement: requirement || '暂无要求',
  629. contact: item.phone ? item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  630. originalPhone: item.phone || '',
  631. isQuality: false
  632. }
  633. }).filter(item => item !== null)
  634. },
  635. // 加载我的资源数据(改为分页加载)
  636. async loadMyResources() {
  637. try {
  638. // 根据当前标签加载对应数据
  639. if (this.currentTab === 'resource') {
  640. // 重置资源列表
  641. this.registeredPageNum = 1
  642. await this.loadRegisteredPage(1)
  643. } else {
  644. // 重置线索列表
  645. this.unregisteredPageNum = 1
  646. await this.loadUnregisteredPage(1)
  647. }
  648. } catch (e) {
  649. console.error('加载资源数据失败:', e)
  650. }
  651. },
  652. // 搜索
  653. async handleSearch() {
  654. // 重置分页到第一页
  655. this.registeredPageNum = 1
  656. this.unregisteredPageNum = 1
  657. // 重新加载资源数据,包含搜索关键词
  658. await this.loadMyResources()
  659. },
  660. // 删除资源
  661. async handleDelete(id) {
  662. // 如果id无效,尝试从当前资源列表中查找
  663. if (id === null || id === undefined || id === 'undefined' || id === 'null' || id === '') {
  664. console.warn('删除资源失败: 资源ID无效,尝试从资源列表中查找', id)
  665. // 尝试从当前显示的资源中找到对应的资源
  666. const allDisplayData = this.currentTab === 'resource'
  667. ? this.getRegisteredDisplayData
  668. : this.getUnregisteredDisplayData
  669. // 如果仍然找不到,显示错误
  670. console.error('删除资源失败: 无法获取资源ID', id)
  671. uni.showToast({
  672. title: '资源ID无效,无法删除',
  673. icon: 'none'
  674. })
  675. return
  676. }
  677. // 确保id是有效的整数
  678. let resourceId = parseInt(id)
  679. if (isNaN(resourceId) || resourceId <= 0) {
  680. // 如果解析失败,尝试从字符串中提取数字
  681. const match = String(id).match(/\d+/)
  682. if (match) {
  683. resourceId = parseInt(match[0])
  684. }
  685. if (isNaN(resourceId) || resourceId <= 0) {
  686. console.error('删除资源失败: 资源ID格式错误', id, '类型:', typeof id)
  687. uni.showToast({
  688. title: '资源ID格式错误,无法删除',
  689. icon: 'none'
  690. })
  691. return
  692. }
  693. }
  694. console.log('准备删除资源,ID:', resourceId, '原始ID:', id)
  695. uni.showModal({
  696. title: '删除确认',
  697. content: '确定要删除该资源吗?',
  698. success: async (res) => {
  699. if (res.confirm) {
  700. try {
  701. uni.showLoading({
  702. title: '删除中...'
  703. })
  704. const baseUrl = process.env.NODE_ENV === 'development'
  705. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  706. : 'https://your-domain.com/api' // 生产环境
  707. const [error, deleteRes] = await uni.request({
  708. url: `${baseUrl}/my-resource/delete/${resourceId}`,
  709. method: 'DELETE'
  710. })
  711. uni.hideLoading()
  712. if (error) {
  713. console.error('删除资源失败:', error)
  714. uni.showToast({
  715. title: '删除失败',
  716. icon: 'none'
  717. })
  718. return
  719. }
  720. if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.code === 200) {
  721. uni.showToast({
  722. title: '删除成功',
  723. icon: 'success'
  724. })
  725. // 删除成功后刷新资源列表
  726. await this.loadMyResources()
  727. } else {
  728. uni.showToast({
  729. title: deleteRes.data.message || '删除失败',
  730. icon: 'none'
  731. })
  732. }
  733. } catch (e) {
  734. uni.hideLoading()
  735. console.error('删除资源异常:', e)
  736. uni.showToast({
  737. title: '删除失败,请稍后重试',
  738. icon: 'none'
  739. })
  740. }
  741. }
  742. }
  743. })
  744. },
  745. // 精准匹配
  746. handleMatch(id) {
  747. // 查找资源项
  748. const allResources = [...this.registeredResources, ...this.unregisteredResources]
  749. const resource = allResources.find(item => item.id === id)
  750. if (!resource) {
  751. uni.showToast({
  752. title: '资源不存在',
  753. icon: 'none'
  754. })
  755. return
  756. }
  757. // 检查用户是否已注册(isUser === 1 且 userId !== null)
  758. if (resource.isUser !== 1 || resource.userId === null || resource.userId === undefined) {
  759. uni.showToast({
  760. title: '该用户还未注册用户端,注册之后才能进行匹配',
  761. icon: 'none',
  762. duration: 3000
  763. })
  764. return
  765. }
  766. // 已注册用户,跳转到精准匹配页面
  767. console.log('精准匹配:', id)
  768. uni.navigateTo({
  769. url: `/pages/matchmaker-workbench/precise-match?resourceId=${id}`
  770. })
  771. },
  772. // 复制联系方式
  773. handleCopyContact(contact) {
  774. if (!contact) {
  775. uni.showToast({
  776. title: '联系方式为空',
  777. icon: 'none'
  778. })
  779. return
  780. }
  781. // 如果是脱敏的手机号,需要从原始数据中获取完整号码
  782. // 这里需要根据实际情况调整,可能需要从item中获取原始phone
  783. // 暂时先复制显示的文本
  784. uni.setClipboardData({
  785. data: contact,
  786. success: () => {
  787. uni.showToast({
  788. title: '已复制到剪贴板',
  789. icon: 'success'
  790. })
  791. },
  792. fail: () => {
  793. uni.showToast({
  794. title: '复制失败',
  795. icon: 'none'
  796. })
  797. }
  798. })
  799. },
  800. // 客户点击事件,跳转到客户详情页面
  801. handleClientClick(id) {
  802. uni.navigateTo({
  803. url: `/pages/matchmaker-workbench/client-detail?resourceId=${id}`
  804. })
  805. },
  806. // 资源项点击事件(安全包装方法)
  807. handleResourceClickSafe(item, index) {
  808. // 如果 item 无效,尝试从数组中获取
  809. if (!item || item === null || item === undefined) {
  810. console.warn('handleResourceClickSafe: item 为空,尝试从数组中获取, index:', index)
  811. // 根据当前标签获取对应的数据数组
  812. const dataArray = this.currentTab === 'resource' ? this.getRegisteredDisplayData : this.getUnregisteredDisplayData
  813. if (dataArray && Array.isArray(dataArray) && index >= 0 && index < dataArray.length) {
  814. item = dataArray[index]
  815. }
  816. }
  817. // 如果仍然无效,直接返回
  818. if (!item || item === null || item === undefined) {
  819. console.error('handleResourceClickSafe: 无法获取有效的 item, index:', index)
  820. return
  821. }
  822. // 调用实际的点击处理方法
  823. this.handleResourceClick(item)
  824. },
  825. // 资源项点击事件
  826. handleResourceClick(item) {
  827. // 检查 item 是否存在
  828. if (!item || item === null || item === undefined) {
  829. console.error('handleResourceClick: item 为空', item)
  830. return
  831. }
  832. console.log('=== 点击资源项 ===')
  833. console.log('item对象:', JSON.stringify(item, null, 2))
  834. console.log('item.id:', item.id)
  835. console.log('item.isUser:', item.isUser)
  836. // 检查 item.id 是否存在
  837. if (!item.id && item.id !== 0) {
  838. console.error('handleResourceClick: item.id 无效', item)
  839. uni.showToast({
  840. title: '资源ID无效',
  841. icon: 'none',
  842. duration: 2000
  843. })
  844. return
  845. }
  846. // 判断是否为已注册用户(isUser === 1 且 userId !== null)
  847. if (item.isUser === 1 && item.userId !== null && item.userId !== undefined) {
  848. // 已注册,跳转到客户详情页面
  849. const resourceId = item.id
  850. console.log('准备跳转,resourceId:', resourceId)
  851. uni.navigateTo({
  852. url: `/pages/matchmaker-workbench/client-detail?resourceId=${resourceId}`,
  853. success: () => {
  854. console.log('跳转成功,resourceId:', resourceId)
  855. },
  856. fail: (err) => {
  857. console.error('跳转失败:', err)
  858. uni.showToast({
  859. title: '跳转失败,请重试',
  860. icon: 'none'
  861. })
  862. }
  863. })
  864. } else {
  865. // 未注册,提示用户
  866. uni.showToast({
  867. title: '该用户还未注册用户端',
  868. icon: 'none',
  869. duration: 2000
  870. })
  871. }
  872. },
  873. // 添加资源
  874. handleAdd() {
  875. // 跳转到信息录入页面
  876. uni.navigateTo({
  877. url: '/pages/matchmaker-workbench/resource-input',
  878. success: () => {
  879. console.log('跳转到信息录入页面成功')
  880. },
  881. fail: (err) => {
  882. console.error('跳转失败:', err)
  883. uni.showToast({
  884. title: '页面跳转失败',
  885. icon: 'none'
  886. })
  887. }
  888. })
  889. },
  890. // 导航到工作台
  891. navigateToWorkbench() {
  892. uni.navigateTo({
  893. url: '/pages/matchmaker-workbench/index'
  894. })
  895. },
  896. // 导航到我的资源
  897. navigateToMyResources() {
  898. // 已在我的资源页面,无需跳转
  899. },
  900. // 导航到排行榜
  901. navigateToRanking() {
  902. uni.navigateTo({
  903. url: '/pages/matchmaker-workbench/ranking'
  904. })
  905. },
  906. // 导航到消息
  907. navigateToMessage() {
  908. uni.navigateTo({
  909. url: '/pages/matchmaker-workbench/message'
  910. })
  911. },
  912. // 导航到我的
  913. navigateToMine() {
  914. uni.navigateTo({
  915. url: '/pages/matchmaker-workbench/mine'
  916. })
  917. },
  918. // 启动自动刷新
  919. startAutoRefresh() {
  920. // 清除之前的定时器
  921. this.stopAutoRefresh()
  922. // 每30秒刷新一次,检查用户注册状态变化
  923. this.refreshTimer = setInterval(async () => {
  924. console.log('定时刷新:检查用户注册状态变化')
  925. // 先批量检查并更新线索用户的注册状态
  926. await this.batchCheckClueRegistrationStatus()
  927. // 然后刷新资源列表
  928. await this.loadMyResources()
  929. }, 30000) // 30秒
  930. },
  931. // 批量检查并更新线索用户的注册状态
  932. async batchCheckClueRegistrationStatus() {
  933. try {
  934. // 获取红娘ID,优先使用userInfo中的matchmakerId,否则使用userId
  935. const userInfo = uni.getStorageSync('userInfo') || {}
  936. const userId = uni.getStorageSync('userId')
  937. let matchmakerId = userInfo.matchmakerId || userId || null
  938. if (!matchmakerId) {
  939. console.warn('未找到红娘ID,跳过批量检查线索用户注册状态')
  940. return
  941. }
  942. // 确保matchmakerId是有效的整数
  943. matchmakerId = parseInt(matchmakerId)
  944. if (isNaN(matchmakerId) || matchmakerId <= 0) {
  945. console.warn('红娘ID无效,跳过批量检查线索用户注册状态')
  946. return
  947. }
  948. const baseUrl = process.env.NODE_ENV === 'development'
  949. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  950. : 'https://your-domain.com/api' // 生产环境
  951. console.log('开始批量检查线索用户注册状态...', '红娘ID:', matchmakerId)
  952. const [error, res] = await uni.request({
  953. url: `${baseUrl}/my-resource/batch-check-clue-registration`,
  954. method: 'POST',
  955. data: {
  956. matchmakerId: matchmakerId
  957. }
  958. })
  959. if (error) {
  960. console.error('批量检查线索用户注册状态失败:', error)
  961. return
  962. }
  963. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  964. const updateCount = res.data.data?.updateCount || 0
  965. if (updateCount > 0) {
  966. console.log(`✅ 批量检查完成,更新了 ${updateCount} 条线索用户记录`)
  967. } else {
  968. console.log('✅ 批量检查完成,没有需要更新的线索用户')
  969. }
  970. } else {
  971. console.warn('批量检查线索用户注册状态返回异常:', res.data)
  972. }
  973. } catch (e) {
  974. console.error('批量检查线索用户注册状态时发生异常:', e)
  975. }
  976. },
  977. // 停止自动刷新
  978. stopAutoRefresh() {
  979. if (this.refreshTimer) {
  980. clearInterval(this.refreshTimer)
  981. this.refreshTimer = null
  982. }
  983. },
  984. // 已注册资源图片加载错误处理
  985. handleImageErrorRegistered(index) {
  986. this.handleImageError(index, 'registered')
  987. },
  988. // 未注册资源图片加载错误处理
  989. handleImageErrorUnregistered(index) {
  990. this.handleImageError(index, 'unregistered')
  991. },
  992. // 已注册资源图片加载成功处理
  993. handleImageLoadRegistered(index) {
  994. this.handleImageLoad(index, 'registered')
  995. },
  996. // 未注册资源图片加载成功处理
  997. handleImageLoadUnregistered(index) {
  998. this.handleImageLoad(index, 'unregistered')
  999. },
  1000. // 图片加载错误处理(内部方法)
  1001. handleImageError(index, type) {
  1002. try {
  1003. // 获取当前显示的数据
  1004. const displayData = type === 'registered' ? this.getRegisteredDisplayData : this.getUnregisteredDisplayData
  1005. const resource = displayData && displayData[index]
  1006. if (!resource) {
  1007. console.warn('图片加载失败:资源不存在,index:', index, 'type:', type)
  1008. return
  1009. }
  1010. const originalUrl = resource.avatar
  1011. const resourceId = resource.id
  1012. console.error('❌ 图片加载失败:', {
  1013. index: index,
  1014. type: type,
  1015. resourceName: resource.name,
  1016. resourceId: resourceId,
  1017. originalAvatarUrl: originalUrl,
  1018. 'URL类型': typeof originalUrl,
  1019. 'URL长度': originalUrl ? originalUrl.length : 0,
  1020. '是否包含placeholder': originalUrl && originalUrl.includes('placeholder'),
  1021. '完整resource对象': resource
  1022. })
  1023. // 如果图片加载失败,且不是已经设置的默认占位图,则设置为空(显示CSS背景)
  1024. // 避免重复设置导致循环
  1025. if (originalUrl &&
  1026. originalUrl.trim() !== '' &&
  1027. !originalUrl.includes('placeholder') &&
  1028. !originalUrl.includes('default') &&
  1029. !originalUrl.includes('via.placeholder')) {
  1030. // 根据resourceId在当前显示数据中找到对应的资源并更新
  1031. console.log('图片加载失败,将URL设置为空,显示CSS默认背景')
  1032. if (type === 'registered') {
  1033. // 更新显示数据
  1034. const displayListIndex = this.registeredResources.findIndex(item => item.id === resourceId)
  1035. if (displayListIndex !== -1) {
  1036. this.$set(this.registeredResources[displayListIndex], 'avatar', '')
  1037. }
  1038. } else {
  1039. // 更新显示数据
  1040. const displayListIndex = this.unregisteredResources.findIndex(item => item.id === resourceId)
  1041. if (displayListIndex !== -1) {
  1042. this.$set(this.unregisteredResources[displayListIndex], 'avatar', '')
  1043. }
  1044. }
  1045. } else {
  1046. console.log('图片加载失败,但URL已经是占位图或空,无需处理')
  1047. console.log('当前URL:', originalUrl)
  1048. }
  1049. } catch (e) {
  1050. console.error('处理图片错误时发生异常:', e)
  1051. }
  1052. },
  1053. // 图片加载成功处理(内部方法)
  1054. handleImageLoad(index, type) {
  1055. try {
  1056. // 获取当前显示的数据
  1057. const displayData = type === 'registered' ? this.getRegisteredDisplayData : this.getUnregisteredDisplayData
  1058. if (displayData && displayData[index]) {
  1059. console.log('图片加载成功:', {
  1060. index: index,
  1061. type: type,
  1062. resource: displayData[index]?.name,
  1063. avatarUrl: displayData[index]?.avatar
  1064. })
  1065. }
  1066. } catch (e) {
  1067. console.error('处理图片加载成功时发生异常:', e)
  1068. }
  1069. }
  1070. }
  1071. }
  1072. </script>
  1073. <style lang="scss" scoped>
  1074. .my-resources {
  1075. min-height: 100vh;
  1076. background: #F5F5F5;
  1077. display: flex;
  1078. flex-direction: column;
  1079. }
  1080. /* 顶部导航栏 */
  1081. .header {
  1082. display: flex;
  1083. align-items: center;
  1084. justify-content: space-between;
  1085. padding: 20rpx 30rpx;
  1086. padding-top: calc(20rpx + env(safe-area-inset-top));
  1087. background: #FFFFFF;
  1088. border-bottom: 1rpx solid #F0F0F0;
  1089. .header-title {
  1090. font-size: 36rpx;
  1091. font-weight: bold;
  1092. color: #9C27B0;
  1093. }
  1094. .header-right {
  1095. .dropdown-arrow {
  1096. font-size: 24rpx;
  1097. color: #9C27B0;
  1098. font-weight: normal;
  1099. }
  1100. }
  1101. }
  1102. /* 资源/线索切换标签 */
  1103. .tab-switcher {
  1104. display: flex;
  1105. align-items: center;
  1106. background: #FFFFFF;
  1107. border-bottom: 1rpx solid #F0F0F0;
  1108. padding: 0 30rpx;
  1109. .tab-item {
  1110. flex: 1;
  1111. display: flex;
  1112. align-items: center;
  1113. justify-content: center;
  1114. padding: 25rpx 0;
  1115. position: relative;
  1116. cursor: pointer;
  1117. transition: all 0.3s;
  1118. .tab-text {
  1119. font-size: 32rpx;
  1120. font-weight: 500;
  1121. color: #666;
  1122. margin-right: 8rpx;
  1123. }
  1124. .tab-count {
  1125. font-size: 26rpx;
  1126. color: #999;
  1127. }
  1128. &.active {
  1129. .tab-text {
  1130. color: #9C27B0;
  1131. font-weight: bold;
  1132. }
  1133. .tab-count {
  1134. color: #9C27B0;
  1135. }
  1136. &::after {
  1137. content: '';
  1138. position: absolute;
  1139. bottom: 0;
  1140. left: 0;
  1141. right: 0;
  1142. height: 4rpx;
  1143. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1144. border-radius: 2rpx 2rpx 0 0;
  1145. }
  1146. }
  1147. &:active {
  1148. opacity: 0.7;
  1149. }
  1150. }
  1151. }
  1152. /* 搜索栏 */
  1153. .search-bar {
  1154. padding: 20rpx;
  1155. background: #F5F5F5;
  1156. .search-input-wrapper {
  1157. display: flex;
  1158. align-items: center;
  1159. background: #FFFFFF;
  1160. border-radius: 30rpx;
  1161. padding: 12rpx 20rpx;
  1162. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  1163. .search-icon-small {
  1164. width: 32rpx;
  1165. height: 32rpx;
  1166. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>');
  1167. background-size: contain;
  1168. background-repeat: no-repeat;
  1169. background-position: center;
  1170. margin-right: 15rpx;
  1171. }
  1172. .search-input {
  1173. flex: 1;
  1174. font-size: 28rpx;
  1175. color: #333;
  1176. border: none;
  1177. outline: none;
  1178. background: transparent;
  1179. &::placeholder {
  1180. color: #999;
  1181. }
  1182. }
  1183. }
  1184. }
  1185. .content {
  1186. flex: 1;
  1187. height: 0; /* 配合 flex: 1 使用,确保 scroll-view 有明确高度 */
  1188. padding: 0 20rpx 140rpx;
  1189. box-sizing: border-box;
  1190. }
  1191. /* 资源分组 */
  1192. .resource-section {
  1193. margin-bottom: 30rpx;
  1194. min-height: calc(50vh - 100rpx); /* 确保至少占半屏高度 */
  1195. display: flex;
  1196. flex-direction: column;
  1197. .section-header {
  1198. display: flex;
  1199. align-items: center;
  1200. padding: 20rpx 0 15rpx;
  1201. margin-bottom: 10rpx;
  1202. flex-shrink: 0;
  1203. .section-title {
  1204. font-size: 32rpx;
  1205. font-weight: bold;
  1206. color: #9C27B0;
  1207. margin-right: 10rpx;
  1208. }
  1209. .section-count {
  1210. font-size: 26rpx;
  1211. color: #999;
  1212. font-weight: normal;
  1213. }
  1214. }
  1215. /* 资源列表容器,确保有固定高度 */
  1216. .resource-list-container {
  1217. flex: 1;
  1218. display: flex;
  1219. flex-direction: column;
  1220. min-height: calc(50vh - 200rpx);
  1221. }
  1222. }
  1223. /* 加载更多提示 */
  1224. .load-more-tip {
  1225. display: flex;
  1226. justify-content: center;
  1227. align-items: center;
  1228. padding: 30rpx 0;
  1229. .load-more-text {
  1230. font-size: 26rpx;
  1231. color: #999;
  1232. }
  1233. }
  1234. /* 没有更多数据提示 */
  1235. .no-more-tip {
  1236. display: flex;
  1237. justify-content: center;
  1238. align-items: center;
  1239. padding: 30rpx 0;
  1240. .no-more-text {
  1241. font-size: 26rpx;
  1242. color: #999;
  1243. }
  1244. }
  1245. /* 空状态 */
  1246. .empty-state {
  1247. display: flex;
  1248. justify-content: center;
  1249. align-items: center;
  1250. padding: 100rpx 0;
  1251. .empty-text {
  1252. font-size: 28rpx;
  1253. color: #999;
  1254. }
  1255. }
  1256. /* 资源列表 */
  1257. .resource-item {
  1258. background: #FFFFFF;
  1259. border-radius: 20rpx;
  1260. margin-bottom: 20rpx;
  1261. padding: 25rpx;
  1262. position: relative;
  1263. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  1264. /* 右上角选中图标 */
  1265. .select-icon {
  1266. position: absolute;
  1267. top: 20rpx;
  1268. right: 20rpx;
  1269. width: 40rpx;
  1270. height: 40rpx;
  1271. background: #FF4444;
  1272. color: #FFFFFF;
  1273. border-radius: 50%;
  1274. display: flex;
  1275. align-items: center;
  1276. justify-content: center;
  1277. font-size: 24rpx;
  1278. font-weight: bold;
  1279. z-index: 10;
  1280. }
  1281. .resource-header {
  1282. display: flex;
  1283. align-items: flex-start;
  1284. margin-bottom: 20rpx;
  1285. padding-right: 50rpx;
  1286. .resource-avatar {
  1287. width: 100rpx;
  1288. height: 100rpx;
  1289. border-radius: 8rpx;
  1290. margin-right: 20rpx;
  1291. flex-shrink: 0;
  1292. background-color: #F5F5F5;
  1293. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23CCCCCC"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
  1294. background-size: 60% 60%;
  1295. background-position: center;
  1296. background-repeat: no-repeat;
  1297. }
  1298. .resource-basic-info {
  1299. flex: 1;
  1300. min-width: 0;
  1301. .name-gender {
  1302. display: flex;
  1303. align-items: center;
  1304. gap: 10rpx;
  1305. margin-bottom: 12rpx;
  1306. .resource-name {
  1307. font-size: 30rpx;
  1308. font-weight: bold;
  1309. color: #333;
  1310. }
  1311. .resource-gender {
  1312. font-size: 26rpx;
  1313. color: #666;
  1314. &.gender-tag {
  1315. display: inline-block;
  1316. padding: 4rpx 12rpx;
  1317. background: #F3E5F5;
  1318. color: #9C27B0;
  1319. border-radius: 12rpx;
  1320. font-size: 22rpx;
  1321. font-weight: 500;
  1322. }
  1323. }
  1324. }
  1325. .status-tag-wrapper {
  1326. display: flex;
  1327. align-items: center;
  1328. .status-tag {
  1329. display: inline-block;
  1330. padding: 4rpx 12rpx;
  1331. border-radius: 12rpx;
  1332. font-size: 22rpx;
  1333. font-weight: 500;
  1334. margin-right: 8rpx;
  1335. &.approved {
  1336. background: #E3F2FD;
  1337. color: #2196F3;
  1338. }
  1339. &.pending {
  1340. background: #FFF3E0;
  1341. color: #FF9800;
  1342. }
  1343. &.register-tag {
  1344. &.registered {
  1345. background: #E8F5E9;
  1346. color: #4CAF50;
  1347. }
  1348. &.unregistered {
  1349. background: #FCE4EC;
  1350. color: #E91E63;
  1351. }
  1352. }
  1353. &.match-tag {
  1354. margin-left: 10rpx;
  1355. &.matched {
  1356. background: #E3F2FD;
  1357. color: #2196F3;
  1358. }
  1359. &.unmatched {
  1360. background: #FFF3E0;
  1361. color: #FF9800;
  1362. }
  1363. }
  1364. /* 审核状态标签样式 */
  1365. &.audit-tag {
  1366. margin-left: 10rpx;
  1367. /* 待审核 - 橙色/黄色 */
  1368. &.audit-pending {
  1369. background: #FFF3E0;
  1370. color: #FF9800;
  1371. border: 1rpx solid #FFB74D;
  1372. }
  1373. /* 审核通过 - 绿色 */
  1374. &.audit-approved {
  1375. background: #E8F5E9;
  1376. color: #4CAF50;
  1377. border: 1rpx solid #81C784;
  1378. }
  1379. /* 审核未通过 - 红色 */
  1380. &.audit-rejected {
  1381. background: #FFEBEE;
  1382. color: #F44336;
  1383. border: 1rpx solid #E57373;
  1384. }
  1385. }
  1386. }
  1387. }
  1388. }
  1389. }
  1390. .resource-details {
  1391. .labels {
  1392. display: flex;
  1393. flex-wrap: wrap;
  1394. gap: 10rpx;
  1395. margin-bottom: 15rpx;
  1396. .label {
  1397. display: inline-block;
  1398. padding: 6rpx 14rpx;
  1399. background: #F3E5F5;
  1400. color: #9C27B0;
  1401. border-radius: 15rpx;
  1402. font-size: 22rpx;
  1403. font-weight: 500;
  1404. }
  1405. }
  1406. .requirement-box {
  1407. background: #F3E5F5;
  1408. border-radius: 12rpx;
  1409. padding: 16rpx 20rpx;
  1410. margin-bottom: 15rpx;
  1411. display: flex;
  1412. align-items: center;
  1413. flex-wrap: wrap;
  1414. .requirement-label {
  1415. font-size: 28rpx;
  1416. color: #7B1FA2;
  1417. font-weight: 500;
  1418. margin-right: 8rpx;
  1419. white-space: nowrap;
  1420. }
  1421. .requirement-content {
  1422. font-size: 28rpx;
  1423. color: #7B1FA2;
  1424. font-weight: 400;
  1425. }
  1426. }
  1427. .contact-box {
  1428. margin-bottom: 20rpx;
  1429. display: flex;
  1430. align-items: center;
  1431. flex-wrap: wrap;
  1432. .contact-label {
  1433. font-size: 26rpx;
  1434. color: #333;
  1435. font-weight: 500;
  1436. margin-right: 10rpx;
  1437. white-space: nowrap;
  1438. }
  1439. .contact-number {
  1440. flex: 1;
  1441. font-size: 26rpx;
  1442. color: #333;
  1443. font-weight: 400;
  1444. min-width: 0;
  1445. }
  1446. .copy-btn {
  1447. font-size: 24rpx;
  1448. color: #9C27B0;
  1449. font-weight: 500;
  1450. margin-left: 15rpx;
  1451. white-space: nowrap;
  1452. cursor: pointer;
  1453. }
  1454. }
  1455. .action-buttons {
  1456. display: flex;
  1457. justify-content: space-between;
  1458. align-items: center;
  1459. gap: 15rpx;
  1460. .delete-btn {
  1461. flex: 1;
  1462. padding: 14rpx 0;
  1463. background: #FFEBEE;
  1464. color: #E91E63;
  1465. border-radius: 25rpx;
  1466. font-size: 26rpx;
  1467. font-weight: 500;
  1468. text-align: center;
  1469. }
  1470. .match-btn {
  1471. flex: 1.5;
  1472. padding: 14rpx 0;
  1473. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1474. color: #FFFFFF;
  1475. border-radius: 25rpx;
  1476. font-size: 26rpx;
  1477. font-weight: 500;
  1478. text-align: center;
  1479. }
  1480. }
  1481. }
  1482. /* 优质资源标签 */
  1483. .quality-tag {
  1484. position: absolute;
  1485. top: 80rpx;
  1486. right: 20rpx;
  1487. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1488. color: #FFFFFF;
  1489. font-size: 22rpx;
  1490. font-weight: bold;
  1491. padding: 8rpx 16rpx;
  1492. border-radius: 20rpx;
  1493. display: flex;
  1494. align-items: center;
  1495. gap: 4rpx;
  1496. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.3);
  1497. z-index: 5;
  1498. .quality-star {
  1499. font-size: 20rpx;
  1500. }
  1501. .quality-text {
  1502. font-size: 22rpx;
  1503. }
  1504. }
  1505. }
  1506. /* 添加按钮 */
  1507. .add-button {
  1508. position: fixed;
  1509. bottom: 130rpx;
  1510. right: 40rpx;
  1511. width: 100rpx;
  1512. height: 100rpx;
  1513. border-radius: 50%;
  1514. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1515. display: flex;
  1516. align-items: center;
  1517. justify-content: center;
  1518. box-shadow: 0 4rpx 20rpx rgba(156, 39, 176, 0.4);
  1519. z-index: 999;
  1520. .add-button-icon {
  1521. font-size: 60rpx;
  1522. color: #FFFFFF;
  1523. line-height: 1;
  1524. font-weight: bold;
  1525. }
  1526. }
  1527. /* 底部导航 */
  1528. .tabbar {
  1529. position: fixed;
  1530. bottom: 0;
  1531. left: 0;
  1532. right: 0;
  1533. height: 100rpx;
  1534. background: #FFFFFF;
  1535. border-top: 1rpx solid #F0F0F0;
  1536. display: flex;
  1537. justify-content: space-around;
  1538. align-items: center;
  1539. padding-bottom: env(safe-area-inset-bottom);
  1540. .tabbar-item {
  1541. display: flex;
  1542. flex-direction: column;
  1543. align-items: center;
  1544. gap: 8rpx;
  1545. padding: 10rpx 0;
  1546. .tabbar-icon {
  1547. width: 44rpx;
  1548. height: 44rpx;
  1549. background-size: contain;
  1550. background-repeat: no-repeat;
  1551. background-position: center;
  1552. position: relative;
  1553. .badge {
  1554. position: absolute;
  1555. top: -8rpx;
  1556. right: -8rpx;
  1557. background: #FF4444;
  1558. color: #FFFFFF;
  1559. font-size: 20rpx;
  1560. font-weight: bold;
  1561. width: 32rpx;
  1562. height: 32rpx;
  1563. display: flex;
  1564. align-items: center;
  1565. justify-content: center;
  1566. border-radius: 16rpx;
  1567. }
  1568. }
  1569. .tabbar-text {
  1570. font-size: 20rpx;
  1571. color: #999;
  1572. }
  1573. &.active {
  1574. .tabbar-text {
  1575. color: #9C27B0;
  1576. font-weight: bold;
  1577. }
  1578. }
  1579. &.home .tabbar-icon {
  1580. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
  1581. }
  1582. &.active.home .tabbar-icon {
  1583. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
  1584. }
  1585. &.resources .tabbar-icon {
  1586. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>');
  1587. }
  1588. &.trophy .tabbar-icon {
  1589. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M18 6l-1.42 1.42-1.59-1.59L13 8.17l-1.42-1.42L9 8.17l-1.59-1.59L6 6l3 3V18c0 1.1.9 2 2 2h4c1.1 0 2-.9 2-2V9l3-3zm-4 12H8v-7.5l4-4 4 4V18z"/></svg>');
  1590. }
  1591. &.active.trophy .tabbar-icon {
  1592. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M18 6l-1.42 1.42-1.59-1.59L13 8.17l-1.42-1.42L9 8.17l-1.59-1.59L6 6l3 3V18c0 1.1.9 2 2 2h4c1.1 0 2-.9 2-2V9l3-3zm-4 12H8v-7.5l4-4 4 4V18z"/></svg>');
  1593. }
  1594. &.message .tabbar-icon {
  1595. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>');
  1596. }
  1597. &.active.message .tabbar-icon {
  1598. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>');
  1599. }
  1600. &.mine .tabbar-icon {
  1601. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
  1602. }
  1603. &.active.mine .tabbar-icon {
  1604. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
  1605. }
  1606. }
  1607. }
  1608. </style>