my-resources.vue 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710
  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="100">
  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="handleResourceClick(item)">
  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="handleResourceClick(item)">
  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: 5, // 资源每页显示数量
  215. registeredTotal: 0, // 资源总数
  216. registeredLoading: false, // 资源加载中标志
  217. // 线索分页
  218. unregisteredPageNum: 1, // 线索当前页码
  219. unregisteredPageSize: 5, // 线索每页显示数量
  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() {
  346. if (this.currentTab === 'resource') {
  347. // 资源列表加载更多
  348. if (!this.registeredLoading && this.registeredResources.length < this.registeredTotal) {
  349. this.loadMoreRegistered()
  350. }
  351. } else {
  352. // 线索列表加载更多
  353. if (!this.unregisteredLoading && this.unregisteredResources.length < this.unregisteredTotal) {
  354. this.loadMoreUnregistered()
  355. }
  356. }
  357. },
  358. // 加载资源指定页数据
  359. loadRegisteredPage(pageNum) {
  360. if (!this.allRegisteredResources || this.allRegisteredResources.length === 0) {
  361. console.warn('loadRegisteredPage: allRegisteredResources为空')
  362. this.registeredResources = []
  363. return
  364. }
  365. const start = (pageNum - 1) * this.registeredPageSize
  366. const end = start + this.registeredPageSize
  367. this.registeredResources = this.allRegisteredResources.slice(0, end)
  368. console.log('loadRegisteredPage - pageNum:', pageNum, 'start:', start, 'end:', end, 'loaded:', this.registeredResources.length, 'total:', this.allRegisteredResources.length)
  369. },
  370. // 加载线索指定页数据
  371. loadUnregisteredPage(pageNum) {
  372. if (!this.allUnregisteredResources || this.allUnregisteredResources.length === 0) {
  373. console.warn('loadUnregisteredPage: allUnregisteredResources为空')
  374. this.unregisteredResources = []
  375. return
  376. }
  377. const start = (pageNum - 1) * this.unregisteredPageSize
  378. const end = start + this.unregisteredPageSize
  379. this.unregisteredResources = this.allUnregisteredResources.slice(0, end)
  380. console.log('loadUnregisteredPage - pageNum:', pageNum, 'start:', start, 'end:', end, 'loaded:', this.unregisteredResources.length, 'total:', this.allUnregisteredResources.length)
  381. },
  382. // 加载更多资源
  383. loadMoreRegistered() {
  384. if (this.registeredLoading) return
  385. if (this.registeredResources.length >= this.registeredTotal) return
  386. this.registeredLoading = true
  387. this.registeredPageNum++
  388. const start = (this.registeredPageNum - 1) * this.registeredPageSize
  389. const end = start + this.registeredPageSize
  390. const newData = this.allRegisteredResources.slice(start, end)
  391. this.registeredResources = [...this.registeredResources, ...newData]
  392. this.registeredLoading = false
  393. },
  394. // 加载更多线索
  395. loadMoreUnregistered() {
  396. if (this.unregisteredLoading) return
  397. if (this.unregisteredResources.length >= this.unregisteredTotal) return
  398. this.unregisteredLoading = true
  399. this.unregisteredPageNum++
  400. const start = (this.unregisteredPageNum - 1) * this.unregisteredPageSize
  401. const end = start + this.unregisteredPageSize
  402. const newData = this.allUnregisteredResources.slice(start, end)
  403. this.unregisteredResources = [...this.unregisteredResources, ...newData]
  404. this.unregisteredLoading = false
  405. },
  406. // 加载我的资源数据
  407. async loadMyResources() {
  408. try {
  409. // 获取当前登录用户ID
  410. const userInfo = uni.getStorageSync('userInfo') || {}
  411. const userId = uni.getStorageSync('userId')
  412. let currentUserId = userInfo.userId || userId || null
  413. // 验证并转换currentUserId为有效的整数
  414. if (currentUserId !== null && currentUserId !== undefined) {
  415. currentUserId = parseInt(currentUserId)
  416. if (isNaN(currentUserId) || currentUserId <= 0) {
  417. currentUserId = null
  418. }
  419. }
  420. if (!currentUserId) {
  421. console.error('无法获取当前登录用户ID')
  422. uni.showToast({
  423. title: '请先登录',
  424. icon: 'none'
  425. })
  426. return
  427. }
  428. // 调用后端接口,传递当前用户ID作为matchmakerId
  429. const baseUrl = process.env.NODE_ENV === 'development'
  430. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  431. : 'https://your-domain.com/api' // 生产环境
  432. // 构建查询参数,确保currentUserId是有效的整数
  433. let url = `${baseUrl}/my-resource/list?currentUserId=${currentUserId}&pageNum=1&pageSize=100`
  434. if (this.searchKeyword && this.searchKeyword.trim()) {
  435. url += `&keyword=${encodeURIComponent(this.searchKeyword.trim())}`
  436. }
  437. const [error, res] = await uni.request({
  438. url: url,
  439. method: 'GET'
  440. })
  441. if (error) {
  442. console.error('加载资源数据失败:', error)
  443. return
  444. }
  445. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  446. // 处理返回的数据
  447. const pageData = res.data.data
  448. console.log('=== 后端返回的完整数据 ===')
  449. console.log('pageData:', JSON.stringify(pageData, null, 2))
  450. if (pageData && pageData.records) {
  451. console.log('records数量:', pageData.records.length)
  452. if (pageData.records.length > 0) {
  453. console.log('第一条记录的完整数据:', JSON.stringify(pageData.records[0], null, 2))
  454. }
  455. // 将后端数据转换为前端需要的格式
  456. const allResources = pageData.records.map(item => {
  457. // 直接使用mate_selection_criteria字段作为择偶要求
  458. // 支持多种可能的字段名(mateSelectionCriteria或mate_selection_criteria)
  459. let requirement = item.mateSelectionCriteria || item.mate_selection_criteria || ''
  460. // 去除首尾空格,如果为空则显示"暂无要求"
  461. if (requirement) {
  462. requirement = requirement.trim()
  463. }
  464. // 处理标签(可以根据实际需求扩展)
  465. const labels = []
  466. if (item.constellation) {
  467. labels.push(item.constellation)
  468. }
  469. if (item.occupation) {
  470. labels.push(item.occupation)
  471. }
  472. // 处理头像URL:确保使用完整的URL
  473. // 尝试多种可能的字段名
  474. let avatarUrl = item.avatarUrl || item.avatar_url || item.avatar || ''
  475. console.log('=== 头像URL处理 ===')
  476. console.log('资源ID:', item.resourceId, '姓名:', item.name)
  477. console.log('原始数据字段:', {
  478. avatarUrl: item.avatarUrl,
  479. avatar_url: item.avatar_url,
  480. avatar: item.avatar,
  481. 'item完整对象': item
  482. })
  483. console.log('提取的avatarUrl值:', avatarUrl, '类型:', typeof avatarUrl, '是否为空:', !avatarUrl)
  484. // 如果头像URL为空或null,设置为空字符串(让CSS背景图显示)
  485. if (!avatarUrl || avatarUrl.trim() === '' || avatarUrl === 'null' || avatarUrl === null || avatarUrl === undefined) {
  486. avatarUrl = '' // 设置为空,让CSS默认背景显示
  487. console.log('⚠️ 头像URL为空,设置为空字符串,将显示CSS默认背景')
  488. } else {
  489. // 确保URL是完整的(如果已经是完整URL则直接使用)
  490. avatarUrl = avatarUrl.trim()
  491. // 如果URL不是以http开头,可能需要拼接基础URL(根据实际情况调整)
  492. if (!avatarUrl.startsWith('http://') && !avatarUrl.startsWith('https://')) {
  493. console.warn('⚠️ 头像URL不是完整URL,可能需要拼接:', avatarUrl)
  494. // 如果是相对路径,可以尝试拼接MinIO基础URL
  495. // avatarUrl = 'http://115.190.125.125:9000/' + avatarUrl
  496. }
  497. // 确保MinIO URL可以正常访问(可能需要处理跨域)
  498. // 如果MinIO配置了公共访问,直接使用URL即可
  499. console.log('✅ 使用用户头像URL:', avatarUrl)
  500. // 验证URL格式
  501. if (avatarUrl.includes('115.190.125.125:9000')) {
  502. console.log('✅ 检测到MinIO URL,URL格式:', avatarUrl)
  503. }
  504. }
  505. console.log('最终设置的avatarUrl:', avatarUrl)
  506. console.log('=== 头像URL处理结束 ===')
  507. // 确保resourceId是有效的整数
  508. let resourceId = item.resourceId || item.resource_id
  509. // 调试日志:检查resourceId的来源
  510. console.log('=== resourceId处理 ===')
  511. console.log('原始item.resourceId:', item.resourceId, '类型:', typeof item.resourceId)
  512. console.log('原始item.resource_id:', item.resource_id, '类型:', typeof item.resource_id)
  513. console.log('提取的resourceId:', resourceId, '类型:', typeof resourceId)
  514. if (resourceId === null || resourceId === undefined || resourceId === 'undefined' || resourceId === 'null') {
  515. console.warn('资源ID无效,跳过该资源:', item)
  516. return null // 返回null,后续会被filter过滤掉
  517. }
  518. resourceId = parseInt(resourceId)
  519. if (isNaN(resourceId) || resourceId <= 0) {
  520. console.warn('资源ID格式错误,跳过该资源:', item)
  521. return null // 返回null,后续会被filter过滤掉
  522. }
  523. console.log('处理后的resourceId:', resourceId, '类型:', typeof resourceId)
  524. console.log('=== resourceId处理结束 ===')
  525. // 处理isUser字段,支持多种可能的字段名(isUser或is_user)
  526. let isUser = item.isUser !== null && item.isUser !== undefined ? item.isUser :
  527. (item.is_user !== null && item.is_user !== undefined ? item.is_user : 0)
  528. // 确保isUser是数字类型
  529. isUser = parseInt(isUser) || 0
  530. // 处理userId字段,支持多种可能的字段名(userId或user_id)
  531. let userId = item.userId !== null && item.userId !== undefined ? item.userId :
  532. (item.user_id !== null && item.user_id !== undefined ? item.user_id : null)
  533. // 确保userId是数字类型或null
  534. if (userId !== null && userId !== undefined) {
  535. userId = parseInt(userId)
  536. if (isNaN(userId) || userId <= 0) {
  537. userId = null
  538. }
  539. }
  540. // 处理isMatch字段
  541. let isMatch = item.isMatch !== null && item.isMatch !== undefined ? item.isMatch :
  542. (item.is_match !== null && item.is_match !== undefined ? item.is_match : 0)
  543. isMatch = parseInt(isMatch) || 0
  544. // 调试日志:检查isUser和userId字段
  545. console.log('=== isUser和userId字段处理 ===')
  546. console.log('资源ID:', resourceId, '姓名:', item.name)
  547. console.log('原始数据:', {
  548. isUser: item.isUser,
  549. is_user: item.is_user,
  550. userId: item.userId,
  551. user_id: item.user_id,
  552. 'item完整对象': item
  553. })
  554. console.log('处理后的isUser值:', isUser, '类型:', typeof isUser)
  555. console.log('处理后的userId值:', userId, '类型:', typeof userId)
  556. console.log('=== isUser和userId字段处理结束 ===')
  557. // 处理标签(从后端返回的tags字段,如果没有则使用原有的labels)
  558. let resourceTags = []
  559. if (item.tags && Array.isArray(item.tags) && item.tags.length > 0) {
  560. // 使用后端返回的标签
  561. resourceTags = item.tags
  562. } else if (labels && labels.length > 0) {
  563. // 如果没有tags,使用原有的labels(星座、职业等)
  564. resourceTags = labels
  565. }
  566. const resourceObj = {
  567. id: resourceId, // 主要使用的ID字段
  568. resourceId: resourceId, // 备用ID字段,确保兼容性
  569. resource_id: resourceId, // 备用ID字段,确保兼容性
  570. avatar: avatarUrl, // 使用处理后的头像URL
  571. name: item.name || '',
  572. gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
  573. status: item.status !== undefined && item.status !== null ? item.status : 0, // 审核状态,默认为0(待审核)
  574. isUser: isUser, // 添加isUser字段,默认为0
  575. userId: userId, // 添加userId字段,可能为null
  576. isMatch: isMatch, // 添加isMatch字段,默认为0
  577. isPlus: false,
  578. labels: resourceTags, // 使用处理后的标签列表
  579. requirement: requirement || '暂无要求',
  580. contact: item.phone ? item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  581. originalPhone: item.phone || '', // 保存原始完整手机号用于复制
  582. isQuality: false
  583. }
  584. console.log('创建资源对象 - ID:', resourceObj.id, '名称:', resourceObj.name, 'isUser:', resourceObj.isUser, 'userId:', resourceObj.userId)
  585. return resourceObj
  586. }).filter(item => item !== null) // 过滤掉无效的资源(resourceId无效的)
  587. // 将资源分为两部分:已注册和未注册(保存全部数据)
  588. // 已注册用户条件:isUser === 1 且 userId !== null 且 userId !== undefined
  589. this.allRegisteredResources = allResources.filter(item => {
  590. const isRegistered = item.isUser === 1 && item.userId !== null && item.userId !== undefined
  591. return isRegistered
  592. })
  593. // 未注册用户(线索):isUser !== 1 或 userId === null 或 userId === undefined
  594. this.allUnregisteredResources = allResources.filter(item => {
  595. const isUnregistered = item.isUser !== 1 || item.userId === null || item.userId === undefined
  596. return isUnregistered
  597. })
  598. // 设置总数
  599. this.registeredTotal = this.allRegisteredResources.length
  600. this.unregisteredTotal = this.allUnregisteredResources.length
  601. console.log('数据分类完成 - 全部资源:', allResources.length, '已注册:', this.allRegisteredResources.length, '未注册:', this.allUnregisteredResources.length)
  602. // 根据当前标签加载第一页数据
  603. if (this.currentTab === 'resource') {
  604. this.registeredPageNum = 1
  605. this.loadRegisteredPage(1)
  606. console.log('加载资源第一页后 - registeredResources.length:', this.registeredResources.length)
  607. } else {
  608. this.unregisteredPageNum = 1
  609. this.loadUnregisteredPage(1)
  610. console.log('加载线索第一页后 - unregisteredResources.length:', this.unregisteredResources.length)
  611. }
  612. // 保留resources用于兼容(包含所有资源)
  613. this.resources = allResources
  614. console.log('处理后的资源列表 - 已注册:', this.registeredResources.length, '未注册:', this.unregisteredResources.length)
  615. }
  616. }
  617. } catch (e) {
  618. console.error('加载资源数据失败:', e)
  619. }
  620. },
  621. // 搜索
  622. async handleSearch() {
  623. // 重置分页到第一页
  624. this.registeredPageNum = 1
  625. this.unregisteredPageNum = 1
  626. // 重新加载资源数据,包含搜索关键词
  627. await this.loadMyResources()
  628. },
  629. // 删除资源
  630. async handleDelete(id) {
  631. // 如果id无效,尝试从当前资源列表中查找
  632. if (id === null || id === undefined || id === 'undefined' || id === 'null' || id === '') {
  633. console.warn('删除资源失败: 资源ID无效,尝试从资源列表中查找', id)
  634. // 尝试从当前显示的资源中找到对应的资源
  635. const allDisplayData = this.currentTab === 'resource'
  636. ? this.getRegisteredDisplayData
  637. : this.getUnregisteredDisplayData
  638. // 如果仍然找不到,显示错误
  639. console.error('删除资源失败: 无法获取资源ID', id)
  640. uni.showToast({
  641. title: '资源ID无效,无法删除',
  642. icon: 'none'
  643. })
  644. return
  645. }
  646. // 确保id是有效的整数
  647. let resourceId = parseInt(id)
  648. if (isNaN(resourceId) || resourceId <= 0) {
  649. // 如果解析失败,尝试从字符串中提取数字
  650. const match = String(id).match(/\d+/)
  651. if (match) {
  652. resourceId = parseInt(match[0])
  653. }
  654. if (isNaN(resourceId) || resourceId <= 0) {
  655. console.error('删除资源失败: 资源ID格式错误', id, '类型:', typeof id)
  656. uni.showToast({
  657. title: '资源ID格式错误,无法删除',
  658. icon: 'none'
  659. })
  660. return
  661. }
  662. }
  663. console.log('准备删除资源,ID:', resourceId, '原始ID:', id)
  664. uni.showModal({
  665. title: '删除确认',
  666. content: '确定要删除该资源吗?',
  667. success: async (res) => {
  668. if (res.confirm) {
  669. try {
  670. uni.showLoading({
  671. title: '删除中...'
  672. })
  673. const baseUrl = process.env.NODE_ENV === 'development'
  674. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  675. : 'https://your-domain.com/api' // 生产环境
  676. const [error, deleteRes] = await uni.request({
  677. url: `${baseUrl}/my-resource/delete/${resourceId}`,
  678. method: 'DELETE'
  679. })
  680. uni.hideLoading()
  681. if (error) {
  682. console.error('删除资源失败:', error)
  683. uni.showToast({
  684. title: '删除失败',
  685. icon: 'none'
  686. })
  687. return
  688. }
  689. if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.code === 200) {
  690. uni.showToast({
  691. title: '删除成功',
  692. icon: 'success'
  693. })
  694. // 删除成功后刷新资源列表
  695. await this.loadMyResources()
  696. } else {
  697. uni.showToast({
  698. title: deleteRes.data.message || '删除失败',
  699. icon: 'none'
  700. })
  701. }
  702. } catch (e) {
  703. uni.hideLoading()
  704. console.error('删除资源异常:', e)
  705. uni.showToast({
  706. title: '删除失败,请稍后重试',
  707. icon: 'none'
  708. })
  709. }
  710. }
  711. }
  712. })
  713. },
  714. // 精准匹配
  715. handleMatch(id) {
  716. // 查找资源项
  717. const allResources = [...this.registeredResources, ...this.unregisteredResources]
  718. const resource = allResources.find(item => item.id === id)
  719. if (!resource) {
  720. uni.showToast({
  721. title: '资源不存在',
  722. icon: 'none'
  723. })
  724. return
  725. }
  726. // 检查用户是否已注册(isUser === 1 且 userId !== null)
  727. if (resource.isUser !== 1 || resource.userId === null || resource.userId === undefined) {
  728. uni.showToast({
  729. title: '该用户还未注册用户端,注册之后才能进行匹配',
  730. icon: 'none',
  731. duration: 3000
  732. })
  733. return
  734. }
  735. // 已注册用户,跳转到精准匹配页面
  736. console.log('精准匹配:', id)
  737. uni.navigateTo({
  738. url: `/pages/matchmaker-workbench/precise-match?resourceId=${id}`
  739. })
  740. },
  741. // 复制联系方式
  742. handleCopyContact(contact) {
  743. if (!contact) {
  744. uni.showToast({
  745. title: '联系方式为空',
  746. icon: 'none'
  747. })
  748. return
  749. }
  750. // 如果是脱敏的手机号,需要从原始数据中获取完整号码
  751. // 这里需要根据实际情况调整,可能需要从item中获取原始phone
  752. // 暂时先复制显示的文本
  753. uni.setClipboardData({
  754. data: contact,
  755. success: () => {
  756. uni.showToast({
  757. title: '已复制到剪贴板',
  758. icon: 'success'
  759. })
  760. },
  761. fail: () => {
  762. uni.showToast({
  763. title: '复制失败',
  764. icon: 'none'
  765. })
  766. }
  767. })
  768. },
  769. // 客户点击事件,跳转到客户详情页面
  770. handleClientClick(id) {
  771. uni.navigateTo({
  772. url: `/pages/matchmaker-workbench/client-detail?resourceId=${id}`
  773. })
  774. },
  775. // 资源项点击事件
  776. handleResourceClick(item) {
  777. console.log('=== 点击资源项 ===')
  778. console.log('item对象:', JSON.stringify(item, null, 2))
  779. console.log('item.id:', item.id)
  780. console.log('item.isUser:', item.isUser)
  781. // 判断是否为已注册用户(isUser === 1 且 userId !== null)
  782. if (item.isUser === 1 && item.userId !== null && item.userId !== undefined) {
  783. // 已注册,跳转到客户详情页面
  784. const resourceId = item.id
  785. console.log('准备跳转,resourceId:', resourceId)
  786. uni.navigateTo({
  787. url: `/pages/matchmaker-workbench/client-detail?resourceId=${resourceId}`,
  788. success: () => {
  789. console.log('跳转成功,resourceId:', resourceId)
  790. },
  791. fail: (err) => {
  792. console.error('跳转失败:', err)
  793. }
  794. })
  795. } else {
  796. // 未注册,提示用户
  797. uni.showToast({
  798. title: '该用户还未注册用户端',
  799. icon: 'none',
  800. duration: 2000
  801. })
  802. }
  803. },
  804. // 添加资源
  805. handleAdd() {
  806. // 跳转到信息录入页面
  807. uni.navigateTo({
  808. url: '/pages/matchmaker-workbench/resource-input',
  809. success: () => {
  810. console.log('跳转到信息录入页面成功')
  811. },
  812. fail: (err) => {
  813. console.error('跳转失败:', err)
  814. uni.showToast({
  815. title: '页面跳转失败',
  816. icon: 'none'
  817. })
  818. }
  819. })
  820. },
  821. // 导航到工作台
  822. navigateToWorkbench() {
  823. uni.navigateTo({
  824. url: '/pages/matchmaker-workbench/index'
  825. })
  826. },
  827. // 导航到我的资源
  828. navigateToMyResources() {
  829. // 已在我的资源页面,无需跳转
  830. },
  831. // 导航到排行榜
  832. navigateToRanking() {
  833. uni.navigateTo({
  834. url: '/pages/matchmaker-workbench/ranking'
  835. })
  836. },
  837. // 导航到消息
  838. navigateToMessage() {
  839. uni.navigateTo({
  840. url: '/pages/matchmaker-workbench/message'
  841. })
  842. },
  843. // 导航到我的
  844. navigateToMine() {
  845. uni.navigateTo({
  846. url: '/pages/matchmaker-workbench/mine'
  847. })
  848. },
  849. // 启动自动刷新
  850. startAutoRefresh() {
  851. // 清除之前的定时器
  852. this.stopAutoRefresh()
  853. // 每30秒刷新一次,检查用户注册状态变化
  854. this.refreshTimer = setInterval(async () => {
  855. console.log('定时刷新:检查用户注册状态变化')
  856. // 先批量检查并更新线索用户的注册状态
  857. await this.batchCheckClueRegistrationStatus()
  858. // 然后刷新资源列表
  859. await this.loadMyResources()
  860. }, 30000) // 30秒
  861. },
  862. // 批量检查并更新线索用户的注册状态
  863. async batchCheckClueRegistrationStatus() {
  864. try {
  865. // 获取红娘ID,优先使用userInfo中的matchmakerId,否则使用userId
  866. const userInfo = uni.getStorageSync('userInfo') || {}
  867. const userId = uni.getStorageSync('userId')
  868. let matchmakerId = userInfo.matchmakerId || userId || null
  869. if (!matchmakerId) {
  870. console.warn('未找到红娘ID,跳过批量检查线索用户注册状态')
  871. return
  872. }
  873. // 确保matchmakerId是有效的整数
  874. matchmakerId = parseInt(matchmakerId)
  875. if (isNaN(matchmakerId) || matchmakerId <= 0) {
  876. console.warn('红娘ID无效,跳过批量检查线索用户注册状态')
  877. return
  878. }
  879. const baseUrl = process.env.NODE_ENV === 'development'
  880. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  881. : 'https://your-domain.com/api' // 生产环境
  882. console.log('开始批量检查线索用户注册状态...', '红娘ID:', matchmakerId)
  883. const [error, res] = await uni.request({
  884. url: `${baseUrl}/my-resource/batch-check-clue-registration`,
  885. method: 'POST',
  886. data: {
  887. matchmakerId: matchmakerId
  888. }
  889. })
  890. if (error) {
  891. console.error('批量检查线索用户注册状态失败:', error)
  892. return
  893. }
  894. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  895. const updateCount = res.data.data?.updateCount || 0
  896. if (updateCount > 0) {
  897. console.log(`✅ 批量检查完成,更新了 ${updateCount} 条线索用户记录`)
  898. } else {
  899. console.log('✅ 批量检查完成,没有需要更新的线索用户')
  900. }
  901. } else {
  902. console.warn('批量检查线索用户注册状态返回异常:', res.data)
  903. }
  904. } catch (e) {
  905. console.error('批量检查线索用户注册状态时发生异常:', e)
  906. }
  907. },
  908. // 停止自动刷新
  909. stopAutoRefresh() {
  910. if (this.refreshTimer) {
  911. clearInterval(this.refreshTimer)
  912. this.refreshTimer = null
  913. }
  914. },
  915. // 已注册资源图片加载错误处理
  916. handleImageErrorRegistered(index) {
  917. this.handleImageError(index, 'registered')
  918. },
  919. // 未注册资源图片加载错误处理
  920. handleImageErrorUnregistered(index) {
  921. this.handleImageError(index, 'unregistered')
  922. },
  923. // 已注册资源图片加载成功处理
  924. handleImageLoadRegistered(index) {
  925. this.handleImageLoad(index, 'registered')
  926. },
  927. // 未注册资源图片加载成功处理
  928. handleImageLoadUnregistered(index) {
  929. this.handleImageLoad(index, 'unregistered')
  930. },
  931. // 图片加载错误处理(内部方法)
  932. handleImageError(index, type) {
  933. try {
  934. // 获取当前显示的数据
  935. const displayData = type === 'registered' ? this.getRegisteredDisplayData : this.getUnregisteredDisplayData
  936. const resource = displayData && displayData[index]
  937. if (!resource) {
  938. console.warn('图片加载失败:资源不存在,index:', index, 'type:', type)
  939. return
  940. }
  941. const originalUrl = resource.avatar
  942. const resourceId = resource.id
  943. console.error('❌ 图片加载失败:', {
  944. index: index,
  945. type: type,
  946. resourceName: resource.name,
  947. resourceId: resourceId,
  948. originalAvatarUrl: originalUrl,
  949. 'URL类型': typeof originalUrl,
  950. 'URL长度': originalUrl ? originalUrl.length : 0,
  951. '是否包含placeholder': originalUrl && originalUrl.includes('placeholder'),
  952. '完整resource对象': resource
  953. })
  954. // 如果图片加载失败,且不是已经设置的默认占位图,则设置为空(显示CSS背景)
  955. // 避免重复设置导致循环
  956. if (originalUrl &&
  957. originalUrl.trim() !== '' &&
  958. !originalUrl.includes('placeholder') &&
  959. !originalUrl.includes('default') &&
  960. !originalUrl.includes('via.placeholder')) {
  961. // 根据resourceId在全部数据中找到对应的资源并更新
  962. console.log('图片加载失败,将URL设置为空,显示CSS默认背景')
  963. if (type === 'registered') {
  964. // 更新全部数据
  965. const allListIndex = this.allRegisteredResources.findIndex(item => item.id === resourceId)
  966. if (allListIndex !== -1) {
  967. this.$set(this.allRegisteredResources[allListIndex], 'avatar', '')
  968. }
  969. // 更新显示数据
  970. const displayListIndex = this.registeredResources.findIndex(item => item.id === resourceId)
  971. if (displayListIndex !== -1) {
  972. this.$set(this.registeredResources[displayListIndex], 'avatar', '')
  973. }
  974. } else {
  975. // 更新全部数据
  976. const allListIndex = this.allUnregisteredResources.findIndex(item => item.id === resourceId)
  977. if (allListIndex !== -1) {
  978. this.$set(this.allUnregisteredResources[allListIndex], 'avatar', '')
  979. }
  980. // 更新显示数据
  981. const displayListIndex = this.unregisteredResources.findIndex(item => item.id === resourceId)
  982. if (displayListIndex !== -1) {
  983. this.$set(this.unregisteredResources[displayListIndex], 'avatar', '')
  984. }
  985. }
  986. } else {
  987. console.log('图片加载失败,但URL已经是占位图或空,无需处理')
  988. console.log('当前URL:', originalUrl)
  989. }
  990. } catch (e) {
  991. console.error('处理图片错误时发生异常:', e)
  992. }
  993. },
  994. // 图片加载成功处理(内部方法)
  995. handleImageLoad(index, type) {
  996. try {
  997. // 获取当前显示的数据
  998. const displayData = type === 'registered' ? this.getRegisteredDisplayData : this.getUnregisteredDisplayData
  999. if (displayData && displayData[index]) {
  1000. console.log('图片加载成功:', {
  1001. index: index,
  1002. type: type,
  1003. resource: displayData[index]?.name,
  1004. avatarUrl: displayData[index]?.avatar
  1005. })
  1006. }
  1007. } catch (e) {
  1008. console.error('处理图片加载成功时发生异常:', e)
  1009. }
  1010. }
  1011. }
  1012. }
  1013. </script>
  1014. <style lang="scss" scoped>
  1015. .my-resources {
  1016. min-height: 100vh;
  1017. background: #F5F5F5;
  1018. display: flex;
  1019. flex-direction: column;
  1020. }
  1021. /* 顶部导航栏 */
  1022. .header {
  1023. display: flex;
  1024. align-items: center;
  1025. justify-content: space-between;
  1026. padding: 20rpx 30rpx;
  1027. padding-top: calc(20rpx + env(safe-area-inset-top));
  1028. background: #FFFFFF;
  1029. border-bottom: 1rpx solid #F0F0F0;
  1030. .header-title {
  1031. font-size: 36rpx;
  1032. font-weight: bold;
  1033. color: #9C27B0;
  1034. }
  1035. .header-right {
  1036. .dropdown-arrow {
  1037. font-size: 24rpx;
  1038. color: #9C27B0;
  1039. font-weight: normal;
  1040. }
  1041. }
  1042. }
  1043. /* 资源/线索切换标签 */
  1044. .tab-switcher {
  1045. display: flex;
  1046. align-items: center;
  1047. background: #FFFFFF;
  1048. border-bottom: 1rpx solid #F0F0F0;
  1049. padding: 0 30rpx;
  1050. .tab-item {
  1051. flex: 1;
  1052. display: flex;
  1053. align-items: center;
  1054. justify-content: center;
  1055. padding: 25rpx 0;
  1056. position: relative;
  1057. cursor: pointer;
  1058. transition: all 0.3s;
  1059. .tab-text {
  1060. font-size: 32rpx;
  1061. font-weight: 500;
  1062. color: #666;
  1063. margin-right: 8rpx;
  1064. }
  1065. .tab-count {
  1066. font-size: 26rpx;
  1067. color: #999;
  1068. }
  1069. &.active {
  1070. .tab-text {
  1071. color: #9C27B0;
  1072. font-weight: bold;
  1073. }
  1074. .tab-count {
  1075. color: #9C27B0;
  1076. }
  1077. &::after {
  1078. content: '';
  1079. position: absolute;
  1080. bottom: 0;
  1081. left: 0;
  1082. right: 0;
  1083. height: 4rpx;
  1084. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1085. border-radius: 2rpx 2rpx 0 0;
  1086. }
  1087. }
  1088. &:active {
  1089. opacity: 0.7;
  1090. }
  1091. }
  1092. }
  1093. /* 搜索栏 */
  1094. .search-bar {
  1095. padding: 20rpx;
  1096. background: #F5F5F5;
  1097. .search-input-wrapper {
  1098. display: flex;
  1099. align-items: center;
  1100. background: #FFFFFF;
  1101. border-radius: 30rpx;
  1102. padding: 12rpx 20rpx;
  1103. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  1104. .search-icon-small {
  1105. width: 32rpx;
  1106. height: 32rpx;
  1107. 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>');
  1108. background-size: contain;
  1109. background-repeat: no-repeat;
  1110. background-position: center;
  1111. margin-right: 15rpx;
  1112. }
  1113. .search-input {
  1114. flex: 1;
  1115. font-size: 28rpx;
  1116. color: #333;
  1117. border: none;
  1118. outline: none;
  1119. background: transparent;
  1120. &::placeholder {
  1121. color: #999;
  1122. }
  1123. }
  1124. }
  1125. }
  1126. .content {
  1127. flex: 1;
  1128. padding: 0 20rpx 140rpx;
  1129. }
  1130. /* 资源分组 */
  1131. .resource-section {
  1132. margin-bottom: 30rpx;
  1133. min-height: calc(50vh - 100rpx); /* 确保至少占半屏高度 */
  1134. display: flex;
  1135. flex-direction: column;
  1136. .section-header {
  1137. display: flex;
  1138. align-items: center;
  1139. padding: 20rpx 0 15rpx;
  1140. margin-bottom: 10rpx;
  1141. flex-shrink: 0;
  1142. .section-title {
  1143. font-size: 32rpx;
  1144. font-weight: bold;
  1145. color: #9C27B0;
  1146. margin-right: 10rpx;
  1147. }
  1148. .section-count {
  1149. font-size: 26rpx;
  1150. color: #999;
  1151. font-weight: normal;
  1152. }
  1153. }
  1154. /* 资源列表容器,确保有固定高度 */
  1155. .resource-list-container {
  1156. flex: 1;
  1157. display: flex;
  1158. flex-direction: column;
  1159. min-height: calc(50vh - 200rpx);
  1160. }
  1161. }
  1162. /* 加载更多提示 */
  1163. .load-more-tip {
  1164. display: flex;
  1165. justify-content: center;
  1166. align-items: center;
  1167. padding: 30rpx 0;
  1168. .load-more-text {
  1169. font-size: 26rpx;
  1170. color: #999;
  1171. }
  1172. }
  1173. /* 没有更多数据提示 */
  1174. .no-more-tip {
  1175. display: flex;
  1176. justify-content: center;
  1177. align-items: center;
  1178. padding: 30rpx 0;
  1179. .no-more-text {
  1180. font-size: 26rpx;
  1181. color: #999;
  1182. }
  1183. }
  1184. /* 空状态 */
  1185. .empty-state {
  1186. display: flex;
  1187. justify-content: center;
  1188. align-items: center;
  1189. padding: 100rpx 0;
  1190. .empty-text {
  1191. font-size: 28rpx;
  1192. color: #999;
  1193. }
  1194. }
  1195. /* 资源列表 */
  1196. .resource-item {
  1197. background: #FFFFFF;
  1198. border-radius: 20rpx;
  1199. margin-bottom: 20rpx;
  1200. padding: 25rpx;
  1201. position: relative;
  1202. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  1203. /* 右上角选中图标 */
  1204. .select-icon {
  1205. position: absolute;
  1206. top: 20rpx;
  1207. right: 20rpx;
  1208. width: 40rpx;
  1209. height: 40rpx;
  1210. background: #FF4444;
  1211. color: #FFFFFF;
  1212. border-radius: 50%;
  1213. display: flex;
  1214. align-items: center;
  1215. justify-content: center;
  1216. font-size: 24rpx;
  1217. font-weight: bold;
  1218. z-index: 10;
  1219. }
  1220. .resource-header {
  1221. display: flex;
  1222. align-items: flex-start;
  1223. margin-bottom: 20rpx;
  1224. padding-right: 50rpx;
  1225. .resource-avatar {
  1226. width: 100rpx;
  1227. height: 100rpx;
  1228. border-radius: 8rpx;
  1229. margin-right: 20rpx;
  1230. flex-shrink: 0;
  1231. background-color: #F5F5F5;
  1232. 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>');
  1233. background-size: 60% 60%;
  1234. background-position: center;
  1235. background-repeat: no-repeat;
  1236. }
  1237. .resource-basic-info {
  1238. flex: 1;
  1239. min-width: 0;
  1240. .name-gender {
  1241. display: flex;
  1242. align-items: center;
  1243. gap: 10rpx;
  1244. margin-bottom: 12rpx;
  1245. .resource-name {
  1246. font-size: 30rpx;
  1247. font-weight: bold;
  1248. color: #333;
  1249. }
  1250. .resource-gender {
  1251. font-size: 26rpx;
  1252. color: #666;
  1253. &.gender-tag {
  1254. display: inline-block;
  1255. padding: 4rpx 12rpx;
  1256. background: #F3E5F5;
  1257. color: #9C27B0;
  1258. border-radius: 12rpx;
  1259. font-size: 22rpx;
  1260. font-weight: 500;
  1261. }
  1262. }
  1263. }
  1264. .status-tag-wrapper {
  1265. display: flex;
  1266. align-items: center;
  1267. .status-tag {
  1268. display: inline-block;
  1269. padding: 4rpx 12rpx;
  1270. border-radius: 12rpx;
  1271. font-size: 22rpx;
  1272. font-weight: 500;
  1273. margin-right: 8rpx;
  1274. &.approved {
  1275. background: #E3F2FD;
  1276. color: #2196F3;
  1277. }
  1278. &.pending {
  1279. background: #FFF3E0;
  1280. color: #FF9800;
  1281. }
  1282. &.register-tag {
  1283. &.registered {
  1284. background: #E8F5E9;
  1285. color: #4CAF50;
  1286. }
  1287. &.unregistered {
  1288. background: #FCE4EC;
  1289. color: #E91E63;
  1290. }
  1291. }
  1292. &.match-tag {
  1293. margin-left: 10rpx;
  1294. &.matched {
  1295. background: #E3F2FD;
  1296. color: #2196F3;
  1297. }
  1298. &.unmatched {
  1299. background: #FFF3E0;
  1300. color: #FF9800;
  1301. }
  1302. }
  1303. /* 审核状态标签样式 */
  1304. &.audit-tag {
  1305. margin-left: 10rpx;
  1306. /* 待审核 - 橙色/黄色 */
  1307. &.audit-pending {
  1308. background: #FFF3E0;
  1309. color: #FF9800;
  1310. border: 1rpx solid #FFB74D;
  1311. }
  1312. /* 审核通过 - 绿色 */
  1313. &.audit-approved {
  1314. background: #E8F5E9;
  1315. color: #4CAF50;
  1316. border: 1rpx solid #81C784;
  1317. }
  1318. /* 审核未通过 - 红色 */
  1319. &.audit-rejected {
  1320. background: #FFEBEE;
  1321. color: #F44336;
  1322. border: 1rpx solid #E57373;
  1323. }
  1324. }
  1325. }
  1326. }
  1327. }
  1328. }
  1329. .resource-details {
  1330. .labels {
  1331. display: flex;
  1332. flex-wrap: wrap;
  1333. gap: 10rpx;
  1334. margin-bottom: 15rpx;
  1335. .label {
  1336. display: inline-block;
  1337. padding: 6rpx 14rpx;
  1338. background: #F3E5F5;
  1339. color: #9C27B0;
  1340. border-radius: 15rpx;
  1341. font-size: 22rpx;
  1342. font-weight: 500;
  1343. }
  1344. }
  1345. .requirement-box {
  1346. background: #F3E5F5;
  1347. border-radius: 12rpx;
  1348. padding: 16rpx 20rpx;
  1349. margin-bottom: 15rpx;
  1350. display: flex;
  1351. align-items: center;
  1352. flex-wrap: wrap;
  1353. .requirement-label {
  1354. font-size: 28rpx;
  1355. color: #7B1FA2;
  1356. font-weight: 500;
  1357. margin-right: 8rpx;
  1358. white-space: nowrap;
  1359. }
  1360. .requirement-content {
  1361. font-size: 28rpx;
  1362. color: #7B1FA2;
  1363. font-weight: 400;
  1364. }
  1365. }
  1366. .contact-box {
  1367. margin-bottom: 20rpx;
  1368. display: flex;
  1369. align-items: center;
  1370. flex-wrap: wrap;
  1371. .contact-label {
  1372. font-size: 26rpx;
  1373. color: #333;
  1374. font-weight: 500;
  1375. margin-right: 10rpx;
  1376. white-space: nowrap;
  1377. }
  1378. .contact-number {
  1379. flex: 1;
  1380. font-size: 26rpx;
  1381. color: #333;
  1382. font-weight: 400;
  1383. min-width: 0;
  1384. }
  1385. .copy-btn {
  1386. font-size: 24rpx;
  1387. color: #9C27B0;
  1388. font-weight: 500;
  1389. margin-left: 15rpx;
  1390. white-space: nowrap;
  1391. cursor: pointer;
  1392. }
  1393. }
  1394. .action-buttons {
  1395. display: flex;
  1396. justify-content: space-between;
  1397. align-items: center;
  1398. gap: 15rpx;
  1399. .delete-btn {
  1400. flex: 1;
  1401. padding: 14rpx 0;
  1402. background: #FFEBEE;
  1403. color: #E91E63;
  1404. border-radius: 25rpx;
  1405. font-size: 26rpx;
  1406. font-weight: 500;
  1407. text-align: center;
  1408. }
  1409. .match-btn {
  1410. flex: 1.5;
  1411. padding: 14rpx 0;
  1412. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1413. color: #FFFFFF;
  1414. border-radius: 25rpx;
  1415. font-size: 26rpx;
  1416. font-weight: 500;
  1417. text-align: center;
  1418. }
  1419. }
  1420. }
  1421. /* 优质资源标签 */
  1422. .quality-tag {
  1423. position: absolute;
  1424. top: 80rpx;
  1425. right: 20rpx;
  1426. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1427. color: #FFFFFF;
  1428. font-size: 22rpx;
  1429. font-weight: bold;
  1430. padding: 8rpx 16rpx;
  1431. border-radius: 20rpx;
  1432. display: flex;
  1433. align-items: center;
  1434. gap: 4rpx;
  1435. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.3);
  1436. z-index: 5;
  1437. .quality-star {
  1438. font-size: 20rpx;
  1439. }
  1440. .quality-text {
  1441. font-size: 22rpx;
  1442. }
  1443. }
  1444. }
  1445. /* 添加按钮 */
  1446. .add-button {
  1447. position: fixed;
  1448. bottom: 130rpx;
  1449. right: 40rpx;
  1450. width: 100rpx;
  1451. height: 100rpx;
  1452. border-radius: 50%;
  1453. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1454. display: flex;
  1455. align-items: center;
  1456. justify-content: center;
  1457. box-shadow: 0 4rpx 20rpx rgba(156, 39, 176, 0.4);
  1458. z-index: 999;
  1459. .add-button-icon {
  1460. font-size: 60rpx;
  1461. color: #FFFFFF;
  1462. line-height: 1;
  1463. font-weight: bold;
  1464. }
  1465. }
  1466. /* 底部导航 */
  1467. .tabbar {
  1468. position: fixed;
  1469. bottom: 0;
  1470. left: 0;
  1471. right: 0;
  1472. height: 100rpx;
  1473. background: #FFFFFF;
  1474. border-top: 1rpx solid #F0F0F0;
  1475. display: flex;
  1476. justify-content: space-around;
  1477. align-items: center;
  1478. padding-bottom: env(safe-area-inset-bottom);
  1479. .tabbar-item {
  1480. display: flex;
  1481. flex-direction: column;
  1482. align-items: center;
  1483. gap: 8rpx;
  1484. padding: 10rpx 0;
  1485. .tabbar-icon {
  1486. width: 44rpx;
  1487. height: 44rpx;
  1488. background-size: contain;
  1489. background-repeat: no-repeat;
  1490. background-position: center;
  1491. position: relative;
  1492. .badge {
  1493. position: absolute;
  1494. top: -8rpx;
  1495. right: -8rpx;
  1496. background: #FF4444;
  1497. color: #FFFFFF;
  1498. font-size: 20rpx;
  1499. font-weight: bold;
  1500. width: 32rpx;
  1501. height: 32rpx;
  1502. display: flex;
  1503. align-items: center;
  1504. justify-content: center;
  1505. border-radius: 16rpx;
  1506. }
  1507. }
  1508. .tabbar-text {
  1509. font-size: 20rpx;
  1510. color: #999;
  1511. }
  1512. &.active {
  1513. .tabbar-text {
  1514. color: #9C27B0;
  1515. font-weight: bold;
  1516. }
  1517. }
  1518. &.home .tabbar-icon {
  1519. 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>');
  1520. }
  1521. &.active.home .tabbar-icon {
  1522. 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>');
  1523. }
  1524. &.resources .tabbar-icon {
  1525. 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>');
  1526. }
  1527. &.trophy .tabbar-icon {
  1528. 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>');
  1529. }
  1530. &.active.trophy .tabbar-icon {
  1531. 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>');
  1532. }
  1533. &.message .tabbar-icon {
  1534. 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>');
  1535. }
  1536. &.active.message .tabbar-icon {
  1537. 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>');
  1538. }
  1539. &.mine .tabbar-icon {
  1540. 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>');
  1541. }
  1542. &.active.mine .tabbar-icon {
  1543. 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>');
  1544. }
  1545. }
  1546. }
  1547. </style>