my-resources.vue 52 KB

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