my-resources.vue 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450
  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. <scroll-view scroll-y class="content">
  18. <!-- 资源部分(已注册用户) -->
  19. <view class="resource-section" v-if="registeredResources.length > 0">
  20. <view class="section-header">
  21. <text class="section-title">资源</text>
  22. <text class="section-count">({{ registeredTotal }})</text>
  23. </view>
  24. <view class="resource-list-container">
  25. <view class="resource-item" v-for="(item, index) in getRegisteredPageData" :key="item.id" v-if="item" @click="handleResourceClick(item)">
  26. <!-- 右上角选中图标 -->
  27. <view class="select-icon">✓-</view>
  28. <view class="resource-header">
  29. <image
  30. :src="item.avatar"
  31. mode="aspectFill"
  32. class="resource-avatar"
  33. @error="handleImageErrorRegistered(index)"
  34. @load="handleImageLoadRegistered(index)"
  35. :lazy-load="true"
  36. ></image>
  37. <view class="resource-basic-info">
  38. <view class="name-gender">
  39. <text class="resource-name">{{ item.name }}</text>
  40. <text class="resource-gender gender-tag">{{ item.gender }}</text>
  41. </view>
  42. <view class="status-tag-wrapper">
  43. <text class="status-tag register-tag registered">已注册</text>
  44. <text class="status-tag match-tag" :class="{ 'matched': item.isMatch === 1, 'unmatched': item.isMatch === 0 || !item.isMatch }">
  45. {{ item.isMatch === 1 ? '已匹配' : '未匹配' }}
  46. </text>
  47. </view>
  48. </view>
  49. </view>
  50. <view class="resource-details">
  51. <view class="labels">
  52. <text class="label" v-for="(label, idx) in item.labels" :key="idx">{{ label }}</text>
  53. </view>
  54. <view class="requirement-box">
  55. <text class="requirement-label">择偶要求:</text>
  56. <text class="requirement-content">{{ item.requirement }}</text>
  57. </view>
  58. <view class="contact-box">
  59. <text class="contact-label">联系方式:</text>
  60. <text class="contact-number">{{ item.contact }}</text>
  61. <text class="copy-btn" @click.stop="handleCopyContact(item.originalPhone || item.contact)">复制</text>
  62. </view>
  63. <view class="action-buttons">
  64. <view class="delete-btn" @click.stop="handleDelete(item.id)">删除</view>
  65. <view class="match-btn" @click.stop="handleMatch(item.id)">精准匹配</view>
  66. </view>
  67. </view>
  68. <!-- 优质资源标签 -->
  69. <view class="quality-tag" v-if="item.isQuality">
  70. <text class="quality-star">★</text>
  71. <text class="quality-text">优质资源</text>
  72. </view>
  73. </view>
  74. <!-- 占位元素,确保数据不足3条时也保持固定高度 -->
  75. <view class="resource-placeholder" v-for="(key, index) in getRegisteredPlaceholderKeys" :key="key"></view>
  76. </view>
  77. <!-- 资源分页控件 -->
  78. <view class="pagination" v-if="registeredTotal > registeredPageSize">
  79. <view class="pagination-btn" :class="{ disabled: registeredPageNum <= 1 }" @click="prevRegisteredPage">
  80. <text>上一页</text>
  81. </view>
  82. <text class="pagination-info">{{ registeredPageNum }} / {{ getRegisteredTotalPages }}</text>
  83. <view class="pagination-btn" :class="{ disabled: registeredPageNum >= getRegisteredTotalPages }" @click="nextRegisteredPage">
  84. <text>下一页</text>
  85. </view>
  86. </view>
  87. </view>
  88. <!-- 线索部分(未注册用户) -->
  89. <view class="resource-section" v-if="unregisteredResources.length > 0">
  90. <view class="section-header">
  91. <text class="section-title">线索</text>
  92. <text class="section-count">({{ unregisteredTotal }})</text>
  93. </view>
  94. <view class="resource-list-container">
  95. <view class="resource-item" v-for="(item, index) in getUnregisteredPageData" :key="item.id" v-if="item" @click="handleResourceClick(item)">
  96. <!-- 右上角选中图标 -->
  97. <view class="select-icon">✓-</view>
  98. <view class="resource-header">
  99. <image
  100. :src="item.avatar"
  101. mode="aspectFill"
  102. class="resource-avatar"
  103. @error="handleImageErrorUnregistered(index)"
  104. @load="handleImageLoadUnregistered(index)"
  105. :lazy-load="true"
  106. ></image>
  107. <view class="resource-basic-info">
  108. <view class="name-gender">
  109. <text class="resource-name">{{ item.name }}</text>
  110. <text class="resource-gender gender-tag">{{ item.gender }}</text>
  111. </view>
  112. <view class="status-tag-wrapper">
  113. <text class="status-tag register-tag unregistered">未注册</text>
  114. </view>
  115. </view>
  116. </view>
  117. <view class="resource-details">
  118. <view class="labels">
  119. <text class="label" v-for="(label, idx) in item.labels" :key="idx">{{ label }}</text>
  120. </view>
  121. <view class="requirement-box">
  122. <text class="requirement-label">择偶要求:</text>
  123. <text class="requirement-content">{{ item.requirement }}</text>
  124. </view>
  125. <view class="contact-box">
  126. <text class="contact-label">联系方式:</text>
  127. <text class="contact-number">{{ item.contact }}</text>
  128. <text class="copy-btn" @click.stop="handleCopyContact(item.originalPhone || item.contact)">复制</text>
  129. </view>
  130. <view class="action-buttons">
  131. <view class="delete-btn" @click.stop="handleDelete(item.id)">删除</view>
  132. <view class="match-btn" @click.stop="handleMatch(item.id)">精准匹配</view>
  133. </view>
  134. </view>
  135. <!-- 优质资源标签 -->
  136. <view class="quality-tag" v-if="item.isQuality">
  137. <text class="quality-star">★</text>
  138. <text class="quality-text">优质资源</text>
  139. </view>
  140. </view>
  141. <!-- 占位元素,确保数据不足3条时也保持固定高度 -->
  142. <view class="resource-placeholder" v-for="(key, index) in getUnregisteredPlaceholderKeys" :key="key"></view>
  143. </view>
  144. <!-- 线索分页控件 -->
  145. <view class="pagination" v-if="unregisteredTotal > unregisteredPageSize">
  146. <view class="pagination-btn" :class="{ disabled: unregisteredPageNum <= 1 }" @click="prevUnregisteredPage">
  147. <text>上一页</text>
  148. </view>
  149. <text class="pagination-info">{{ unregisteredPageNum }} / {{ getUnregisteredTotalPages }}</text>
  150. <view class="pagination-btn" :class="{ disabled: unregisteredPageNum >= getUnregisteredTotalPages }" @click="nextUnregisteredPage">
  151. <text>下一页</text>
  152. </view>
  153. </view>
  154. </view>
  155. <!-- 空状态 -->
  156. <view class="empty-state" v-if="registeredTotal === 0 && unregisteredTotal === 0">
  157. <text class="empty-text">暂无资源</text>
  158. </view>
  159. </scroll-view>
  160. <!-- 底部添加按钮 -->
  161. <view class="add-button" @click="handleAdd">
  162. <text class="add-button-icon">+</text>
  163. </view>
  164. <!-- 底部导航 -->
  165. <view class="tabbar">
  166. <view class="tabbar-item" @click="navigateToWorkbench">
  167. <view class="tabbar-icon home"></view>
  168. <text class="tabbar-text">工作台</text>
  169. </view>
  170. <view class="tabbar-item active" @click="navigateToMyResources">
  171. <view class="tabbar-icon resources"></view>
  172. <text class="tabbar-text">我的资源</text>
  173. </view>
  174. <view class="tabbar-item" @click="navigateToRanking">
  175. <view class="tabbar-icon trophy"></view>
  176. <text class="tabbar-text">排行榜</text>
  177. </view>
  178. <view class="tabbar-item" @click="navigateToMessage">
  179. <view class="tabbar-icon message">
  180. <view v-if="unreadCount > 0" class="badge">{{ unreadCount }}</view>
  181. </view>
  182. <text class="tabbar-text">消息</text>
  183. </view>
  184. <view class="tabbar-item" @click="navigateToMine">
  185. <view class="tabbar-icon mine"></view>
  186. <text class="tabbar-text">我的</text>
  187. </view>
  188. </view>
  189. </view>
  190. </template>
  191. <script>
  192. import api from '@/utils/api.js'
  193. export default {
  194. data() {
  195. return {
  196. searchKeyword: '',
  197. isFirstLoad: true, // 标记是否为首次加载
  198. resources: [], // 保留用于兼容
  199. registeredResources: [], // 已注册用户(资源部分)- 全部数据
  200. unregisteredResources: [], // 未注册用户(线索部分)- 全部数据
  201. refreshTimer: null, // 定时刷新器
  202. // 资源分页
  203. registeredPageNum: 1, // 资源当前页码
  204. registeredPageSize: 3, // 资源每页显示数量
  205. registeredTotal: 0, // 资源总数
  206. // 线索分页
  207. unregisteredPageNum: 1, // 线索当前页码
  208. unregisteredPageSize: 3, // 线索每页显示数量
  209. unregisteredTotal: 0 // 线索总数
  210. }
  211. },
  212. computed: {
  213. // 全局未读消息数
  214. unreadCount() {
  215. return this.$store.getters.getTotalUnread || 0
  216. },
  217. // 获取资源当前页数据(确保始终返回3条,不足的用null占位)
  218. getRegisteredPageData() {
  219. const start = (this.registeredPageNum - 1) * this.registeredPageSize
  220. const end = start + this.registeredPageSize
  221. const pageData = this.registeredResources.slice(start, end)
  222. // 如果数据不足3条,用null填充到3条
  223. while (pageData.length < this.registeredPageSize) {
  224. pageData.push(null)
  225. }
  226. return pageData
  227. },
  228. // 获取线索当前页数据(确保始终返回3条,不足的用null占位)
  229. getUnregisteredPageData() {
  230. const start = (this.unregisteredPageNum - 1) * this.unregisteredPageSize
  231. const end = start + this.unregisteredPageSize
  232. const pageData = this.unregisteredResources.slice(start, end)
  233. // 如果数据不足3条,用null填充到3条
  234. while (pageData.length < this.unregisteredPageSize) {
  235. pageData.push(null)
  236. }
  237. return pageData
  238. },
  239. // 获取资源占位元素的key列表
  240. getRegisteredPlaceholderKeys() {
  241. return this.getRegisteredPageData.map((item, index) => {
  242. return item ? null : 'registered-placeholder-' + index
  243. }).filter(key => key !== null)
  244. },
  245. // 获取线索占位元素的key列表
  246. getUnregisteredPlaceholderKeys() {
  247. return this.getUnregisteredPageData.map((item, index) => {
  248. return item ? null : 'unregistered-placeholder-' + index
  249. }).filter(key => key !== null)
  250. },
  251. // 资源总页数
  252. getRegisteredTotalPages() {
  253. return Math.ceil(this.registeredTotal / this.registeredPageSize)
  254. },
  255. // 线索总页数
  256. getUnregisteredTotalPages() {
  257. return Math.ceil(this.unregisteredTotal / this.unregisteredPageSize)
  258. }
  259. },
  260. onLoad() {
  261. // 加载我的资源数据
  262. this.loadMyResources()
  263. this.isFirstLoad = false
  264. // 监听刷新事件
  265. uni.$on('refreshResourceList', () => {
  266. console.log('收到刷新资源列表事件')
  267. this.loadMyResources()
  268. })
  269. // 启动定时刷新,每30秒检查一次用户注册状态变化
  270. this.startAutoRefresh()
  271. },
  272. onShow() {
  273. // 页面显示时刷新列表(从其他页面返回时,非首次加载)
  274. if (!this.isFirstLoad) {
  275. this.loadMyResources()
  276. }
  277. // 重新启动定时刷新
  278. this.startAutoRefresh()
  279. },
  280. onHide() {
  281. // 页面隐藏时停止定时刷新
  282. this.stopAutoRefresh()
  283. },
  284. onUnload() {
  285. // 页面卸载时移除事件监听和停止定时刷新
  286. uni.$off('refreshResourceList')
  287. this.stopAutoRefresh()
  288. },
  289. methods: {
  290. // 加载我的资源数据
  291. async loadMyResources() {
  292. try {
  293. // 获取当前登录用户ID
  294. const userInfo = uni.getStorageSync('userInfo') || {}
  295. const userId = uni.getStorageSync('userId')
  296. let currentUserId = userInfo.userId || userId || null
  297. // 验证并转换currentUserId为有效的整数
  298. if (currentUserId !== null && currentUserId !== undefined) {
  299. currentUserId = parseInt(currentUserId)
  300. if (isNaN(currentUserId) || currentUserId <= 0) {
  301. currentUserId = null
  302. }
  303. }
  304. if (!currentUserId) {
  305. console.error('无法获取当前登录用户ID')
  306. uni.showToast({
  307. title: '请先登录',
  308. icon: 'none'
  309. })
  310. return
  311. }
  312. // 调用后端接口,传递当前用户ID作为matchmakerId
  313. const baseUrl = process.env.NODE_ENV === 'development'
  314. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  315. : 'https://your-domain.com/api' // 生产环境
  316. // 构建查询参数,确保currentUserId是有效的整数
  317. let url = `${baseUrl}/my-resource/list?currentUserId=${currentUserId}&pageNum=1&pageSize=100`
  318. if (this.searchKeyword && this.searchKeyword.trim()) {
  319. url += `&keyword=${encodeURIComponent(this.searchKeyword.trim())}`
  320. }
  321. const [error, res] = await uni.request({
  322. url: url,
  323. method: 'GET'
  324. })
  325. if (error) {
  326. console.error('加载资源数据失败:', error)
  327. return
  328. }
  329. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  330. // 处理返回的数据
  331. const pageData = res.data.data
  332. console.log('=== 后端返回的完整数据 ===')
  333. console.log('pageData:', JSON.stringify(pageData, null, 2))
  334. if (pageData && pageData.records) {
  335. console.log('records数量:', pageData.records.length)
  336. if (pageData.records.length > 0) {
  337. console.log('第一条记录的完整数据:', JSON.stringify(pageData.records[0], null, 2))
  338. }
  339. // 将后端数据转换为前端需要的格式
  340. const allResources = pageData.records.map(item => {
  341. // 直接使用mate_selection_criteria字段作为择偶要求
  342. // 支持多种可能的字段名(mateSelectionCriteria或mate_selection_criteria)
  343. let requirement = item.mateSelectionCriteria || item.mate_selection_criteria || ''
  344. // 去除首尾空格,如果为空则显示"暂无要求"
  345. if (requirement) {
  346. requirement = requirement.trim()
  347. }
  348. // 处理标签(可以根据实际需求扩展)
  349. const labels = []
  350. if (item.constellation) {
  351. labels.push(item.constellation)
  352. }
  353. if (item.occupation) {
  354. labels.push(item.occupation)
  355. }
  356. // 处理头像URL:确保使用完整的URL
  357. // 尝试多种可能的字段名
  358. let avatarUrl = item.avatarUrl || item.avatar_url || item.avatar || ''
  359. console.log('=== 头像URL处理 ===')
  360. console.log('资源ID:', item.resourceId, '姓名:', item.name)
  361. console.log('原始数据字段:', {
  362. avatarUrl: item.avatarUrl,
  363. avatar_url: item.avatar_url,
  364. avatar: item.avatar,
  365. 'item完整对象': item
  366. })
  367. console.log('提取的avatarUrl值:', avatarUrl, '类型:', typeof avatarUrl, '是否为空:', !avatarUrl)
  368. // 如果头像URL为空或null,设置为空字符串(让CSS背景图显示)
  369. if (!avatarUrl || avatarUrl.trim() === '' || avatarUrl === 'null' || avatarUrl === null || avatarUrl === undefined) {
  370. avatarUrl = '' // 设置为空,让CSS默认背景显示
  371. console.log('⚠️ 头像URL为空,设置为空字符串,将显示CSS默认背景')
  372. } else {
  373. // 确保URL是完整的(如果已经是完整URL则直接使用)
  374. avatarUrl = avatarUrl.trim()
  375. // 如果URL不是以http开头,可能需要拼接基础URL(根据实际情况调整)
  376. if (!avatarUrl.startsWith('http://') && !avatarUrl.startsWith('https://')) {
  377. console.warn('⚠️ 头像URL不是完整URL,可能需要拼接:', avatarUrl)
  378. // 如果是相对路径,可以尝试拼接MinIO基础URL
  379. // avatarUrl = 'http://115.190.125.125:9000/' + avatarUrl
  380. }
  381. // 确保MinIO URL可以正常访问(可能需要处理跨域)
  382. // 如果MinIO配置了公共访问,直接使用URL即可
  383. console.log('✅ 使用用户头像URL:', avatarUrl)
  384. // 验证URL格式
  385. if (avatarUrl.includes('115.190.125.125:9000')) {
  386. console.log('✅ 检测到MinIO URL,URL格式:', avatarUrl)
  387. }
  388. }
  389. console.log('最终设置的avatarUrl:', avatarUrl)
  390. console.log('=== 头像URL处理结束 ===')
  391. // 确保resourceId是有效的整数
  392. let resourceId = item.resourceId || item.resource_id
  393. // 调试日志:检查resourceId的来源
  394. console.log('=== resourceId处理 ===')
  395. console.log('原始item.resourceId:', item.resourceId, '类型:', typeof item.resourceId)
  396. console.log('原始item.resource_id:', item.resource_id, '类型:', typeof item.resource_id)
  397. console.log('提取的resourceId:', resourceId, '类型:', typeof resourceId)
  398. if (resourceId === null || resourceId === undefined || resourceId === 'undefined' || resourceId === 'null') {
  399. console.warn('资源ID无效,跳过该资源:', item)
  400. return null // 返回null,后续会被filter过滤掉
  401. }
  402. resourceId = parseInt(resourceId)
  403. if (isNaN(resourceId) || resourceId <= 0) {
  404. console.warn('资源ID格式错误,跳过该资源:', item)
  405. return null // 返回null,后续会被filter过滤掉
  406. }
  407. console.log('处理后的resourceId:', resourceId, '类型:', typeof resourceId)
  408. console.log('=== resourceId处理结束 ===')
  409. // 处理isUser字段,支持多种可能的字段名(isUser或is_user)
  410. let isUser = item.isUser !== null && item.isUser !== undefined ? item.isUser :
  411. (item.is_user !== null && item.is_user !== undefined ? item.is_user : 0)
  412. // 确保isUser是数字类型
  413. isUser = parseInt(isUser) || 0
  414. // 处理isMatch字段
  415. let isMatch = item.isMatch !== null && item.isMatch !== undefined ? item.isMatch :
  416. (item.is_match !== null && item.is_match !== undefined ? item.is_match : 0)
  417. isMatch = parseInt(isMatch) || 0
  418. // 调试日志:检查isUser字段
  419. console.log('=== isUser字段处理 ===')
  420. console.log('资源ID:', resourceId, '姓名:', item.name)
  421. console.log('原始数据:', {
  422. isUser: item.isUser,
  423. is_user: item.is_user,
  424. 'item完整对象': item
  425. })
  426. console.log('处理后的isUser值:', isUser, '类型:', typeof isUser)
  427. console.log('=== isUser字段处理结束 ===')
  428. // 处理标签(从后端返回的tags字段,如果没有则使用原有的labels)
  429. let resourceTags = []
  430. if (item.tags && Array.isArray(item.tags) && item.tags.length > 0) {
  431. // 使用后端返回的标签
  432. resourceTags = item.tags
  433. } else if (labels && labels.length > 0) {
  434. // 如果没有tags,使用原有的labels(星座、职业等)
  435. resourceTags = labels
  436. }
  437. return {
  438. id: resourceId,
  439. avatar: avatarUrl, // 使用处理后的头像URL
  440. name: item.name || '',
  441. gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
  442. status: '已审核', // 可以根据实际字段判断
  443. isUser: isUser, // 添加isUser字段,默认为0
  444. isMatch: isMatch, // 添加isMatch字段,默认为0
  445. isPlus: false,
  446. labels: resourceTags, // 使用处理后的标签列表
  447. requirement: requirement || '暂无要求',
  448. contact: item.phone ? item.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  449. originalPhone: item.phone || '', // 保存原始完整手机号用于复制
  450. isQuality: false
  451. }
  452. }).filter(item => item !== null) // 过滤掉无效的资源(resourceId无效的)
  453. // 将资源分为两部分:已注册和未注册
  454. this.registeredResources = allResources.filter(item => item.isUser === 1)
  455. this.unregisteredResources = allResources.filter(item => item.isUser === 0)
  456. // 设置总数
  457. this.registeredTotal = this.registeredResources.length
  458. this.unregisteredTotal = this.unregisteredResources.length
  459. // 如果当前页超出范围,重置到第一页
  460. if (this.registeredPageNum > Math.ceil(this.registeredTotal / this.registeredPageSize) && this.registeredTotal > 0) {
  461. this.registeredPageNum = 1
  462. }
  463. if (this.unregisteredPageNum > Math.ceil(this.unregisteredTotal / this.unregisteredPageSize) && this.unregisteredTotal > 0) {
  464. this.unregisteredPageNum = 1
  465. }
  466. // 保留resources用于兼容(包含所有资源)
  467. this.resources = allResources
  468. console.log('处理后的资源列表 - 已注册:', this.registeredResources.length, '未注册:', this.unregisteredResources.length)
  469. }
  470. }
  471. } catch (e) {
  472. console.error('加载资源数据失败:', e)
  473. }
  474. },
  475. // 搜索
  476. async handleSearch() {
  477. // 重置分页到第一页
  478. this.registeredPageNum = 1
  479. this.unregisteredPageNum = 1
  480. // 重新加载资源数据,包含搜索关键词
  481. await this.loadMyResources()
  482. },
  483. // 资源上一页
  484. prevRegisteredPage() {
  485. if (this.registeredPageNum > 1) {
  486. this.registeredPageNum--
  487. }
  488. },
  489. // 资源下一页
  490. nextRegisteredPage() {
  491. const totalPages = Math.ceil(this.registeredTotal / this.registeredPageSize)
  492. if (this.registeredPageNum < totalPages) {
  493. this.registeredPageNum++
  494. }
  495. },
  496. // 线索上一页
  497. prevUnregisteredPage() {
  498. if (this.unregisteredPageNum > 1) {
  499. this.unregisteredPageNum--
  500. }
  501. },
  502. // 线索下一页
  503. nextUnregisteredPage() {
  504. const totalPages = Math.ceil(this.unregisteredTotal / this.unregisteredPageSize)
  505. if (this.unregisteredPageNum < totalPages) {
  506. this.unregisteredPageNum++
  507. }
  508. },
  509. // 删除资源
  510. async handleDelete(id) {
  511. // 验证id是否有效
  512. if (id === null || id === undefined || id === 'undefined' || id === 'null' || id === '') {
  513. console.error('删除资源失败: 资源ID无效', id)
  514. uni.showToast({
  515. title: '资源ID无效,无法删除',
  516. icon: 'none'
  517. })
  518. return
  519. }
  520. // 确保id是有效的整数
  521. const resourceId = parseInt(id)
  522. if (isNaN(resourceId) || resourceId <= 0) {
  523. console.error('删除资源失败: 资源ID格式错误', id)
  524. uni.showToast({
  525. title: '资源ID格式错误,无法删除',
  526. icon: 'none'
  527. })
  528. return
  529. }
  530. uni.showModal({
  531. title: '删除确认',
  532. content: '确定要删除该资源吗?',
  533. success: async (res) => {
  534. if (res.confirm) {
  535. try {
  536. uni.showLoading({
  537. title: '删除中...'
  538. })
  539. const baseUrl = process.env.NODE_ENV === 'development'
  540. ? 'http://localhost:8083/api' // 开发环境 - 通过网关
  541. : 'https://your-domain.com/api' // 生产环境
  542. const [error, deleteRes] = await uni.request({
  543. url: `${baseUrl}/my-resource/delete/${resourceId}`,
  544. method: 'DELETE'
  545. })
  546. uni.hideLoading()
  547. if (error) {
  548. console.error('删除资源失败:', error)
  549. uni.showToast({
  550. title: '删除失败',
  551. icon: 'none'
  552. })
  553. return
  554. }
  555. if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.code === 200) {
  556. uni.showToast({
  557. title: '删除成功',
  558. icon: 'success'
  559. })
  560. // 删除成功后刷新资源列表,并重置分页
  561. this.registeredPageNum = 1
  562. this.unregisteredPageNum = 1
  563. await this.loadMyResources()
  564. } else {
  565. uni.showToast({
  566. title: deleteRes.data.message || '删除失败',
  567. icon: 'none'
  568. })
  569. }
  570. } catch (e) {
  571. uni.hideLoading()
  572. console.error('删除资源异常:', e)
  573. uni.showToast({
  574. title: '删除失败,请稍后重试',
  575. icon: 'none'
  576. })
  577. }
  578. }
  579. }
  580. })
  581. },
  582. // 精准匹配
  583. handleMatch(id) {
  584. // 查找资源项
  585. const allResources = [...this.registeredResources, ...this.unregisteredResources]
  586. const resource = allResources.find(item => item.id === id)
  587. if (!resource) {
  588. uni.showToast({
  589. title: '资源不存在',
  590. icon: 'none'
  591. })
  592. return
  593. }
  594. // 检查用户是否已注册
  595. if (resource.isUser !== 1) {
  596. uni.showToast({
  597. title: '该用户还未注册用户端,注册之后才能进行匹配',
  598. icon: 'none',
  599. duration: 3000
  600. })
  601. return
  602. }
  603. // 已注册用户,跳转到精准匹配页面
  604. console.log('精准匹配:', id)
  605. uni.navigateTo({
  606. url: `/pages/matchmaker-workbench/precise-match?resourceId=${id}`
  607. })
  608. },
  609. // 复制联系方式
  610. handleCopyContact(contact) {
  611. if (!contact) {
  612. uni.showToast({
  613. title: '联系方式为空',
  614. icon: 'none'
  615. })
  616. return
  617. }
  618. // 如果是脱敏的手机号,需要从原始数据中获取完整号码
  619. // 这里需要根据实际情况调整,可能需要从item中获取原始phone
  620. // 暂时先复制显示的文本
  621. uni.setClipboardData({
  622. data: contact,
  623. success: () => {
  624. uni.showToast({
  625. title: '已复制到剪贴板',
  626. icon: 'success'
  627. })
  628. },
  629. fail: () => {
  630. uni.showToast({
  631. title: '复制失败',
  632. icon: 'none'
  633. })
  634. }
  635. })
  636. },
  637. // 客户点击事件,跳转到客户详情页面
  638. handleClientClick(id) {
  639. uni.navigateTo({
  640. url: `/pages/matchmaker-workbench/client-detail?resourceId=${id}`
  641. })
  642. },
  643. // 资源项点击事件
  644. handleResourceClick(item) {
  645. console.log('=== 点击资源项 ===')
  646. console.log('item对象:', JSON.stringify(item, null, 2))
  647. console.log('item.id:', item.id)
  648. console.log('item.isUser:', item.isUser)
  649. // 判断是否为已注册用户
  650. if (item.isUser === 1) {
  651. // 已注册,跳转到客户详情页面
  652. const resourceId = item.id
  653. console.log('准备跳转,resourceId:', resourceId)
  654. uni.navigateTo({
  655. url: `/pages/matchmaker-workbench/client-detail?resourceId=${resourceId}`,
  656. success: () => {
  657. console.log('跳转成功,resourceId:', resourceId)
  658. },
  659. fail: (err) => {
  660. console.error('跳转失败:', err)
  661. }
  662. })
  663. } else {
  664. // 未注册,提示用户
  665. uni.showToast({
  666. title: '该用户还未注册用户端',
  667. icon: 'none',
  668. duration: 2000
  669. })
  670. }
  671. },
  672. // 添加资源
  673. handleAdd() {
  674. // 跳转到信息录入页面
  675. uni.navigateTo({
  676. url: '/pages/matchmaker-workbench/resource-input',
  677. success: () => {
  678. console.log('跳转到信息录入页面成功')
  679. },
  680. fail: (err) => {
  681. console.error('跳转失败:', err)
  682. uni.showToast({
  683. title: '页面跳转失败',
  684. icon: 'none'
  685. })
  686. }
  687. })
  688. },
  689. // 导航到工作台
  690. navigateToWorkbench() {
  691. uni.navigateTo({
  692. url: '/pages/matchmaker-workbench/index'
  693. })
  694. },
  695. // 导航到我的资源
  696. navigateToMyResources() {
  697. // 已在我的资源页面,无需跳转
  698. },
  699. // 导航到排行榜
  700. navigateToRanking() {
  701. uni.navigateTo({
  702. url: '/pages/matchmaker-workbench/ranking'
  703. })
  704. },
  705. // 导航到消息
  706. navigateToMessage() {
  707. uni.navigateTo({
  708. url: '/pages/matchmaker-workbench/message'
  709. })
  710. },
  711. // 导航到我的
  712. navigateToMine() {
  713. uni.navigateTo({
  714. url: '/pages/matchmaker-workbench/mine'
  715. })
  716. },
  717. // 启动自动刷新
  718. startAutoRefresh() {
  719. // 清除之前的定时器
  720. this.stopAutoRefresh()
  721. // 每30秒刷新一次,检查用户注册状态变化
  722. this.refreshTimer = setInterval(() => {
  723. console.log('定时刷新:检查用户注册状态变化')
  724. this.loadMyResources()
  725. }, 30000) // 30秒
  726. },
  727. // 停止自动刷新
  728. stopAutoRefresh() {
  729. if (this.refreshTimer) {
  730. clearInterval(this.refreshTimer)
  731. this.refreshTimer = null
  732. }
  733. },
  734. // 已注册资源图片加载错误处理
  735. handleImageErrorRegistered(index) {
  736. this.handleImageError(index, 'registered')
  737. },
  738. // 未注册资源图片加载错误处理
  739. handleImageErrorUnregistered(index) {
  740. this.handleImageError(index, 'unregistered')
  741. },
  742. // 已注册资源图片加载成功处理
  743. handleImageLoadRegistered(index) {
  744. this.handleImageLoad(index, 'registered')
  745. },
  746. // 未注册资源图片加载成功处理
  747. handleImageLoadUnregistered(index) {
  748. this.handleImageLoad(index, 'unregistered')
  749. },
  750. // 图片加载错误处理(内部方法)
  751. handleImageError(index, type) {
  752. try {
  753. // 获取当前页的数据
  754. const pageData = type === 'registered' ? this.getRegisteredPageData : this.getUnregisteredPageData
  755. const resource = pageData && pageData[index]
  756. if (!resource) {
  757. console.warn('图片加载失败:资源不存在,index:', index, 'type:', type)
  758. return
  759. }
  760. const originalUrl = resource.avatar
  761. const resourceId = resource.id
  762. console.error('❌ 图片加载失败:', {
  763. index: index,
  764. type: type,
  765. resourceName: resource.name,
  766. resourceId: resourceId,
  767. originalAvatarUrl: originalUrl,
  768. 'URL类型': typeof originalUrl,
  769. 'URL长度': originalUrl ? originalUrl.length : 0,
  770. '是否包含placeholder': originalUrl && originalUrl.includes('placeholder'),
  771. '完整resource对象': resource
  772. })
  773. // 如果图片加载失败,且不是已经设置的默认占位图,则设置为空(显示CSS背景)
  774. // 避免重复设置导致循环
  775. if (originalUrl &&
  776. originalUrl.trim() !== '' &&
  777. !originalUrl.includes('placeholder') &&
  778. !originalUrl.includes('default') &&
  779. !originalUrl.includes('via.placeholder')) {
  780. // 根据resourceId在全部数据中找到对应的资源并更新
  781. console.log('图片加载失败,将URL设置为空,显示CSS默认背景')
  782. if (type === 'registered') {
  783. const fullList = this.registeredResources
  784. const foundIndex = fullList.findIndex(item => item.id === resourceId)
  785. if (foundIndex !== -1) {
  786. this.$set(this.registeredResources[foundIndex], 'avatar', '')
  787. }
  788. } else {
  789. const fullList = this.unregisteredResources
  790. const foundIndex = fullList.findIndex(item => item.id === resourceId)
  791. if (foundIndex !== -1) {
  792. this.$set(this.unregisteredResources[foundIndex], 'avatar', '')
  793. }
  794. }
  795. } else {
  796. console.log('图片加载失败,但URL已经是占位图或空,无需处理')
  797. console.log('当前URL:', originalUrl)
  798. }
  799. } catch (e) {
  800. console.error('处理图片错误时发生异常:', e)
  801. }
  802. },
  803. // 图片加载成功处理(内部方法)
  804. handleImageLoad(index, type) {
  805. try {
  806. // 获取当前页的数据
  807. const pageData = type === 'registered' ? this.getRegisteredPageData : this.getUnregisteredPageData
  808. if (pageData && pageData[index]) {
  809. console.log('图片加载成功:', {
  810. index: index,
  811. type: type,
  812. resource: pageData[index]?.name,
  813. avatarUrl: pageData[index]?.avatar
  814. })
  815. }
  816. } catch (e) {
  817. console.error('处理图片加载成功时发生异常:', e)
  818. }
  819. }
  820. }
  821. }
  822. </script>
  823. <style lang="scss" scoped>
  824. .my-resources {
  825. min-height: 100vh;
  826. background: #F5F5F5;
  827. display: flex;
  828. flex-direction: column;
  829. }
  830. /* 顶部导航栏 */
  831. .header {
  832. display: flex;
  833. align-items: center;
  834. justify-content: space-between;
  835. padding: 20rpx 30rpx;
  836. padding-top: calc(20rpx + env(safe-area-inset-top));
  837. background: #FFFFFF;
  838. border-bottom: 1rpx solid #F0F0F0;
  839. .header-title {
  840. font-size: 36rpx;
  841. font-weight: bold;
  842. color: #9C27B0;
  843. }
  844. .header-right {
  845. .dropdown-arrow {
  846. font-size: 24rpx;
  847. color: #9C27B0;
  848. font-weight: normal;
  849. }
  850. }
  851. }
  852. /* 搜索栏 */
  853. .search-bar {
  854. padding: 20rpx;
  855. background: #F5F5F5;
  856. .search-input-wrapper {
  857. display: flex;
  858. align-items: center;
  859. background: #FFFFFF;
  860. border-radius: 30rpx;
  861. padding: 12rpx 20rpx;
  862. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
  863. .search-icon-small {
  864. width: 32rpx;
  865. height: 32rpx;
  866. 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>');
  867. background-size: contain;
  868. background-repeat: no-repeat;
  869. background-position: center;
  870. margin-right: 15rpx;
  871. }
  872. .search-input {
  873. flex: 1;
  874. font-size: 28rpx;
  875. color: #333;
  876. border: none;
  877. outline: none;
  878. background: transparent;
  879. &::placeholder {
  880. color: #999;
  881. }
  882. }
  883. }
  884. }
  885. .content {
  886. flex: 1;
  887. padding: 0 20rpx 140rpx;
  888. }
  889. /* 资源分组 */
  890. .resource-section {
  891. margin-bottom: 30rpx;
  892. min-height: calc(50vh - 100rpx); /* 确保至少占半屏高度 */
  893. display: flex;
  894. flex-direction: column;
  895. .section-header {
  896. display: flex;
  897. align-items: center;
  898. padding: 20rpx 0 15rpx;
  899. margin-bottom: 10rpx;
  900. flex-shrink: 0;
  901. .section-title {
  902. font-size: 32rpx;
  903. font-weight: bold;
  904. color: #9C27B0;
  905. margin-right: 10rpx;
  906. }
  907. .section-count {
  908. font-size: 26rpx;
  909. color: #999;
  910. font-weight: normal;
  911. }
  912. }
  913. /* 资源列表容器,确保有固定高度 */
  914. .resource-list-container {
  915. flex: 1;
  916. display: flex;
  917. flex-direction: column;
  918. min-height: calc(50vh - 200rpx);
  919. }
  920. }
  921. /* 分页控件 */
  922. .pagination {
  923. display: flex;
  924. align-items: center;
  925. justify-content: center;
  926. padding: 30rpx 0;
  927. gap: 30rpx;
  928. margin-top: 20rpx;
  929. .pagination-btn {
  930. padding: 12rpx 30rpx;
  931. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  932. color: #FFFFFF;
  933. border-radius: 25rpx;
  934. font-size: 26rpx;
  935. font-weight: 500;
  936. text-align: center;
  937. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.3);
  938. transition: all 0.3s;
  939. &:active {
  940. transform: scale(0.95);
  941. box-shadow: 0 1rpx 4rpx rgba(156, 39, 176, 0.4);
  942. }
  943. &.disabled {
  944. background: #E0E0E0;
  945. color: #999;
  946. box-shadow: none;
  947. opacity: 0.6;
  948. &:active {
  949. transform: none;
  950. }
  951. }
  952. }
  953. .pagination-info {
  954. font-size: 26rpx;
  955. color: #666;
  956. font-weight: 500;
  957. }
  958. }
  959. /* 占位元素,用于保持固定高度 */
  960. .resource-placeholder {
  961. /* 使用与resource-item相同的高度和margin来占位 */
  962. /* resource-item: padding 25rpx * 2 + margin-bottom 20rpx + 内容高度约250rpx = 约320rpx */
  963. height: 320rpx;
  964. margin-bottom: 20rpx;
  965. /* 不显示内容,但保持空间 */
  966. opacity: 0;
  967. pointer-events: none;
  968. flex-shrink: 0;
  969. }
  970. /* 空状态 */
  971. .empty-state {
  972. display: flex;
  973. justify-content: center;
  974. align-items: center;
  975. padding: 100rpx 0;
  976. .empty-text {
  977. font-size: 28rpx;
  978. color: #999;
  979. }
  980. }
  981. /* 资源列表 */
  982. .resource-item {
  983. background: #FFFFFF;
  984. border-radius: 20rpx;
  985. margin-bottom: 20rpx;
  986. padding: 25rpx;
  987. position: relative;
  988. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  989. /* 右上角选中图标 */
  990. .select-icon {
  991. position: absolute;
  992. top: 20rpx;
  993. right: 20rpx;
  994. width: 40rpx;
  995. height: 40rpx;
  996. background: #FF4444;
  997. color: #FFFFFF;
  998. border-radius: 50%;
  999. display: flex;
  1000. align-items: center;
  1001. justify-content: center;
  1002. font-size: 24rpx;
  1003. font-weight: bold;
  1004. z-index: 10;
  1005. }
  1006. .resource-header {
  1007. display: flex;
  1008. align-items: flex-start;
  1009. margin-bottom: 20rpx;
  1010. padding-right: 50rpx;
  1011. .resource-avatar {
  1012. width: 100rpx;
  1013. height: 100rpx;
  1014. border-radius: 8rpx;
  1015. margin-right: 20rpx;
  1016. flex-shrink: 0;
  1017. background-color: #F5F5F5;
  1018. 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>');
  1019. background-size: 60% 60%;
  1020. background-position: center;
  1021. background-repeat: no-repeat;
  1022. }
  1023. .resource-basic-info {
  1024. flex: 1;
  1025. min-width: 0;
  1026. .name-gender {
  1027. display: flex;
  1028. align-items: center;
  1029. gap: 10rpx;
  1030. margin-bottom: 12rpx;
  1031. .resource-name {
  1032. font-size: 30rpx;
  1033. font-weight: bold;
  1034. color: #333;
  1035. }
  1036. .resource-gender {
  1037. font-size: 26rpx;
  1038. color: #666;
  1039. &.gender-tag {
  1040. display: inline-block;
  1041. padding: 4rpx 12rpx;
  1042. background: #F3E5F5;
  1043. color: #9C27B0;
  1044. border-radius: 12rpx;
  1045. font-size: 22rpx;
  1046. font-weight: 500;
  1047. }
  1048. }
  1049. }
  1050. .status-tag-wrapper {
  1051. display: flex;
  1052. align-items: center;
  1053. .status-tag {
  1054. display: inline-block;
  1055. padding: 4rpx 12rpx;
  1056. border-radius: 12rpx;
  1057. font-size: 22rpx;
  1058. font-weight: 500;
  1059. margin-right: 8rpx;
  1060. &.approved {
  1061. background: #E3F2FD;
  1062. color: #2196F3;
  1063. }
  1064. &.pending {
  1065. background: #FFF3E0;
  1066. color: #FF9800;
  1067. }
  1068. &.register-tag {
  1069. &.registered {
  1070. background: #E8F5E9;
  1071. color: #4CAF50;
  1072. }
  1073. &.unregistered {
  1074. background: #FCE4EC;
  1075. color: #E91E63;
  1076. }
  1077. }
  1078. &.match-tag {
  1079. margin-left: 10rpx;
  1080. &.matched {
  1081. background: #E3F2FD;
  1082. color: #2196F3;
  1083. }
  1084. &.unmatched {
  1085. background: #FFF3E0;
  1086. color: #FF9800;
  1087. }
  1088. }
  1089. }
  1090. }
  1091. }
  1092. }
  1093. .resource-details {
  1094. .labels {
  1095. display: flex;
  1096. flex-wrap: wrap;
  1097. gap: 10rpx;
  1098. margin-bottom: 15rpx;
  1099. .label {
  1100. display: inline-block;
  1101. padding: 6rpx 14rpx;
  1102. background: #F3E5F5;
  1103. color: #9C27B0;
  1104. border-radius: 15rpx;
  1105. font-size: 22rpx;
  1106. font-weight: 500;
  1107. }
  1108. }
  1109. .requirement-box {
  1110. background: #F3E5F5;
  1111. border-radius: 12rpx;
  1112. padding: 16rpx 20rpx;
  1113. margin-bottom: 15rpx;
  1114. display: flex;
  1115. align-items: center;
  1116. flex-wrap: wrap;
  1117. .requirement-label {
  1118. font-size: 28rpx;
  1119. color: #7B1FA2;
  1120. font-weight: 500;
  1121. margin-right: 8rpx;
  1122. white-space: nowrap;
  1123. }
  1124. .requirement-content {
  1125. font-size: 28rpx;
  1126. color: #7B1FA2;
  1127. font-weight: 400;
  1128. }
  1129. }
  1130. .contact-box {
  1131. margin-bottom: 20rpx;
  1132. display: flex;
  1133. align-items: center;
  1134. flex-wrap: wrap;
  1135. .contact-label {
  1136. font-size: 26rpx;
  1137. color: #333;
  1138. font-weight: 500;
  1139. margin-right: 10rpx;
  1140. white-space: nowrap;
  1141. }
  1142. .contact-number {
  1143. flex: 1;
  1144. font-size: 26rpx;
  1145. color: #333;
  1146. font-weight: 400;
  1147. min-width: 0;
  1148. }
  1149. .copy-btn {
  1150. font-size: 24rpx;
  1151. color: #9C27B0;
  1152. font-weight: 500;
  1153. margin-left: 15rpx;
  1154. white-space: nowrap;
  1155. cursor: pointer;
  1156. }
  1157. }
  1158. .action-buttons {
  1159. display: flex;
  1160. justify-content: space-between;
  1161. align-items: center;
  1162. gap: 15rpx;
  1163. .delete-btn {
  1164. flex: 1;
  1165. padding: 14rpx 0;
  1166. background: #FFEBEE;
  1167. color: #E91E63;
  1168. border-radius: 25rpx;
  1169. font-size: 26rpx;
  1170. font-weight: 500;
  1171. text-align: center;
  1172. }
  1173. .match-btn {
  1174. flex: 1.5;
  1175. padding: 14rpx 0;
  1176. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1177. color: #FFFFFF;
  1178. border-radius: 25rpx;
  1179. font-size: 26rpx;
  1180. font-weight: 500;
  1181. text-align: center;
  1182. }
  1183. }
  1184. }
  1185. /* 优质资源标签 */
  1186. .quality-tag {
  1187. position: absolute;
  1188. top: 80rpx;
  1189. right: 20rpx;
  1190. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1191. color: #FFFFFF;
  1192. font-size: 22rpx;
  1193. font-weight: bold;
  1194. padding: 8rpx 16rpx;
  1195. border-radius: 20rpx;
  1196. display: flex;
  1197. align-items: center;
  1198. gap: 4rpx;
  1199. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.3);
  1200. z-index: 5;
  1201. .quality-star {
  1202. font-size: 20rpx;
  1203. }
  1204. .quality-text {
  1205. font-size: 22rpx;
  1206. }
  1207. }
  1208. }
  1209. /* 添加按钮 */
  1210. .add-button {
  1211. position: fixed;
  1212. bottom: 130rpx;
  1213. right: 40rpx;
  1214. width: 100rpx;
  1215. height: 100rpx;
  1216. border-radius: 50%;
  1217. background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
  1218. display: flex;
  1219. align-items: center;
  1220. justify-content: center;
  1221. box-shadow: 0 4rpx 20rpx rgba(156, 39, 176, 0.4);
  1222. z-index: 999;
  1223. .add-button-icon {
  1224. font-size: 60rpx;
  1225. color: #FFFFFF;
  1226. line-height: 1;
  1227. font-weight: bold;
  1228. }
  1229. }
  1230. /* 底部导航 */
  1231. .tabbar {
  1232. position: fixed;
  1233. bottom: 0;
  1234. left: 0;
  1235. right: 0;
  1236. height: 100rpx;
  1237. background: #FFFFFF;
  1238. border-top: 1rpx solid #F0F0F0;
  1239. display: flex;
  1240. justify-content: space-around;
  1241. align-items: center;
  1242. padding-bottom: env(safe-area-inset-bottom);
  1243. .tabbar-item {
  1244. display: flex;
  1245. flex-direction: column;
  1246. align-items: center;
  1247. gap: 8rpx;
  1248. padding: 10rpx 0;
  1249. .tabbar-icon {
  1250. width: 44rpx;
  1251. height: 44rpx;
  1252. background-size: contain;
  1253. background-repeat: no-repeat;
  1254. background-position: center;
  1255. position: relative;
  1256. .badge {
  1257. position: absolute;
  1258. top: -8rpx;
  1259. right: -8rpx;
  1260. background: #FF4444;
  1261. color: #FFFFFF;
  1262. font-size: 20rpx;
  1263. font-weight: bold;
  1264. width: 32rpx;
  1265. height: 32rpx;
  1266. display: flex;
  1267. align-items: center;
  1268. justify-content: center;
  1269. border-radius: 16rpx;
  1270. }
  1271. }
  1272. .tabbar-text {
  1273. font-size: 20rpx;
  1274. color: #999;
  1275. }
  1276. &.active {
  1277. .tabbar-text {
  1278. color: #9C27B0;
  1279. font-weight: bold;
  1280. }
  1281. }
  1282. &.home .tabbar-icon {
  1283. 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>');
  1284. }
  1285. &.active.home .tabbar-icon {
  1286. 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>');
  1287. }
  1288. &.resources .tabbar-icon {
  1289. 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>');
  1290. }
  1291. &.trophy .tabbar-icon {
  1292. 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>');
  1293. }
  1294. &.active.trophy .tabbar-icon {
  1295. 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>');
  1296. }
  1297. &.message .tabbar-icon {
  1298. 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>');
  1299. }
  1300. &.active.message .tabbar-icon {
  1301. 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>');
  1302. }
  1303. &.mine .tabbar-icon {
  1304. 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>');
  1305. }
  1306. &.active.mine .tabbar-icon {
  1307. 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>');
  1308. }
  1309. }
  1310. }
  1311. </style>