report-list.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. <template>
  2. <view class="list-container">
  3. <!-- 成员过滤 -->
  4. <view class="member-filter">
  5. <scroll-view class="filter-scroll" scroll-x="true" :show-scrollbar="false">
  6. <view class="filter-chips">
  7. <view class="chip" :class="{ 'chip-active': !selectedMemberId }" @click="selectAllMembers">
  8. <text class="chip-text">全部</text>
  9. </view>
  10. <view
  11. class="chip"
  12. v-for="item in members"
  13. :key="item.id"
  14. :class="{ 'chip-active': selectedMemberId === item._filterId }"
  15. @click="selectMember(item)"
  16. >
  17. <text class="chip-text">{{ item.nickname || '成员' }}</text>
  18. </view>
  19. </view>
  20. </scroll-view>
  21. </view>
  22. <!-- 跨家庭报告转移申请 -->
  23. <view class="transfer-wrap" v-if="transferIncoming.length > 0 || transferOutgoing.length > 0">
  24. <text class="transfer-section-title">报告转移申请</text>
  25. <!-- 待本家庭处理:对方家庭申请将本家庭报告转入其家庭 -->
  26. <view class="transfer-card" v-for="item in transferIncoming" :key="item.id">
  27. <view class="transfer-info">
  28. <text class="transfer-person">{{ item.personName || '健康报告' }}</text>
  29. <text class="transfer-meta">报告日期:{{ formatDate(item.reportDate) || '--' }}</text>
  30. <text class="transfer-meta">申请时间:{{ formatDateTime(item.createdAt) }}</text>
  31. </view>
  32. <view class="transfer-actions">
  33. <view class="transfer-btn transfer-deny" @click.stop="denyTransfer(item)">拒绝</view>
  34. <view class="transfer-btn transfer-approve" @click.stop="approveTransfer(item)">同意</view>
  35. </view>
  36. </view>
  37. <!-- 本家庭发起的申请 -->
  38. <view class="transfer-card" v-for="item in transferOutgoing" :key="item.id">
  39. <view class="transfer-info">
  40. <text class="transfer-person">{{ item.personName || '健康报告' }}</text>
  41. <text class="transfer-meta">申请时间:{{ formatDateTime(item.createdAt) }}</text>
  42. <text class="transfer-status" :class="'status-' + item.status">{{ transferStatusText(item.status) }}</text>
  43. </view>
  44. </view>
  45. </view>
  46. <!-- 采集中草稿 -->
  47. <view class="collecting-wrap" v-if="collectingReports.length > 0">
  48. <view class="collecting-card" v-for="item in collectingReports" :key="item.draftId">
  49. <view class="collecting-icon">
  50. <view class="spinner"></view>
  51. </view>
  52. <view class="collecting-info">
  53. <text class="collecting-title">数据采集中</text>
  54. <text class="collecting-file">{{ item.originalFilename || '健康报告' }}</text>
  55. </view>
  56. <text class="collecting-status">解析完成后自动入库</text>
  57. </view>
  58. </view>
  59. <!-- 加载状态 -->
  60. <view class="loading-wrap" v-if="loading">
  61. <text class="loading-text">加载中...</text>
  62. </view>
  63. <!-- 报告列表 -->
  64. <view class="report-list" v-else-if="reports.length > 0">
  65. <view class="swipe-row" v-for="item in reports" :key="item.id">
  66. <!-- 右侧操作按钮(左滑后露出:删除 + 调整归属) -->
  67. <view class="swipe-actions">
  68. <view class="swipe-delete" @click.stop="confirmDelete(item)">删除</view>
  69. <view class="swipe-adjust" @click.stop="adjustBinding(item)">调整归属</view>
  70. </view>
  71. <!-- 卡片内容(可左滑) -->
  72. <view class="swipe-content"
  73. :class="{ 'swipe-moving': curId === item.id }"
  74. :style="'transform: translateX(' + (curId === item.id ? moveOffset : (openMap[item.id] ? -160 : 0)) + 'px)'"
  75. @touchstart="onTouchStart($event, item)"
  76. @touchmove="onTouchMove"
  77. @touchend="onTouchEnd"
  78. @click="onRowClick(item)">
  79. <view class="report-card">
  80. <view class="report-card-left">
  81. <view class="report-type-badge" :class="'badge-' + (item.reportType === 'gut_flora' ? 'gut' : item.reportType === 'tongue' ? 'tongue' : 'exam')">
  82. <text>{{ item.reportType === 'gut_flora' ? '肠道检测' : item.reportType === 'tongue' ? '舌诊' : '体检报告' }}</text>
  83. </view>
  84. <text class="report-person" v-if="item.personName">{{ item.personName }}</text>
  85. <text class="report-date">报告日期:{{ formatDate(item.reportDate) }}</text>
  86. <text class="report-upload-date">上传日期:{{ formatDateTime(item.createdAt) }}</text>
  87. </view>
  88. <view class="report-card-right">
  89. <text class="report-score">{{ item.overallScore || '--' }}<text class="score-unit">分</text></text>
  90. <text class="report-arrow">›</text>
  91. </view>
  92. </view>
  93. </view>
  94. </view>
  95. </view>
  96. <!-- 空状态 -->
  97. <view class="empty-wrap" v-else>
  98. <text class="empty-icon">📋</text>
  99. <text class="empty-title">暂无健康报告</text>
  100. <text class="empty-desc">上传体检报告或肠道检测报告,获取 解读</text>
  101. <view class="empty-btn" @click="goToUpload">上传报告</view>
  102. </view>
  103. <!-- 成员调整面板 -->
  104. <view class="adjust-panel" v-if="showAdjustPanel">
  105. <view class="panel-header">
  106. <text class="panel-title">调整报告归属</text>
  107. <view class="panel-close" @click="closeAdjustPanel">×</view>
  108. </view>
  109. <view class="panel-content">
  110. <!-- 取消归属选项 -->
  111. <view class="cancel-bind-option" @click="cancelBinding" v-if="adjustCurrentMemberId">
  112. <text class="cancel-bind-text">取消归属</text>
  113. <text class="cancel-bind-arrow">×</text>
  114. </view>
  115. <view class="member-filter">
  116. <scroll-view class="filter-scroll" scroll-x="true" :show-scrollbar="false">
  117. <view class="filter-chips">
  118. <view class="chip" :class="{ 'chip-active': !selectedAdjustMemberId }" @click="selectAllAdjustMembers">
  119. <text class="chip-text">全部</text>
  120. </view>
  121. <view
  122. class="chip"
  123. v-for="member in adjustMembers"
  124. :key="member.id"
  125. :class="{ 'chip-active': selectedAdjustMemberId === member._filterId }"
  126. @click="selectMemberAdjust(member)"
  127. >
  128. <text class="chip-text">{{ member.nickname || '成员' }}</text>
  129. </view>
  130. </view>
  131. </scroll-view>
  132. </view>
  133. <!-- 家庭成员列表 -->
  134. <view class="member-list" v-if="adjustMembers.length > 0">
  135. <view
  136. class="member-item"
  137. v-for="(member, mIdx) in adjustMembers"
  138. :key="mIdx"
  139. @click="selectMemberAdjust(member)"
  140. >
  141. <text class="member-avatar">{{ (member.nickname || '?').substring(0, 1) }}</text>
  142. <text class="member-name">{{ member.nickname || '未知' }}</text>
  143. </view>
  144. </view>
  145. </view>
  146. </view>
  147. <!-- 底部占位 -->
  148. <view class="bottom-spacer"></view>
  149. </view>
  150. </template>
  151. <script>
  152. import { parseDate } from '../../utils/format.js'
  153. import { getReportList, deleteReport, getFamilyMemberList, getCollectingReports, reassignReport, getReportTransferList, approveReportTransfer, denyReportTransfer } from '../../utils/api.js'
  154. export default {
  155. data() {
  156. return {
  157. reports: [],
  158. members: [],
  159. selectedMemberId: null,
  160. loading: false,
  161. memberId: null,
  162. // 异步采集中的草稿
  163. collectingReports: [],
  164. collectTimer: null,
  165. // 左滑删除
  166. openMap: {},
  167. curId: null,
  168. moveOffset: 0,
  169. startX: 0,
  170. startY: 0,
  171. touchDidMove: false,
  172. baseOffset: 0,
  173. // 调整归属面板状态
  174. showAdjustPanel: false,
  175. adjustReportId: null,
  176. adjustCurrentMemberId: null,
  177. // 调整面板成员数据
  178. adjustMembers: [],
  179. selectedAdjustMemberId: null,
  180. candidates: [],
  181. showAutoCreateForm: false,
  182. newMemberName: '',
  183. newMemberGender: 'male',
  184. // 跨家庭报告转移申请
  185. transferIncoming: [],
  186. transferOutgoing: []
  187. }
  188. },
  189. onLoad(options) {
  190. // 默认选中本人(登录用户):
  191. // - 无显式 memberId 时用登录 userId 过滤,后端 getMemberReports 兼容 users.id / family_members.id;
  192. // - 不再优先 currentChildId/currentMemberId——切换视角进入时默认也是本人,点成员 chip 才切换
  193. if (options.memberId) {
  194. this.memberId = parseInt(options.memberId, 10)
  195. } else {
  196. var loginUserId = uni.getStorageSync('userId') || ''
  197. this.memberId = loginUserId ? parseInt(loginUserId, 10) : null
  198. // 成员列表返回后把 chip 高亮从 users.id 归一到本人的 family_members.id(_filterId)
  199. this._selfUserId = loginUserId
  200. }
  201. this.selectedMemberId = this.memberId
  202. this.loadMembers()
  203. this.loadData()
  204. this.loadTransferRequests()
  205. this.startCollectPolling()
  206. },
  207. onShow() {
  208. // 从报告上传页返回时刷新列表,展示后台异步采集完成/失败的报告
  209. if (this._pageShown) {
  210. this.loadData()
  211. this.loadCollectingReports()
  212. this.loadTransferRequests()
  213. }
  214. this._pageShown = true
  215. },
  216. onUnload() {
  217. if (this.collectTimer) {
  218. clearInterval(this.collectTimer)
  219. this.collectTimer = null
  220. }
  221. },
  222. methods: {
  223. adjustBinding(item) {
  224. var self = this
  225. // 显示成员选择面板
  226. self.showAdjustPanel = true
  227. self.adjustReportId = item.id
  228. // 记录当前归属成员ID(用于判断是否显示"取消归属"选项)
  229. self.adjustCurrentMemberId = item.subjectId
  230. // 使用已加载的成员列表
  231. self.adjustMembers = self.members
  232. self.selectedAdjustMemberId = null
  233. },
  234. cancelBinding() {
  235. var self = this
  236. self.showAdjustPanel = false
  237. // 调用后端接口取消归属(传入 null)
  238. reassignReport(self.adjustReportId, null).then(function(res) {
  239. if (res.code === 200) {
  240. uni.showToast({ title: '已取消归属', icon: 'success' })
  241. self.loadData()
  242. } else {
  243. uni.showToast({ title: res.message || '操作失败', icon: 'none' })
  244. }
  245. }).catch(function(e) {
  246. console.log('取消归属失败', e)
  247. uni.showToast({ title: '操作失败', icon: 'none' })
  248. })
  249. },
  250. closeAdjustPanel() {
  251. this.showAdjustPanel = false
  252. },
  253. selectAdjustMember(member) {
  254. var self = this
  255. self.showAdjustPanel = false
  256. // 调用后端接口修改归属
  257. reassignReport(self.adjustReportId, member.id).then(function(res) {
  258. if (res.code === 200) {
  259. uni.showToast({ title: '归属修改成功', icon: 'success' })
  260. self.loadData()
  261. } else {
  262. uni.showToast({ title: res.message || '修改失败', icon: 'none' })
  263. }
  264. }).catch(function(e) {
  265. console.log('修改归属失败', e)
  266. uni.showToast({ title: '修改失败', icon: 'none' })
  267. })
  268. },
  269. startCollectPolling() {
  270. var self = this
  271. if (self.collectTimer) {
  272. clearInterval(self.collectTimer)
  273. }
  274. self.loadCollectingReports()
  275. self.collectTimer = setInterval(function() {
  276. self.loadCollectingReports()
  277. }, 6000)
  278. },
  279. loadCollectingReports() {
  280. var self = this
  281. var prevCount = self.collectingReports.length
  282. getCollectingReports().then(function(res) {
  283. if (res.code === 200) {
  284. var list = res.data || []
  285. self.collectingReports = list
  286. // 采集卡片消失后刷新列表,让刚入库的报告立刻出现
  287. if (prevCount > 0 && list.length < prevCount) {
  288. self.loadData()
  289. }
  290. }
  291. }).catch(function() {
  292. // 采集进度轮询失败静默,下一轮重试
  293. })
  294. },
  295. loadMembers() {
  296. var self = this
  297. getFamilyMemberList({}).then(function(res) {
  298. if (res.code === 200 && Array.isArray(res.data)) {
  299. // _filterId 预计算,供 :class 内联比较使用(小程序 :class 禁止方法调用)
  300. var list = []
  301. for (var i = 0; i < res.data.length; i++) {
  302. var m = res.data[i]
  303. list.push({
  304. id: m.id,
  305. userId: m.userId,
  306. nickname: m.nickname,
  307. avatar: m.avatar,
  308. _filterId: m.id || m.userId
  309. })
  310. }
  311. self.members = list
  312. // 默认选中本人时,把 chip 高亮从 users.id 归一到本人的 family_members.id(_filterId):
  313. // 两个 id 后端返回同一份报告,数据已在 onLoad 拉取过,这里只修高亮、不重复请求
  314. if (self._selfUserId && self.selectedMemberId != null &&
  315. Number(self.selectedMemberId) === Number(self._selfUserId)) {
  316. for (var j = 0; j < list.length; j++) {
  317. if (list[j].userId != null && Number(list[j].userId) === Number(self._selfUserId)) {
  318. self.selectedMemberId = list[j]._filterId
  319. break
  320. }
  321. }
  322. }
  323. }
  324. }).catch(function(e) {
  325. console.log('获取家庭成员失败', e)
  326. })
  327. },
  328. selectAllMembers() {
  329. if (this.selectedMemberId === null) return
  330. this.selectedMemberId = null
  331. this.loadData()
  332. },
  333. selectAllAdjustMembers() {
  334. if (this.selectedAdjustMemberId === null) return
  335. this.selectedAdjustMemberId = null
  336. },
  337. selectMember(item) {
  338. var filterId = item._filterId
  339. if (this.selectedMemberId === filterId) return
  340. this.selectedMemberId = filterId
  341. this.loadData()
  342. },
  343. selectMemberAdjust(member) {
  344. var self = this
  345. self.showAdjustPanel = false
  346. // 调用后端接口修改归属
  347. reassignReport(self.adjustReportId, member.id).then(function(res) {
  348. if (res.code === 200) {
  349. uni.showToast({ title: '归属修改成功', icon: 'success' })
  350. self.loadData()
  351. } else {
  352. uni.showToast({ title: res.message || '修改失败', icon: 'none' })
  353. }
  354. }).catch(function(e) {
  355. console.log('修改归属失败', e)
  356. uni.showToast({ title: '修改失败', icon: 'none' })
  357. })
  358. },
  359. loadData() {
  360. var self = this
  361. self.loading = true
  362. getReportList(self.selectedMemberId).then(function(res) {
  363. self.reports = res.data || []
  364. }).catch(function(e) {
  365. console.log('获取报告列表失败', e)
  366. uni.showToast({ title: '加载失败', icon: 'none' })
  367. }).finally(function() {
  368. self.loading = false
  369. })
  370. },
  371. // 跨家庭报告转移申请列表(incoming=待本家庭处理 / outgoing=本家庭发起)
  372. loadTransferRequests() {
  373. var self = this
  374. getReportTransferList().then(function(res) {
  375. if (res && res.code === 200) {
  376. var data = res.data || {}
  377. self.transferIncoming = data.incoming instanceof Array ? data.incoming : []
  378. self.transferOutgoing = data.outgoing instanceof Array ? data.outgoing : []
  379. }
  380. }).catch(function(e) {
  381. console.log('获取转移申请失败', e)
  382. })
  383. },
  384. transferStatusText(status) {
  385. if (status === 'approved') return '对方已同意,报告已转入'
  386. if (status === 'denied') return '对方已拒绝'
  387. return '等待对方同意'
  388. },
  389. approveTransfer(item) {
  390. var self = this
  391. uni.showModal({
  392. title: '同意转移',
  393. content: '同意后该报告将转入对方家庭,您的家庭将不再显示此报告。是否同意?',
  394. confirmColor: '#F56C6C',
  395. success: function(res) {
  396. if (!res.confirm) return
  397. approveReportTransfer(item.id).then(function(r) {
  398. if (r && r.code === 200) {
  399. uni.showToast({ title: '已同意,报告已转入对方家庭', icon: 'none' })
  400. self.loadTransferRequests()
  401. self.loadData()
  402. } else {
  403. uni.showToast({ title: (r && r.message) || '操作失败', icon: 'none' })
  404. }
  405. }).catch(function() {
  406. uni.showToast({ title: '操作失败', icon: 'none' })
  407. })
  408. }
  409. })
  410. },
  411. denyTransfer(item) {
  412. var self = this
  413. uni.showModal({
  414. title: '拒绝转移',
  415. content: '拒绝后该报告继续留在您的家庭。',
  416. success: function(res) {
  417. if (!res.confirm) return
  418. denyReportTransfer(item.id, '').then(function(r) {
  419. if (r && r.code === 200) {
  420. uni.showToast({ title: '已拒绝', icon: 'none' })
  421. self.loadTransferRequests()
  422. } else {
  423. uni.showToast({ title: (r && r.message) || '操作失败', icon: 'none' })
  424. }
  425. }).catch(function() {
  426. uni.showToast({ title: '操作失败', icon: 'none' })
  427. })
  428. }
  429. })
  430. },
  431. genderText(gender) {
  432. if (gender === 'male') return '男'
  433. if (gender === 'female') return '女'
  434. return ''
  435. },
  436. toggleAutoCreate() {
  437. var self = this
  438. self.showAutoCreateForm = !self.showAutoCreateForm
  439. },
  440. confirmCreateMember() {
  441. var self = this
  442. // 简单的创建逻辑 - 这里需要后端支持创建成员
  443. uni.showToast({ title: '创建功能待开发', icon: 'none' })
  444. self.showAutoCreateForm = false
  445. },
  446. goToDetail(reportId, reportType) {
  447. var url
  448. if (reportType === 'gut_flora') {
  449. url = '/pages/health/gut-flora-detail?reportId=' + reportId
  450. } else if (reportType === 'dan') {
  451. url = '/pages/dan-assessment/report-result?reportId=' + reportId
  452. } else {
  453. url = '/pages/health/report-detail?reportType=' + (reportType || 'gut_flora') + '&reportId=' + reportId
  454. }
  455. uni.navigateTo({ url: url })
  456. },
  457. onRowClick(item) {
  458. // 刚滑动过,忽略本次点击
  459. if (this.touchDidMove) {
  460. this.touchDidMove = false
  461. return
  462. }
  463. // 行处于打开态,点击先收起
  464. if (this.openMap[item.id]) {
  465. this.$set(this.openMap, item.id, false)
  466. return
  467. }
  468. this.goToDetail(item.id, item.reportType)
  469. },
  470. onTouchStart(e, item) {
  471. // 先收起其它已打开行
  472. if (this.curId !== null && this.curId !== item.id && this.openMap[this.curId]) {
  473. this.$set(this.openMap, this.curId, false)
  474. }
  475. var touch = e.touches[0]
  476. this.curId = item.id
  477. this.startX = touch.clientX
  478. this.startY = touch.clientY
  479. this.touchDidMove = false
  480. this.baseOffset = this.openMap[item.id] ? -160 : 0
  481. this.moveOffset = this.baseOffset
  482. },
  483. onTouchMove(e) {
  484. if (this.curId === null) return
  485. var touch = e.touches[0]
  486. var dx = touch.clientX - this.startX
  487. var dy = touch.clientY - this.startY
  488. // 横向滑动(水平分量大于垂直分量)
  489. if (Math.abs(dx) > Math.abs(dy)) {
  490. this.touchDidMove = Math.abs(dx) > 10
  491. var offset = this.baseOffset + dx
  492. if (offset < -160) offset = -160
  493. if (offset > 0) offset = 0
  494. this.moveOffset = offset
  495. }
  496. },
  497. onTouchEnd() {
  498. if (this.curId === null) return
  499. // 位移超过阈值则打开,否则收起
  500. var shouldOpen = this.moveOffset < -20
  501. this.$set(this.openMap, this.curId, shouldOpen)
  502. this.moveOffset = shouldOpen ? -160 : 0
  503. this.curId = null
  504. },
  505. goToUpload() {
  506. uni.navigateTo({ url: '/pages/health/report-upload' })
  507. },
  508. confirmDelete(item) {
  509. var self = this
  510. uni.showModal({
  511. title: '删除报告',
  512. content: '确定删除该健康报告吗?删除后不可恢复。',
  513. confirmColor: '#F56C6C',
  514. success: function(res) {
  515. if (!res.confirm) return
  516. self.doDelete(item)
  517. }
  518. })
  519. },
  520. doDelete(item) {
  521. var self = this
  522. deleteReport(item.id).then(function(res) {
  523. if (res.code === 200) {
  524. uni.showToast({ title: '已删除', icon: 'success' })
  525. self.loadData()
  526. } else {
  527. uni.showToast({ title: res.message || '删除失败', icon: 'none' })
  528. }
  529. }).catch(function(e) {
  530. console.log('删除报告失败', e)
  531. uni.showToast({ title: '删除失败', icon: 'none' })
  532. })
  533. },
  534. formatDate(dateStr) {
  535. var d = parseDate(dateStr)
  536. if (!d) return ''
  537. var y = d.getFullYear()
  538. var m = ('0' + (d.getMonth() + 1)).slice(-2)
  539. var day = ('0' + d.getDate()).slice(-2)
  540. return y + '-' + m + '-' + day
  541. },
  542. formatDateTime(dateStr) {
  543. var d = parseDate(dateStr)
  544. if (!d) return ''
  545. return this.formatDate(dateStr) + ' ' +
  546. ('0' + d.getHours()).slice(-2) + ':' +
  547. ('0' + d.getMinutes()).slice(-2) + ':' +
  548. ('0' + d.getSeconds()).slice(-2)
  549. },
  550. },
  551. }
  552. </script>
  553. <style scoped>
  554. .list-container {
  555. min-height: 100vh;
  556. background: #F5F7FA;
  557. }
  558. /* ===== 成员过滤 ===== */
  559. .member-filter {
  560. background: #F5F7FA;
  561. padding: 20rpx 30rpx 4rpx;
  562. }
  563. .filter-scroll {
  564. width: 100%;
  565. white-space: nowrap;
  566. }
  567. .filter-chips {
  568. display: flex;
  569. flex-direction: row;
  570. }
  571. .chip {
  572. flex-shrink: 0;
  573. background: #fff;
  574. border: 1rpx solid #E2E8F0;
  575. border-radius: 28rpx;
  576. padding: 10rpx 30rpx;
  577. margin-right: 16rpx;
  578. }
  579. .chip-active {
  580. background: #5B9BD5;
  581. border-color: #5B9BD5;
  582. }
  583. .chip-text {
  584. font-size: 26rpx;
  585. color: #64748B;
  586. }
  587. .chip-active .chip-text {
  588. color: #fff;
  589. }
  590. /* ===== 跨家庭报告转移申请 ===== */
  591. .transfer-wrap {
  592. margin: 20rpx 30rpx 0;
  593. }
  594. .transfer-section-title {
  595. display: block;
  596. font-size: 28rpx;
  597. font-weight: 600;
  598. color: #1E293B;
  599. margin-bottom: 12rpx;
  600. }
  601. .transfer-card {
  602. display: flex;
  603. flex-direction: row;
  604. align-items: center;
  605. justify-content: space-between;
  606. background: #fff;
  607. border: 1rpx solid #E2E8F0;
  608. border-radius: 16rpx;
  609. padding: 20rpx 24rpx;
  610. margin-bottom: 16rpx;
  611. }
  612. .transfer-info {
  613. flex: 1;
  614. display: flex;
  615. flex-direction: column;
  616. }
  617. .transfer-person {
  618. font-size: 28rpx;
  619. font-weight: 600;
  620. color: #1E293B;
  621. }
  622. .transfer-meta {
  623. font-size: 24rpx;
  624. color: #94A3B8;
  625. margin-top: 6rpx;
  626. }
  627. .transfer-actions {
  628. display: flex;
  629. flex-direction: row;
  630. flex-shrink: 0;
  631. }
  632. .transfer-btn {
  633. font-size: 24rpx;
  634. padding: 10rpx 28rpx;
  635. border-radius: 24rpx;
  636. margin-left: 12rpx;
  637. }
  638. .transfer-deny {
  639. background: #F1F5F9;
  640. color: #64748B;
  641. }
  642. .transfer-approve {
  643. background: #5B9BD5;
  644. color: #fff;
  645. }
  646. .transfer-status {
  647. font-size: 24rpx;
  648. margin-top: 6rpx;
  649. }
  650. .status-pending {
  651. color: #F0B429;
  652. }
  653. .status-approved {
  654. color: #34C759;
  655. }
  656. .status-denied {
  657. color: #94A3B8;
  658. }
  659. /* ===== 加载状态 ===== */
  660. .loading-wrap {
  661. display: flex;
  662. justify-content: center;
  663. padding: 80rpx 0;
  664. }
  665. .loading-text {
  666. font-size: 28rpx;
  667. color: #999;
  668. }
  669. /* ===== 采集中草稿 ===== */
  670. .collecting-wrap {
  671. margin: 20rpx 30rpx 0;
  672. }
  673. .collecting-card {
  674. display: flex;
  675. flex-direction: row;
  676. align-items: center;
  677. background: #FFF8E6;
  678. border: 1rpx solid #F5D78E;
  679. border-radius: 16rpx;
  680. padding: 20rpx 24rpx;
  681. margin-bottom: 16rpx;
  682. }
  683. .collecting-icon {
  684. width: 56rpx;
  685. height: 56rpx;
  686. border-radius: 50%;
  687. background: #F0B429;
  688. display: flex;
  689. align-items: center;
  690. justify-content: center;
  691. margin-right: 20rpx;
  692. }
  693. .spinner {
  694. width: 28rpx;
  695. height: 28rpx;
  696. border: 4rpx solid rgba(255,255,255,0.4);
  697. border-top-color: #fff;
  698. border-radius: 50%;
  699. animation: collecting-spin 1s linear infinite;
  700. }
  701. @keyframes collecting-spin {
  702. 0% { transform: rotate(0deg); }
  703. 100% { transform: rotate(360deg); }
  704. }
  705. .collecting-info {
  706. flex: 1;
  707. display: flex;
  708. flex-direction: column;
  709. min-width: 0;
  710. }
  711. .collecting-title {
  712. font-size: 28rpx;
  713. font-weight: 600;
  714. color: #7A5C00;
  715. }
  716. .collecting-file {
  717. font-size: 24rpx;
  718. color: #A08A45;
  719. margin-top: 4rpx;
  720. overflow: hidden;
  721. text-overflow: ellipsis;
  722. white-space: nowrap;
  723. }
  724. .collecting-status {
  725. font-size: 22rpx;
  726. color: #C0A75A;
  727. flex-shrink: 0;
  728. }
  729. /* ===== 报告列表 ===== */
  730. .report-list {
  731. margin: 20rpx 30rpx;
  732. }
  733. .swipe-row {
  734. position: relative;
  735. overflow: hidden;
  736. border-radius: 16rpx;
  737. margin-bottom: 16rpx;
  738. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  739. }
  740. .swipe-actions {
  741. position: absolute;
  742. top: 0;
  743. right: 0;
  744. height: 100%;
  745. display: flex;
  746. flex-direction: row;
  747. }
  748. .swipe-delete {
  749. width: 80px;
  750. height: 100%;
  751. background: #F56C6C;
  752. color: #fff;
  753. font-size: 28rpx;
  754. display: flex;
  755. align-items: center;
  756. justify-content: center;
  757. }
  758. .swipe-adjust {
  759. width: 80px;
  760. height: 100%;
  761. background: #5B9BD5;
  762. color: #fff;
  763. font-size: 28rpx;
  764. display: flex;
  765. align-items: center;
  766. justify-content: center;
  767. }
  768. .swipe-content {
  769. position: relative;
  770. z-index: 2;
  771. background: #fff;
  772. transition: transform 0.25s ease;
  773. }
  774. .swipe-moving {
  775. transition: none;
  776. }
  777. .report-card {
  778. border-radius: 0;
  779. padding: 28rpx 24rpx;
  780. display: flex;
  781. flex-direction: row;
  782. align-items: center;
  783. justify-content: space-between;
  784. }
  785. .report-card:active {
  786. opacity: 0.8;
  787. }
  788. .report-card-left {
  789. display: flex;
  790. flex-direction: column;
  791. gap: 2rpx;
  792. flex: 1;
  793. min-width: 0;
  794. }
  795. .report-type-badge {
  796. display: inline-block;
  797. font-size: 22rpx;
  798. padding: 4rpx 18rpx;
  799. border-radius: 20rpx;
  800. align-self: flex-start;
  801. }
  802. .badge-exam {
  803. background: #E3F2FD;
  804. color: #1565C0;
  805. }
  806. .badge-gut {
  807. background: #E8F5E9;
  808. color: #2E7D32;
  809. }
  810. .badge-tongue {
  811. background: #FFF3E0;
  812. color: #E65100;
  813. }
  814. .report-person {
  815. font-size: 26rpx;
  816. color: #333;
  817. font-weight: 500;
  818. margin-top: 4rpx;
  819. }
  820. .report-date {
  821. font-size: 22rpx;
  822. color: #888;
  823. margin-top: 4rpx;
  824. }
  825. .report-upload-date {
  826. font-size: 22rpx;
  827. color: #AAA;
  828. margin-top: 2rpx;
  829. }
  830. .report-card-right {
  831. display: flex;
  832. flex-direction: row;
  833. align-items: center;
  834. gap: 12rpx;
  835. }
  836. .report-score {
  837. font-size: 36rpx;
  838. font-weight: bold;
  839. color: #333;
  840. }
  841. .score-unit {
  842. font-size: 22rpx;
  843. color: #999;
  844. font-weight: normal;
  845. }
  846. .report-arrow {
  847. font-size: 36rpx;
  848. color: #CCC;
  849. }
  850. /* ===== 空状态 ===== */
  851. .empty-wrap {
  852. display: flex;
  853. flex-direction: column;
  854. align-items: center;
  855. padding: 120rpx 60rpx;
  856. }
  857. .empty-icon {
  858. font-size: 80rpx;
  859. margin-bottom: 24rpx;
  860. }
  861. .empty-title {
  862. font-size: 32rpx;
  863. font-weight: bold;
  864. color: #333;
  865. margin-bottom: 12rpx;
  866. }
  867. .empty-desc {
  868. font-size: 26rpx;
  869. color: #999;
  870. text-align: center;
  871. margin-bottom: 40rpx;
  872. line-height: 1.5;
  873. }
  874. .empty-btn {
  875. background: #5B9BD5;
  876. color: #fff;
  877. font-size: 28rpx;
  878. padding: 20rpx 60rpx;
  879. border-radius: 40rpx;
  880. }
  881. .empty-btn:active {
  882. opacity: 0.8;
  883. }
  884. /* ===== 底部 ===== */
  885. .bottom-spacer {
  886. height: 60rpx;
  887. }
  888. /* ===== 调整归属面板 ===== */
  889. .adjust-panel {
  890. position: fixed;
  891. top: 0;
  892. left: 0;
  893. right: 0;
  894. bottom: 0;
  895. background: rgba(0, 0, 0, 0.5);
  896. z-index: 999;
  897. display: flex;
  898. align-items: flex-end;
  899. }
  900. .adjust-panel .panel-content {
  901. background: #fff;
  902. border-radius: 32rpx 32rpx 0 0;
  903. padding: 32rpx 30rpx;
  904. max-height: 70vh;
  905. overflow-y: auto;
  906. width: 100%;
  907. }
  908. .adjust-panel .panel-header {
  909. display: flex;
  910. align-items: center;
  911. justify-content: space-between;
  912. margin-bottom: 24rpx;
  913. }
  914. .adjust-panel .panel-title {
  915. font-size: 32rpx;
  916. font-weight: 600;
  917. color: #333;
  918. }
  919. .adjust-panel .panel-close {
  920. font-size: 48rpx;
  921. color: #999;
  922. line-height: 1;
  923. padding: 0 8rpx;
  924. }
  925. .adjust-panel .cancel-bind-option {
  926. display: flex;
  927. align-items: center;
  928. justify-content: space-between;
  929. padding: 24rpx 0;
  930. border-bottom: 1rpx solid #f0f0f0;
  931. margin-bottom: 16rpx;
  932. }
  933. .adjust-panel .cancel-bind-text {
  934. font-size: 28rpx;
  935. color: #F56C6C;
  936. }
  937. .adjust-panel .cancel-bind-arrow {
  938. font-size: 32rpx;
  939. color: #ccc;
  940. }
  941. .adjust-panel .member-list {
  942. margin-top: 16rpx;
  943. }
  944. .adjust-panel .member-item {
  945. display: flex;
  946. align-items: center;
  947. padding: 20rpx 0;
  948. border-bottom: 1rpx solid #f5f5f5;
  949. }
  950. .adjust-panel .member-item:last-child {
  951. border-bottom: none;
  952. }
  953. .adjust-panel .member-avatar {
  954. width: 64rpx;
  955. height: 64rpx;
  956. border-radius: 50%;
  957. background: #5B9BD5;
  958. color: #fff;
  959. display: flex;
  960. align-items: center;
  961. justify-content: center;
  962. font-size: 28rpx;
  963. font-weight: 500;
  964. margin-right: 20rpx;
  965. flex-shrink: 0;
  966. }
  967. .adjust-panel .member-name {
  968. font-size: 28rpx;
  969. color: #333;
  970. }
  971. </style>