client-detail.vue 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593
  1. <template>
  2. <view class="client-detail" :class="{ 'dark-mode': isDarkTheme }">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <view class="back-icon" @click="handleBack"></view>
  6. <text class="header-title">客户信息</text>
  7. <view class="header-right"></view>
  8. </view>
  9. <!-- 客户基本信息 -->
  10. <view class="client-basic-info">
  11. <image :src="clientInfo.avatar" class="client-avatar" mode="aspectFill"></image>
  12. <view class="basic-info">
  13. <view class="client-name-wrapper">
  14. <text class="client-name">{{ clientInfo.name }}</text>
  15. <text class="gender-tag">{{ clientInfo.gender }}</text>
  16. </view>
  17. <view class="status-tag-wrapper">
  18. <text class="status-tag register-tag registered">已注册</text>
  19. <text class="status-tag match-tag" :class="{ 'matched': followForm.matchStatus === 1, 'unmatched': followForm.matchStatus === 0 || followForm.matchStatus === null }">
  20. {{ followForm.matchStatus === 1 ? '已匹配' : '未匹配' }}
  21. </text>
  22. </view>
  23. <view class="client-tags">
  24. <text class="tag" v-for="(tag, index) in clientInfo.tags" :key="index">{{ tag }}</text>
  25. </view>
  26. </view>
  27. </view>
  28. <!-- 择偶要求 -->
  29. <view class="requirement-section">
  30. <view class="section-title">
  31. <text class="title-icon">💑</text>
  32. <text class="title-text">择偶要求</text>
  33. </view>
  34. <text class="requirement-content">{{ clientInfo.requirement }}</text>
  35. </view>
  36. <!-- 联系方式 -->
  37. <view class="contact-section">
  38. <view class="contact-item">
  39. <text class="contact-label">联系方式</text>
  40. <view class="contact-value">
  41. <text class="contact-number">{{ clientInfo.contact }}</text>
  42. <text class="copy-btn" @click="handleCopy(clientInfo.contact)">复制</text>
  43. </view>
  44. </view>
  45. <view class="contact-item" v-if="clientInfo.backupContact">
  46. <text class="contact-label">备用联系方式</text>
  47. <view class="contact-value">
  48. <text class="contact-number">{{ clientInfo.backupContact }}</text>
  49. <text class="copy-btn" @click="handleCopy(clientInfo.backupContact)">复制</text>
  50. </view>
  51. </view>
  52. </view>
  53. <!-- 跟进统计 -->
  54. <view class="stats-section">
  55. <text class="stats-title">跟进统计</text>
  56. <view class="stats-grid">
  57. <view class="stat-item">
  58. <text class="stat-value">{{ clientInfo.stats.followTimes }}</text>
  59. <text class="stat-label">跟进次数</text>
  60. </view>
  61. <view class="stat-item">
  62. <text class="stat-value">{{ clientInfo.stats.matchCount }}</text>
  63. <text class="stat-label">匹配对象</text>
  64. </view>
  65. <view class="stat-item">
  66. <text class="stat-value">{{ clientInfo.stats.phoneCalls }}</text>
  67. <text class="stat-label">电话沟通</text>
  68. </view>
  69. <view class="stat-item">
  70. <text class="stat-value">{{ clientInfo.stats.interviews }}</text>
  71. <text class="stat-label">面谈次数</text>
  72. </view>
  73. </view>
  74. </view>
  75. <!-- 标签切换 -->
  76. <view class="tabs-section">
  77. <text class="tab" :class="{ active: activeTab === 'details' }" @click="switchTab('details')">详细信息</text>
  78. <text class="tab" :class="{ active: activeTab === 'follow' }" @click="switchTab('follow')">跟进</text>
  79. </view>
  80. <!-- 详细信息 -->
  81. <scroll-view scroll-y class="details-section" v-if="activeTab === 'details'">
  82. <!-- 基本信息 -->
  83. <view class="detail-item">
  84. <text class="detail-label">婚姻状况:</text>
  85. <text class="detail-value">{{ clientInfo.details.maritalStatus }}</text>
  86. </view>
  87. <view class="detail-item">
  88. <text class="detail-label">学历:</text>
  89. <text class="detail-value">{{ clientInfo.details.education }}</text>
  90. </view>
  91. <view class="detail-item">
  92. <text class="detail-label">职业:</text>
  93. <text class="detail-value">{{ clientInfo.details.occupation }}</text>
  94. </view>
  95. <view class="detail-item">
  96. <text class="detail-label">收入:</text>
  97. <text class="detail-value">
  98. {{ clientInfo.details.income }}
  99. <text v-if="clientInfo.details.income && clientInfo.details.income !== '未知'">万元/年</text>
  100. </text>
  101. </view>
  102. <view class="detail-item">
  103. <text class="detail-label">购房情况:</text>
  104. <text class="detail-value">{{ clientInfo.details.housing }}</text>
  105. </view>
  106. <!-- 择偶要求详情 -->
  107. <view class="sub-section-title requirement-section-title">择偶详细要求</view>
  108. <view class="detail-item">
  109. <text class="detail-label">年龄范围:</text>
  110. <text class="detail-value">{{ clientInfo.details.requirement.ageRange }}</text>
  111. </view>
  112. <view class="detail-item">
  113. <text class="detail-label">身高要求:</text>
  114. <text class="detail-value">{{ clientInfo.details.requirement.height }}</text>
  115. </view>
  116. <view class="detail-item">
  117. <text class="detail-label">婚姻状况:</text>
  118. <text class="detail-value">{{ clientInfo.details.requirement.maritalStatus }}</text>
  119. </view>
  120. <view class="detail-item">
  121. <text class="detail-label">学历要求:</text>
  122. <text class="detail-value">{{ clientInfo.details.requirement.education }}</text>
  123. </view>
  124. <view class="detail-item" v-if="clientInfo.details.requirement.other">
  125. <text class="detail-label">其他要求:</text>
  126. <text class="detail-value">{{ clientInfo.details.requirement.other }}</text>
  127. </view>
  128. <!-- 客户标签 -->
  129. <view class="sub-section-title tags-section-title">客户标签</view>
  130. <view class="client-tags">
  131. <text class="tag" v-for="(tag, index) in clientInfo.tags" :key="index">{{ tag }}</text>
  132. </view>
  133. </scroll-view>
  134. <!-- 跟进信息 -->
  135. <scroll-view scroll-y class="follow-section" v-else>
  136. <!-- 跟进方式 -->
  137. <view class="follow-group">
  138. <text class="group-label">跟进方式</text>
  139. <view class="follow-type-options">
  140. <view class="follow-type-item" :class="{ active: followForm.type === 'phone' }" @click="followForm.type = 'phone'">
  141. <text class="radio-circle"></text>
  142. <text class="option-text">电话</text>
  143. </view>
  144. <view class="follow-type-item" :class="{ active: followForm.type === 'meet' }" @click="followForm.type = 'meet'">
  145. <text class="radio-circle"></text>
  146. <text class="option-text">面谈</text>
  147. </view>
  148. <view class="follow-type-item" :class="{ active: followForm.type === 'other' }" @click="followForm.type = 'other'">
  149. <text class="radio-circle"></text>
  150. <text class="option-text">其他</text>
  151. </view>
  152. </view>
  153. </view>
  154. <!-- 匹配状态 -->
  155. <view class="follow-group">
  156. <text class="group-label">匹配状态</text>
  157. <view class="status-tags-row">
  158. <view class="status-tag" :class="{ active: followForm.matchStatus === 0 }" @click="handleMatchStatusChange(0)">未匹配</view>
  159. <view class="status-tag" :class="{ active: followForm.matchStatus === 1 }" @click="handleMatchStatusChange(1)">已匹配</view>
  160. </view>
  161. </view>
  162. <!-- 跟进进度 -->
  163. <view class="follow-group">
  164. <text class="group-label">跟进进度</text>
  165. <view class="status-tags-row">
  166. <view class="status-tag" :class="{ active: followForm.progress === 'notMet' }" @click="followForm.progress = 'notMet'">未见面</view>
  167. <view class="status-tag" :class="{ active: followForm.progress === 'communicating' }" @click="followForm.progress = 'communicating'">沟通中</view>
  168. <view class="status-tag" :class="{ active: followForm.invitationStatus === 0 }" @click="handleInvitationStatusChange(0)">未邀约</view>
  169. <view class="status-tag" :class="{ active: followForm.invitationStatus === 1 }" @click="handleInvitationStatusChange(1)">已邀约</view>
  170. </view>
  171. </view>
  172. <!-- 邀约时间 -->
  173. <view class="follow-group">
  174. <text class="group-label">邀约时间</text>
  175. <view class="input-like">
  176. <picker mode="date" :value="followForm.inviteDate" @change="onInviteDateChange">
  177. <view class="picker-inner">{{ followForm.inviteDate || '请选择日期' }}</view>
  178. </picker>
  179. </view>
  180. </view>
  181. <!-- 跟进备注 -->
  182. <view class="follow-group">
  183. <text class="group-label">跟进备注</text>
  184. <textarea class="textarea" v-model="followForm.remark" placeholder="请输入跟进备注详细信息..." placeholder-style="color:#C5A4D8" />
  185. </view>
  186. <!-- 提交按钮(预留) -->
  187. <view class="follow-submit-btn" :class="{ 'disabled': !canFollow }" @click="handleSubmitFollow">保存跟进</view>
  188. </scroll-view>
  189. </view>
  190. </template>
  191. <script>
  192. export default {
  193. name: 'client-detail',
  194. data() {
  195. return {
  196. activeTab: 'details',
  197. followForm: {
  198. type: 'phone',
  199. progress: 'matched',
  200. matchStatus: 0, // 0-未匹配,1-已匹配
  201. invitationStatus: 0, // 0-未邀约,1-已邀约
  202. inviteDate: '',
  203. remark: ''
  204. },
  205. resourceId: null, // 资源ID
  206. fromQualityResources: false, // 是否从优质资源列表进入
  207. currentUserId: null, // 当前登录用户的用户ID
  208. currentMatchmakerId: null, // 当前红娘的matchmaker_id
  209. resourceMatchmakerId: null, // 资源的matchmaker_id
  210. canFollow: true, // 是否可以跟进
  211. clientInfo: {
  212. id: 1,
  213. name: '',
  214. gender: '男',
  215. status: '已匹配',
  216. tags: [],
  217. avatar: '',
  218. requirement: '暂无要求',
  219. contact: '',
  220. backupContact: '',
  221. originalPhone: '', // 保存原始手机号用于复制
  222. originalBackupPhone: '', // 保存原始备用手机号用于复制
  223. stats: {
  224. followTimes: 0,
  225. matchCount: 0,
  226. phoneCalls: 0,
  227. interviews: 0
  228. },
  229. details: {
  230. maritalStatus: '',
  231. education: '',
  232. occupation: '',
  233. income: '',
  234. housing: '',
  235. requirement: {
  236. ageRange: '',
  237. height: '',
  238. maritalStatus: '',
  239. education: '',
  240. other: ''
  241. }
  242. }
  243. },
  244. isDarkTheme: false
  245. }
  246. },
  247. onShow() {
  248. const stored = uni.getStorageSync('matchmakerDarkMode')
  249. if (stored === true || stored === false) {
  250. this.isDarkTheme = stored
  251. }
  252. // 如果已经有resourceId,刷新统计数据
  253. if (this.resourceId) {
  254. this.loadFollowStatistics(this.resourceId)
  255. }
  256. },
  257. onLoad(options) {
  258. // 从URL参数获取资源ID或用户ID
  259. let resourceId = null
  260. let userId = null
  261. if (options.resourceId) {
  262. resourceId = options.resourceId
  263. } else if (options.id) {
  264. // 兼容旧的id参数
  265. resourceId = options.id
  266. }
  267. // 检查是否有userId参数(从优质资源列表进入时使用)
  268. if (options.userId) {
  269. userId = options.userId
  270. }
  271. // 检查是否从优质资源列表进入
  272. if (options.fromQualityResources === '1' || options.fromQualityResources === 1) {
  273. this.fromQualityResources = true
  274. }
  275. // 获取当前登录用户的ID
  276. const currentUserId = uni.getStorageSync('userId')
  277. if (currentUserId) {
  278. const rawUserId = parseInt(currentUserId)
  279. if (!isNaN(rawUserId) && rawUserId > 0) {
  280. this.currentUserId = rawUserId
  281. // 获取当前红娘的matchmaker_id
  282. this.getCurrentMatchmakerId()
  283. }
  284. }
  285. // 优先使用userId(从优质资源列表进入)
  286. if (userId) {
  287. userId = parseInt(userId)
  288. if (isNaN(userId) || userId <= 0) {
  289. uni.showToast({
  290. title: '用户ID无效',
  291. icon: 'none'
  292. })
  293. return
  294. }
  295. // 使用userId加载用户详情
  296. this.loadUserInfo(userId)
  297. } else if (resourceId) {
  298. // 确保resourceId是数字类型
  299. resourceId = parseInt(resourceId)
  300. if (isNaN(resourceId) || resourceId <= 0) {
  301. uni.showToast({
  302. title: '资源ID无效',
  303. icon: 'none'
  304. })
  305. return
  306. }
  307. this.resourceId = resourceId
  308. this.loadClientInfo(resourceId)
  309. } else {
  310. uni.showToast({
  311. title: '未获取到资源ID或用户ID',
  312. icon: 'none'
  313. })
  314. }
  315. },
  316. methods: {
  317. // 返回上一页
  318. handleBack() {
  319. uni.navigateBack()
  320. },
  321. // 复制联系方式
  322. handleCopy(contact) {
  323. // 如果传入的是脱敏的手机号,使用原始手机号
  324. let phoneToCopy = contact
  325. if (contact === this.clientInfo.contact && this.clientInfo.originalPhone) {
  326. phoneToCopy = this.clientInfo.originalPhone
  327. } else if (contact === this.clientInfo.backupContact && this.clientInfo.originalBackupPhone) {
  328. phoneToCopy = this.clientInfo.originalBackupPhone
  329. } else {
  330. // 尝试从脱敏号码中提取
  331. phoneToCopy = contact.replace(/\*+/g, '')
  332. }
  333. uni.setClipboardData({
  334. data: phoneToCopy,
  335. success: () => {
  336. uni.showToast({
  337. title: '复制成功',
  338. icon: 'success'
  339. })
  340. },
  341. fail: () => {
  342. uni.showToast({
  343. title: '复制失败',
  344. icon: 'none'
  345. })
  346. }
  347. })
  348. },
  349. // 切换标签
  350. switchTab(tab) {
  351. this.activeTab = tab
  352. },
  353. // 邀约日期变更
  354. onInviteDateChange(e) {
  355. this.followForm.inviteDate = e.detail.value
  356. },
  357. // 匹配状态变更
  358. handleMatchStatusChange(status) {
  359. this.followForm.matchStatus = status
  360. },
  361. // 邀约状态变更
  362. handleInvitationStatusChange(status) {
  363. this.followForm.invitationStatus = status
  364. // 如果选择已邀约且没有设置邀约时间,提示用户选择
  365. if (status === 1 && !this.followForm.inviteDate) {
  366. uni.showToast({
  367. title: '请选择邀约时间',
  368. icon: 'none'
  369. })
  370. }
  371. },
  372. // 加载最新的跟进信息,更新表单字段
  373. async loadLatestFollowInfo(resourceId) {
  374. try {
  375. const baseUrl = process.env.NODE_ENV === 'development'
  376. ? 'https://api.zhongruanke.cn/api' // 开发环境 - 通过网关
  377. : 'https://your-domain.com/api' // 生产环境
  378. // 获取最新的跟进记录
  379. const [error, res] = await uni.request({
  380. url: `${baseUrl}/my-resource/latest-follow/${resourceId}`,
  381. method: 'GET',
  382. header: {
  383. 'Content-Type': 'application/json'
  384. }
  385. })
  386. if (error) {
  387. return
  388. }
  389. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  390. const followUp = res.data.data
  391. if (followUp) {
  392. // 更新跟进方式(1-电话,2-面谈,3-其他)
  393. if (followUp.followType !== null && followUp.followType !== undefined) {
  394. const followType = followUp.followType
  395. if (followType === 1) {
  396. this.followForm.type = 'phone'
  397. } else if (followType === 2) {
  398. this.followForm.type = 'meet'
  399. } else if (followType === 3) {
  400. this.followForm.type = 'other'
  401. }
  402. }
  403. // 更新跟进进度(1-已匹配,2-未见面,3-沟通中)
  404. if (followUp.followProgress !== null && followUp.followProgress !== undefined) {
  405. const progress = followUp.followProgress
  406. if (progress === 1) {
  407. this.followForm.progress = 'matched'
  408. } else if (progress === 2) {
  409. this.followForm.progress = 'notMet'
  410. } else if (progress === 3) {
  411. this.followForm.progress = 'communicating'
  412. }
  413. }
  414. // 更新匹配状态
  415. if (followUp.isMatch !== null && followUp.isMatch !== undefined) {
  416. this.followForm.matchStatus = parseInt(followUp.isMatch) || 0
  417. }
  418. // 更新邀约状态
  419. if (followUp.isInvitation !== null && followUp.isInvitation !== undefined) {
  420. this.followForm.invitationStatus = parseInt(followUp.isInvitation) || 0
  421. }
  422. // 更新邀约时间
  423. if (followUp.invitationTime) {
  424. const date = new Date(followUp.invitationTime)
  425. const year = date.getFullYear()
  426. const month = String(date.getMonth() + 1).padStart(2, '0')
  427. const day = String(date.getDate()).padStart(2, '0')
  428. this.followForm.inviteDate = `${year}-${month}-${day}`
  429. } else {
  430. this.followForm.inviteDate = ''
  431. }
  432. // 更新备注
  433. if (followUp.remark !== null && followUp.remark !== undefined) {
  434. this.followForm.remark = followUp.remark || ''
  435. } else {
  436. this.followForm.remark = ''
  437. }
  438. }
  439. }
  440. } catch (e) {
  441. }
  442. },
  443. // 加载跟进统计数据
  444. async loadFollowStatistics(resourceId) {
  445. try {
  446. const baseUrl = process.env.NODE_ENV === 'development'
  447. ? 'https://api.zhongruanke.cn/api' // 开发环境 - 通过网关
  448. : 'https://your-domain.com/api' // 生产环境
  449. const [error, res] = await uni.request({
  450. url: `${baseUrl}/my-resource/follow-statistics/${resourceId}`,
  451. method: 'GET',
  452. header: {
  453. 'Content-Type': 'application/json'
  454. }
  455. })
  456. if (error) {
  457. return
  458. }
  459. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  460. const statistics = res.data.data
  461. // 更新统计数据(支持驼峰和下划线两种字段名格式)
  462. if (this.clientInfo && this.clientInfo.stats) {
  463. const followCount = statistics?.followCount ?? statistics?.follow_count ?? 0
  464. const matchCount = statistics?.matchCount ?? statistics?.match_count ?? 0
  465. const phoneCount = statistics?.phoneCount ?? statistics?.phone_count ?? 0
  466. const interviewCount = statistics?.interviewCount ?? statistics?.interview_count ?? 0
  467. this.clientInfo.stats.followTimes = followCount
  468. this.clientInfo.stats.matchCount = matchCount
  469. this.clientInfo.stats.phoneCalls = phoneCount
  470. this.clientInfo.stats.interviews = interviewCount
  471. }
  472. }
  473. } catch (e) {
  474. }
  475. },
  476. // 提交跟进
  477. async handleSubmitFollow() {
  478. // 检查是否可以跟进
  479. if (!this.canFollow) {
  480. uni.showToast({
  481. title: '该用户还没有添加到你的资源当中',
  482. icon: 'none',
  483. duration: 2000
  484. })
  485. return
  486. }
  487. if (!this.resourceId) {
  488. uni.showToast({
  489. title: '资源ID不存在',
  490. icon: 'none'
  491. })
  492. return
  493. }
  494. try {
  495. uni.showLoading({
  496. title: '保存中...'
  497. })
  498. const baseUrl = process.env.NODE_ENV === 'development'
  499. ? 'https://api.zhongruanke.cn/api' // 开发环境 - 通过网关
  500. : 'https://your-domain.com/api' // 生产环境
  501. // 准备请求数据
  502. // 跟进方式转换:phone->1, meet->2, other->3
  503. let followType = 1 // 默认电话
  504. if (this.followForm.type === 'meet') {
  505. followType = 2
  506. } else if (this.followForm.type === 'other') {
  507. followType = 3
  508. }
  509. // 跟进进度转换:matched->1, notMet->2, communicating->3
  510. let progress = 1 // 默认已匹配
  511. if (this.followForm.progress === 'notMet') {
  512. progress = 2
  513. } else if (this.followForm.progress === 'communicating') {
  514. progress = 3
  515. }
  516. const requestData = {
  517. followType: followType,
  518. progress: this.followForm.progress, // 保持字符串格式,后端会转换
  519. matchStatus: this.followForm.matchStatus,
  520. invitationStatus: this.followForm.invitationStatus,
  521. invitationTime: this.followForm.inviteDate || null,
  522. remark: this.followForm.remark || '' // 添加备注字段
  523. }
  524. const [error, res] = await uni.request({
  525. url: `${baseUrl}/my-resource/update-follow/${this.resourceId}`,
  526. method: 'PUT',
  527. data: requestData,
  528. header: {
  529. 'Content-Type': 'application/json'
  530. }
  531. })
  532. uni.hideLoading()
  533. if (error) {
  534. uni.showToast({
  535. title: '保存失败',
  536. icon: 'none'
  537. })
  538. return
  539. }
  540. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  541. uni.showToast({
  542. title: '跟进已保存',
  543. icon: 'success'
  544. })
  545. // 刷新统计数据
  546. if (this.resourceId) {
  547. this.loadFollowStatistics(this.resourceId)
  548. }
  549. // 重新加载最新的跟进信息,更新表单字段
  550. if (this.resourceId) {
  551. this.loadLatestFollowInfo(this.resourceId)
  552. }
  553. // 触发资源列表刷新事件
  554. uni.$emit('refreshResourceList')
  555. } else {
  556. const errorMsg = res.data?.message || '保存失败'
  557. uni.showToast({
  558. title: errorMsg,
  559. icon: 'none'
  560. })
  561. }
  562. } catch (e) {
  563. uni.hideLoading()
  564. uni.showToast({
  565. title: '保存失败,请稍后重试',
  566. icon: 'none'
  567. })
  568. }
  569. },
  570. // 获取当前红娘的matchmaker_id
  571. async getCurrentMatchmakerId() {
  572. if (!this.currentUserId) {
  573. return
  574. }
  575. try {
  576. const baseUrl = process.env.NODE_ENV === 'development'
  577. ? 'https://api.zhongruanke.cn/api' // 开发环境 - 通过网关
  578. : 'https://your-domain.com/api' // 生产环境
  579. // 根据user_id查询红娘信息
  580. const [error, res] = await uni.request({
  581. url: `${baseUrl}/matchmaker/by-user/${this.currentUserId}`,
  582. method: 'GET'
  583. })
  584. if (error) {
  585. return
  586. }
  587. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  588. const matchmaker = res.data.data
  589. this.currentMatchmakerId = matchmaker.matchmakerId || matchmaker.matchmaker_id
  590. // 如果已经加载了资源信息,重新判断是否可以跟进
  591. if (this.resourceMatchmakerId !== null && this.resourceMatchmakerId !== undefined) {
  592. if (this.fromQualityResources) {
  593. if (!this.currentMatchmakerId || this.resourceMatchmakerId !== this.currentMatchmakerId) {
  594. this.canFollow = false
  595. } else {
  596. this.canFollow = true
  597. }
  598. }
  599. }
  600. }
  601. } catch (e) {
  602. }
  603. },
  604. // 从API加载客户信息
  605. async loadClientInfo(resourceId) {
  606. try {
  607. uni.showLoading({
  608. title: '加载中...'
  609. })
  610. const baseUrl = process.env.NODE_ENV === 'development'
  611. ? 'https://api.zhongruanke.cn/api' // 开发环境 - 通过网关
  612. : 'https://your-domain.com/api' // 生产环境
  613. const requestUrl = `${baseUrl}/my-resource/client-detail/${resourceId}`
  614. const [error, res] = await uni.request({
  615. url: requestUrl,
  616. method: 'GET',
  617. timeout: 30000, // 设置30秒超时
  618. header: {
  619. 'Content-Type': 'application/json'
  620. }
  621. })
  622. uni.hideLoading()
  623. if (error) {
  624. let errorMsg = '加载失败'
  625. if (error.errMsg) {
  626. if (error.errMsg.includes('timeout') || error.errMsg.includes('超时')) {
  627. errorMsg = '请求超时,请检查网络连接'
  628. } else if (error.errMsg.includes('fail')) {
  629. errorMsg = '网络连接失败,请检查服务器是否启动'
  630. }
  631. }
  632. uni.showToast({
  633. title: errorMsg,
  634. icon: 'none',
  635. duration: 3000
  636. })
  637. return
  638. }
  639. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  640. const data = res.data.data
  641. // 支持驼峰和下划线两种字段名格式
  642. const resourceId = data.resourceId || data.resource_id
  643. const avatarUrl = data.avatarUrl || data.avatar_url
  644. const backupPhone = data.backupPhone || data.backup_phone
  645. const marrStatus = data.marrStatus !== undefined ? data.marrStatus : data.marr_status
  646. // 择偶要求从my_resource表的mate_selection_criteria字段获取
  647. const mateSelectionCriteria = data.mateSelectionCriteria || data.mate_selection_criteria
  648. // 获取资源的matchmaker_id
  649. const resourceMatchmakerId = data.matchmakerId || data.matchmaker_id
  650. this.resourceMatchmakerId = resourceMatchmakerId
  651. // 判断是否可以跟进:如果从优质资源列表进入,且资源不属于当前红娘,则不能跟进
  652. if (this.fromQualityResources) {
  653. // 如果还没有获取到当前红娘的ID,先获取
  654. if (!this.currentMatchmakerId) {
  655. await this.getCurrentMatchmakerId()
  656. }
  657. if (!this.currentMatchmakerId || this.resourceMatchmakerId !== this.currentMatchmakerId) {
  658. this.canFollow = false
  659. } else {
  660. this.canFollow = true
  661. }
  662. } else {
  663. // 从我的资源列表进入,默认可以跟进
  664. this.canFollow = true
  665. }
  666. // 择偶要求详情字段(从partner_requirement表,用于详情页)
  667. const minAge = data.minAge !== undefined ? data.minAge : data.min_age
  668. const maxAge = data.maxAge !== undefined ? data.maxAge : data.max_age
  669. const minHeight = data.minHeight !== undefined ? data.minHeight : data.min_height
  670. const maxHeight = data.maxHeight !== undefined ? data.maxHeight : data.max_height
  671. const educationLevel = data.educationLevel !== undefined ? data.educationLevel : data.education_level
  672. const salaryRange = data.salaryRange !== undefined ? data.salaryRange : data.salary_range
  673. const houseRequirement = data.houseRequirement !== undefined ? data.houseRequirement : data.house_requirement
  674. const carRequirement = data.carRequirement !== undefined ? data.carRequirement : data.car_requirement
  675. const maritalStatusRequirement = data.maritalStatusRequirement !== undefined ? data.maritalStatusRequirement : data.marital_status_requirement
  676. const preferredCity = data.preferredCity || data.preferred_city
  677. const otherRequirements = data.otherRequirements || data.other_requirements
  678. // 处理标签列表(从后端返回的tags字段,如果没有则使用星座和职业)
  679. let clientTags = []
  680. if (data.tags && Array.isArray(data.tags) && data.tags.length > 0) {
  681. // 使用后端返回的标签
  682. clientTags = data.tags
  683. } else {
  684. // 如果没有tags,使用星座和职业作为标签
  685. if (data.constellation) {
  686. clientTags.push(data.constellation)
  687. }
  688. if (data.occupation) {
  689. clientTags.push(data.occupation)
  690. }
  691. }
  692. // 加载匹配状态和邀约状态
  693. const isMatch = data.isMatch !== null && data.isMatch !== undefined ? data.isMatch :
  694. (data.is_match !== null && data.is_match !== undefined ? data.is_match : 0)
  695. const isInvitation = data.isInvitation !== null && data.isInvitation !== undefined ? data.isInvitation :
  696. (data.is_Invitation !== null && data.is_Invitation !== undefined ? data.is_Invitation : 0)
  697. const invitationTime = data.invitationTime || data.Invitation_time
  698. this.followForm.matchStatus = parseInt(isMatch) || 0
  699. this.followForm.invitationStatus = parseInt(isInvitation) || 0
  700. if (invitationTime) {
  701. // 将日期转换为YYYY-MM-DD格式
  702. const date = new Date(invitationTime)
  703. const year = date.getFullYear()
  704. const month = String(date.getMonth() + 1).padStart(2, '0')
  705. const day = String(date.getDate()).padStart(2, '0')
  706. this.followForm.inviteDate = `${year}-${month}-${day}`
  707. }
  708. // 映射数据到clientInfo
  709. this.clientInfo = {
  710. id: resourceId,
  711. name: data.name || '',
  712. gender: data.gender === 1 ? '男' : data.gender === 2 ? '女' : '未知',
  713. status: '已匹配', // 可以根据实际字段判断
  714. tags: clientTags, // 使用处理后的标签列表
  715. avatar: avatarUrl || '',
  716. requirement: mateSelectionCriteria || '暂无要求', // 择偶要求显示my_resource表的mate_selection_criteria字段
  717. contact: data.phone ? data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  718. backupContact: backupPhone ? backupPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  719. originalPhone: data.phone || '', // 保存原始手机号
  720. originalBackupPhone: backupPhone || '', // 保存原始备用手机号
  721. stats: {
  722. followTimes: 0, // 将从统计数据接口获取
  723. matchCount: 0,
  724. phoneCalls: 0,
  725. interviews: 0
  726. },
  727. details: {
  728. maritalStatus: this.formatMaritalStatus(marrStatus),
  729. education: data.diploma || '未知',
  730. occupation: data.occupation || '未知',
  731. income: data.income || '未知',
  732. housing: data.house === 1 ? '已购房' : data.house === 0 ? '未购房' : '未知',
  733. requirement: {
  734. ageRange: minAge && maxAge ? `${minAge}-${maxAge}岁` : '不限',
  735. height: minHeight ? `${minHeight}cm以上` : '不限',
  736. maritalStatus: this.formatMaritalStatusRequirement(maritalStatusRequirement),
  737. education: this.formatEducationLevel(educationLevel),
  738. other: otherRequirements || ''
  739. }
  740. }
  741. }
  742. // 标签已经在上面处理过了,这里不需要再添加
  743. // 加载跟进统计数据
  744. this.loadFollowStatistics(resourceId)
  745. } else {
  746. uni.showToast({
  747. title: res.data.message || '加载失败',
  748. icon: 'none'
  749. })
  750. }
  751. } catch (e) {
  752. uni.hideLoading()
  753. uni.showToast({
  754. title: '加载失败',
  755. icon: 'none'
  756. })
  757. }
  758. },
  759. // 从API加载用户信息(用于优质资源列表点击查看详情)
  760. async loadUserInfo(userId) {
  761. try {
  762. uni.showLoading({
  763. title: '加载中...'
  764. })
  765. const baseUrl = process.env.NODE_ENV === 'development'
  766. ? 'https://api.zhongruanke.cn/api'
  767. : 'https://api.zhongruanke.cn/api'
  768. const requestUrl = `${baseUrl}/my-resource/user-detail/${userId}`
  769. const [error, res] = await uni.request({
  770. url: requestUrl,
  771. method: 'GET',
  772. timeout: 30000,
  773. header: {
  774. 'Content-Type': 'application/json'
  775. }
  776. })
  777. uni.hideLoading()
  778. if (error) {
  779. let errorMsg = '加载失败'
  780. if (error.errMsg) {
  781. if (error.errMsg.includes('timeout') || error.errMsg.includes('超时')) {
  782. errorMsg = '请求超时,请检查网络连接'
  783. } else if (error.errMsg.includes('fail')) {
  784. errorMsg = '网络连接失败'
  785. }
  786. }
  787. uni.showToast({
  788. title: errorMsg,
  789. icon: 'none',
  790. duration: 3000
  791. })
  792. return
  793. }
  794. if (res.statusCode === 200 && res.data && res.data.code === 200) {
  795. const data = res.data.data
  796. // 支持驼峰和下划线两种字段名格式
  797. const avatarUrl = data.avatarUrl || data.avatar_url
  798. // 择偶要求详情字段
  799. const minAge = data.minAge !== undefined ? data.minAge : data.min_age
  800. const maxAge = data.maxAge !== undefined ? data.maxAge : data.max_age
  801. const minHeight = data.minHeight !== undefined ? data.minHeight : data.min_height
  802. const maxHeight = data.maxHeight !== undefined ? data.maxHeight : data.max_height
  803. const educationLevel = data.educationLevel !== undefined ? data.educationLevel : data.education_level
  804. const salaryRange = data.salaryRange !== undefined ? data.salaryRange : data.salary_range
  805. const houseRequirement = data.houseRequirement !== undefined ? data.houseRequirement : data.house_requirement
  806. const carRequirement = data.carRequirement !== undefined ? data.carRequirement : data.car_requirement
  807. const maritalStatusRequirement = data.maritalStatusRequirement !== undefined ? data.maritalStatusRequirement : data.marital_status_requirement
  808. const preferredCity = data.preferredCity || data.preferred_city
  809. const otherRequirements = data.otherRequirements || data.other_requirements
  810. // 处理标签列表
  811. let clientTags = []
  812. if (data.tags && Array.isArray(data.tags) && data.tags.length > 0) {
  813. clientTags = data.tags
  814. } else {
  815. if (data.constellation) {
  816. clientTags.push(data.constellation)
  817. }
  818. if (data.occupation) {
  819. clientTags.push(data.occupation)
  820. }
  821. }
  822. // 从优质资源进入,不能跟进(因为还没有添加到我的资源)
  823. this.canFollow = false
  824. // 映射数据到clientInfo
  825. this.clientInfo = {
  826. id: userId,
  827. name: data.name || data.nickname || '',
  828. gender: data.gender === 1 ? '男' : data.gender === 2 ? '女' : '未知',
  829. status: '未绑定',
  830. tags: clientTags,
  831. avatar: avatarUrl || '',
  832. requirement: '暂无要求',
  833. contact: data.phone ? data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
  834. backupContact: '',
  835. originalPhone: data.phone || '',
  836. originalBackupPhone: '',
  837. stats: {
  838. followTimes: 0,
  839. matchCount: 0,
  840. phoneCalls: 0,
  841. interviews: 0
  842. },
  843. details: {
  844. maritalStatus: this.formatMaritalStatus(data.marrStatus || data.marr_status),
  845. education: data.diploma || '未知',
  846. occupation: data.occupation || '未知',
  847. income: data.income || '未知',
  848. housing: data.house === 1 ? '已购房' : data.house === 0 ? '未购房' : '未知',
  849. requirement: {
  850. ageRange: minAge && maxAge ? `${minAge}-${maxAge}岁` : '不限',
  851. height: minHeight ? `${minHeight}cm以上` : '不限',
  852. maritalStatus: this.formatMaritalStatusRequirement(maritalStatusRequirement),
  853. education: this.formatEducationLevel(educationLevel),
  854. other: otherRequirements || ''
  855. }
  856. }
  857. }
  858. } else {
  859. uni.showToast({
  860. title: res.data.message || '加载失败',
  861. icon: 'none'
  862. })
  863. }
  864. } catch (e) {
  865. uni.hideLoading()
  866. uni.showToast({
  867. title: '加载失败',
  868. icon: 'none'
  869. })
  870. }
  871. },
  872. // 格式化婚姻状况
  873. formatMaritalStatus(status) {
  874. if (status === null || status === undefined) return '未知'
  875. switch (status) {
  876. case 0: return '未婚'
  877. case 1: return '离异'
  878. case 2: return '丧偶'
  879. default: return '未知'
  880. }
  881. },
  882. // 格式化婚姻状况要求
  883. formatMaritalStatusRequirement(status) {
  884. if (status === null || status === undefined) return '不限'
  885. switch (status) {
  886. case 0: return '不限'
  887. case 1: return '未婚'
  888. case 2: return '离异'
  889. case 3: return '丧偶'
  890. default: return '不限'
  891. }
  892. },
  893. // 格式化学历要求
  894. formatEducationLevel(level) {
  895. if (level === null || level === undefined) return '不限'
  896. switch (level) {
  897. case 0: return '不限'
  898. case 1: return '高中及以上'
  899. case 2: return '专科及以上'
  900. case 3: return '本科及以上'
  901. case 4: return '硕士及以上'
  902. case 5: return '博士及以上'
  903. default: return '不限'
  904. }
  905. }
  906. }
  907. }
  908. </script>
  909. <style lang="scss" scoped>
  910. .client-detail {
  911. min-height: 100vh;
  912. background: linear-gradient(180deg, #FFF5F8 0%, #F8E8F0 50%, #FFF9F9 100%);
  913. padding-top: 90rpx;
  914. }
  915. /* 顶部导航栏 */
  916. .header {
  917. position: fixed;
  918. top: 0;
  919. left: 0;
  920. right: 0;
  921. height: 90rpx;
  922. display: flex;
  923. align-items: center;
  924. justify-content: space-between;
  925. padding: 0 20rpx;
  926. background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
  927. z-index: 999;
  928. .back-icon {
  929. width: 44rpx;
  930. height: 44rpx;
  931. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23333"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>');
  932. background-size: contain;
  933. background-repeat: no-repeat;
  934. background-position: center;
  935. }
  936. .header-title {
  937. font-size: 38rpx;
  938. font-weight: bold;
  939. color: #333;
  940. }
  941. .header-right {
  942. width: 44rpx;
  943. }
  944. }
  945. /* 客户基本信息 */
  946. .client-basic-info {
  947. display: flex;
  948. align-items: flex-start;
  949. padding: 35rpx;
  950. background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
  951. margin: 20rpx;
  952. border-radius: 24rpx;
  953. box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
  954. border: 1rpx solid rgba(243, 229, 245, 0.8);
  955. .client-avatar {
  956. width: 160rpx;
  957. height: 160rpx;
  958. border-radius: 50%;
  959. margin-right: 30rpx;
  960. background-color: #F5F5F5;
  961. }
  962. .basic-info {
  963. flex: 1;
  964. .client-name-wrapper {
  965. display: flex;
  966. align-items: center;
  967. gap: 12rpx;
  968. margin-bottom: 12rpx;
  969. }
  970. .client-name {
  971. font-size: 38rpx;
  972. font-weight: 600;
  973. color: #2C2C2C;
  974. letter-spacing: 0.5rpx;
  975. }
  976. .gender-tag {
  977. font-size: 24rpx;
  978. font-weight: 500;
  979. padding: 6rpx 14rpx;
  980. border-radius: 12rpx;
  981. background: #E3F2FD;
  982. color: #2196F3;
  983. border: 1rpx solid #BBDEFB;
  984. }
  985. .status-tag-wrapper {
  986. display: flex;
  987. align-items: center;
  988. gap: 10rpx;
  989. margin-bottom: 18rpx;
  990. }
  991. .status-tag {
  992. display: inline-block;
  993. padding: 4rpx 12rpx;
  994. border-radius: 12rpx;
  995. font-size: 22rpx;
  996. font-weight: 500;
  997. &.register-tag {
  998. &.registered {
  999. background: #E8F5E9;
  1000. color: #4CAF50;
  1001. }
  1002. }
  1003. &.match-tag {
  1004. &.matched {
  1005. background: #E3F2FD;
  1006. color: #2196F3;
  1007. }
  1008. &.unmatched {
  1009. background: #FFF3E0;
  1010. color: #FF9800;
  1011. }
  1012. }
  1013. }
  1014. .client-status {
  1015. font-size: 26rpx;
  1016. color: #FF6B8A;
  1017. margin-bottom: 18rpx;
  1018. display: inline-block;
  1019. padding: 4rpx 16rpx;
  1020. background: linear-gradient(135deg, #FFE5EB 0%, #FFF0F5 100%);
  1021. border-radius: 12rpx;
  1022. font-weight: 500;
  1023. }
  1024. .client-tags {
  1025. display: flex;
  1026. flex-wrap: wrap;
  1027. gap: 10rpx;
  1028. .tag {
  1029. padding: 10rpx 22rpx;
  1030. background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
  1031. color: #7B1FA2;
  1032. border-radius: 24rpx;
  1033. font-size: 24rpx;
  1034. font-weight: 600;
  1035. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.15);
  1036. border: 1rpx solid rgba(156, 39, 176, 0.2);
  1037. }
  1038. }
  1039. }
  1040. }
  1041. /* 择偶要求 */
  1042. .requirement-section {
  1043. padding: 35rpx;
  1044. background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
  1045. margin: 0 20rpx 20rpx;
  1046. border-radius: 24rpx;
  1047. box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
  1048. border: 1rpx solid rgba(243, 229, 245, 0.8);
  1049. .section-title {
  1050. display: flex;
  1051. align-items: center;
  1052. margin-bottom: 20rpx;
  1053. .title-icon {
  1054. font-size: 28rpx;
  1055. margin-right: 10rpx;
  1056. }
  1057. .title-text {
  1058. font-size: 30rpx;
  1059. font-weight: 600;
  1060. color: #2C2C2C;
  1061. letter-spacing: 0.5rpx;
  1062. }
  1063. }
  1064. .requirement-content {
  1065. font-size: 28rpx;
  1066. color: #555;
  1067. line-height: 1.8;
  1068. font-weight: 400;
  1069. }
  1070. }
  1071. /* 联系方式 */
  1072. .contact-section {
  1073. padding: 35rpx;
  1074. background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
  1075. margin: 0 20rpx 20rpx;
  1076. border-radius: 24rpx;
  1077. box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
  1078. border: 1rpx solid rgba(243, 229, 245, 0.8);
  1079. .contact-item {
  1080. display: flex;
  1081. justify-content: space-between;
  1082. align-items: center;
  1083. margin-bottom: 25rpx;
  1084. &:last-child {
  1085. margin-bottom: 0;
  1086. }
  1087. .contact-label {
  1088. font-size: 28rpx;
  1089. color: #2C2C2C;
  1090. font-weight: 500;
  1091. }
  1092. .contact-value {
  1093. display: flex;
  1094. align-items: center;
  1095. gap: 20rpx;
  1096. .contact-number {
  1097. font-size: 28rpx;
  1098. color: #555;
  1099. font-weight: 400;
  1100. }
  1101. .copy-btn {
  1102. padding: 10rpx 24rpx;
  1103. background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
  1104. color: #7B1FA2;
  1105. border-radius: 24rpx;
  1106. font-size: 24rpx;
  1107. font-weight: 600;
  1108. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.2);
  1109. border: 1rpx solid rgba(156, 39, 176, 0.2);
  1110. transition: all 0.3s;
  1111. &:active {
  1112. transform: scale(0.95);
  1113. box-shadow: 0 1rpx 4rpx rgba(156, 39, 176, 0.3);
  1114. }
  1115. }
  1116. }
  1117. }
  1118. }
  1119. /* 跟进统计 */
  1120. .stats-section {
  1121. padding: 30rpx;
  1122. background-color: #FFFFFF;
  1123. margin: 0 20rpx 20rpx;
  1124. border-radius: 20rpx;
  1125. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
  1126. .stats-title {
  1127. font-size: 28rpx;
  1128. font-weight: bold;
  1129. color: #333;
  1130. margin-bottom: 25rpx;
  1131. }
  1132. .stats-grid {
  1133. display: grid;
  1134. grid-template-columns: repeat(4, 1fr);
  1135. gap: 20rpx;
  1136. .stat-item {
  1137. display: flex;
  1138. flex-direction: column;
  1139. align-items: center;
  1140. justify-content: center;
  1141. padding: 20rpx;
  1142. background-color: #FFF3F5;
  1143. border-radius: 15rpx;
  1144. .stat-value {
  1145. font-size: 48rpx;
  1146. font-weight: bold;
  1147. color: #FF6B8A;
  1148. margin-bottom: 10rpx;
  1149. }
  1150. .stat-label {
  1151. font-size: 22rpx;
  1152. color: #666;
  1153. }
  1154. }
  1155. }
  1156. }
  1157. /* 标签切换 */
  1158. .tabs-section {
  1159. display: flex;
  1160. padding: 0 20rpx;
  1161. background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
  1162. margin: 0 20rpx 20rpx;
  1163. border-radius: 24rpx;
  1164. box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
  1165. border: 1rpx solid rgba(243, 229, 245, 0.8);
  1166. .tab {
  1167. flex: 1;
  1168. text-align: center;
  1169. padding: 24rpx 0;
  1170. font-size: 30rpx;
  1171. color: #888;
  1172. font-weight: 500;
  1173. transition: all 0.3s;
  1174. position: relative;
  1175. &.active {
  1176. color: #7B1FA2;
  1177. font-weight: 600;
  1178. &::after {
  1179. content: '';
  1180. position: absolute;
  1181. bottom: 0;
  1182. left: 50%;
  1183. transform: translateX(-50%);
  1184. width: 60rpx;
  1185. height: 6rpx;
  1186. background: linear-gradient(90deg, #9C27B0 0%, #E1BEE7 100%);
  1187. border-radius: 3rpx;
  1188. }
  1189. }
  1190. }
  1191. }
  1192. /* 详细信息滚动区域 */
  1193. .details-section {
  1194. padding: 35rpx 25rpx 100rpx;
  1195. background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
  1196. margin: 0 20rpx 20rpx;
  1197. border-radius: 24rpx;
  1198. box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
  1199. border: 1rpx solid rgba(243, 229, 245, 0.8);
  1200. box-sizing: border-box;
  1201. position: relative;
  1202. .detail-item {
  1203. display: flex;
  1204. margin-bottom: 28rpx;
  1205. padding: 12rpx 0;
  1206. border-bottom: 1rpx solid rgba(240, 240, 240, 0.6);
  1207. &:last-of-type {
  1208. border-bottom: none;
  1209. }
  1210. .detail-label {
  1211. width: 200rpx;
  1212. font-size: 28rpx;
  1213. color: #2C2C2C;
  1214. font-weight: 500;
  1215. letter-spacing: 0.3rpx;
  1216. }
  1217. .detail-value {
  1218. flex: 1;
  1219. font-size: 28rpx;
  1220. color: #555;
  1221. font-weight: 400;
  1222. text-align: right;
  1223. }
  1224. }
  1225. .sub-section-title {
  1226. font-size: 32rpx;
  1227. font-weight: 600;
  1228. color: #2C2C2C;
  1229. margin: 30rpx 0 20rpx;
  1230. padding-bottom: 12rpx;
  1231. border-bottom: 2rpx solid rgba(240, 240, 240, 0.8);
  1232. letter-spacing: 0.5rpx;
  1233. }
  1234. /* 择偶详细要求部分 - 添加明显的上分隔 */
  1235. .requirement-section-title {
  1236. margin-top: 60rpx;
  1237. padding: 32rpx 24rpx 20rpx;
  1238. border-top: 3rpx solid rgba(156, 39, 176, 0.15);
  1239. border-bottom: 2rpx solid rgba(156, 39, 176, 0.1);
  1240. position: relative;
  1241. background: linear-gradient(135deg, #F8F4F9 0%, #FFFBFD 100%);
  1242. border-radius: 16rpx 16rpx 0 0;
  1243. }
  1244. .requirement-section-title::before {
  1245. content: '';
  1246. position: absolute;
  1247. left: 0;
  1248. top: 0;
  1249. width: 8rpx;
  1250. height: 100%;
  1251. background: linear-gradient(180deg, #9C27B0 0%, #BA68C8 50%, #E1BEE7 100%);
  1252. border-radius: 0 4rpx 4rpx 0;
  1253. box-shadow: 2rpx 0 8rpx rgba(156, 39, 176, 0.2);
  1254. }
  1255. /* 客户标签部分 - 添加明显的上分隔 */
  1256. .tags-section-title {
  1257. margin-top: 60rpx;
  1258. padding: 32rpx 24rpx 20rpx;
  1259. border-top: 3rpx solid rgba(156, 39, 176, 0.15);
  1260. border-bottom: 2rpx solid rgba(156, 39, 176, 0.1);
  1261. position: relative;
  1262. background: linear-gradient(135deg, #F8F4F9 0%, #FFFBFD 100%);
  1263. border-radius: 16rpx 16rpx 0 0;
  1264. }
  1265. .tags-section-title::before {
  1266. content: '';
  1267. position: absolute;
  1268. left: 0;
  1269. top: 0;
  1270. width: 8rpx;
  1271. height: 100%;
  1272. background: linear-gradient(180deg, #9C27B0 0%, #BA68C8 50%, #E1BEE7 100%);
  1273. border-radius: 0 4rpx 4rpx 0;
  1274. box-shadow: 2rpx 0 8rpx rgba(156, 39, 176, 0.2);
  1275. }
  1276. .client-tags {
  1277. display: flex;
  1278. flex-wrap: wrap;
  1279. gap: 12rpx;
  1280. padding: 20rpx 0;
  1281. .tag {
  1282. padding: 12rpx 24rpx;
  1283. background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
  1284. color: #7B1FA2;
  1285. border-radius: 24rpx;
  1286. font-size: 26rpx;
  1287. font-weight: 600;
  1288. box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.15);
  1289. border: 1rpx solid rgba(156, 39, 176, 0.2);
  1290. transition: all 0.3s;
  1291. &:active {
  1292. transform: scale(0.95);
  1293. }
  1294. }
  1295. }
  1296. }
  1297. /* 跟进信息 */
  1298. .follow-section {
  1299. background-color: #FFFFFF;
  1300. margin: 0 20rpx 20rpx;
  1301. border-radius: 20rpx;
  1302. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
  1303. padding: 30rpx 20rpx 40rpx;
  1304. max-height: 700rpx;
  1305. box-sizing: border-box;
  1306. }
  1307. .follow-group {
  1308. margin-bottom: 30rpx;
  1309. .group-label {
  1310. font-size: 28rpx;
  1311. color: #333;
  1312. font-weight: bold;
  1313. margin-bottom: 16rpx;
  1314. display: block;
  1315. }
  1316. }
  1317. /* 跟进方式单选 */
  1318. .follow-type-options {
  1319. display: flex;
  1320. align-items: center;
  1321. gap: 30rpx;
  1322. .follow-type-item {
  1323. display: flex;
  1324. align-items: center;
  1325. gap: 10rpx;
  1326. font-size: 26rpx;
  1327. color: #666;
  1328. .radio-circle {
  1329. width: 26rpx;
  1330. height: 26rpx;
  1331. border-radius: 50%;
  1332. border: 2rpx solid #C5A4D8;
  1333. box-sizing: border-box;
  1334. }
  1335. &.active {
  1336. color: #D81B60;
  1337. .radio-circle {
  1338. background-color: #D81B60;
  1339. border-color: #D81B60;
  1340. }
  1341. }
  1342. }
  1343. }
  1344. /* 跟进进度标签 */
  1345. .status-tags-row {
  1346. display: flex;
  1347. flex-wrap: wrap;
  1348. gap: 16rpx;
  1349. .status-tag {
  1350. min-width: 140rpx;
  1351. text-align: center;
  1352. padding: 14rpx 20rpx;
  1353. border-radius: 20rpx;
  1354. border: 2rpx solid #E1BEE7;
  1355. font-size: 26rpx;
  1356. color: #9C27B0;
  1357. background-color: #FAF5FF;
  1358. box-sizing: border-box;
  1359. &.active {
  1360. background-color: #F8BBD0;
  1361. border-color: #F06292;
  1362. color: #FFFFFF;
  1363. }
  1364. }
  1365. }
  1366. /* 邀约状态/时间 等输入类展示 */
  1367. .input-like {
  1368. width: 100%;
  1369. padding: 18rpx 22rpx;
  1370. border-radius: 16rpx;
  1371. background-color: #FAF5FF;
  1372. font-size: 26rpx;
  1373. color: #666;
  1374. box-sizing: border-box;
  1375. }
  1376. .picker-inner {
  1377. font-size: 26rpx;
  1378. color: #666;
  1379. }
  1380. /* 备注输入框 */
  1381. .textarea {
  1382. width: 100%;
  1383. min-height: 160rpx;
  1384. padding: 18rpx 22rpx;
  1385. border-radius: 16rpx;
  1386. background-color: #FAF5FF;
  1387. font-size: 26rpx;
  1388. color: #333;
  1389. box-sizing: border-box;
  1390. }
  1391. /* 保存按钮 */
  1392. .follow-submit-btn {
  1393. &.disabled {
  1394. background: #CCCCCC !important;
  1395. color: #999999 !important;
  1396. opacity: 0.6;
  1397. cursor: not-allowed;
  1398. }
  1399. margin-top: 10rpx;
  1400. width: 100%;
  1401. padding: 22rpx 0;
  1402. text-align: center;
  1403. border-radius: 26rpx;
  1404. background: linear-gradient(135deg, #F48FB1 0%, #EC407A 100%);
  1405. color: #FFFFFF;
  1406. font-size: 30rpx;
  1407. font-weight: bold;
  1408. }
  1409. </style>