approvalCenter.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. <template>
  2. <view class="container">
  3. <!-- 搜索+筛选栏 -->
  4. <view class="top-box">
  5. <view class="input-item">
  6. <image class="icon search-icon"
  7. src="https://ysrw-1395926010.cos.ap-chengdu.myqcloud.com/image/search.png" mode="widthFix"></image>
  8. <input v-model="searchKeywords" @input="handleSearchInput" placeholder="请输入发起人姓名、手机号"
  9. placeholder-class="placeholder" />
  10. </view>
  11. </view>
  12. <!-- 顶部选项卡 -->
  13. <view class="top-tabs">
  14. <view class="top-tab-item" :class="{ active: currentTopTab === index }" @click="switchTopTab(index)"
  15. v-for="(item, index) in topTabs" :key="index">
  16. {{ item.label }}·{{ item.badge }}
  17. </view>
  18. </view>
  19. <!-- 核心修改:新增子标签容器,作为popup-box的定位参考 -->
  20. <view class="sub-tabs-wrapper">
  21. <!-- 动态子标签 -->
  22. <view class="sub-tabs">
  23. <view class="sub-tab-item"
  24. :class="{ active: currentSubTab === 'all' || currentSubTab === 'taskCreate' }"
  25. @click="openSubTabPopup('all')">
  26. <text>{{ selectedSubTabLabels['all'] || '全部审核' }}</text>
  27. <image class="icon" src="https://ysrw-1395926010.cos.ap-chengdu.myqcloud.com/image/icon_expand.png"
  28. mode="widthFix"></image>
  29. </view>
  30. <view class="sub-tab-item"
  31. :class="{ active: currentSubTab === 'unfinished' || currentSubTab === 'finished' }"
  32. @click="openSubTabPopup('unfinished')">
  33. <text>{{ selectedSubTabLabels['unfinished'] || '最新发起' }}</text>
  34. <image class="icon" src="https://ysrw-1395926010.cos.ap-chengdu.myqcloud.com/image/icon_expand.png"
  35. mode="widthFix"></image>
  36. </view>
  37. </view>
  38. <!-- 子标签弹窗 -->
  39. <view class="sub-tab-popup-overlay" v-if="showSubTabPopup" @click="showSubTabPopup = false"></view>
  40. <view class="sub-tab-popup" v-if="showSubTabPopup">
  41. <view class="sub-tab-options">
  42. <!-- 全部审核选项 -->
  43. <template v-if="currentPopupType === 'all'">
  44. <view class="sub-tab-option" :class="{ active: selectedSubTabOptions['all'] === 'all' }"
  45. @click="selectSubTabOption('all', '全部审核')">
  46. <text>全部审核</text>
  47. </view>
  48. <view class="sub-tab-option" :class="{ active: selectedSubTabOptions['all'] === 'taskCreate' }"
  49. @click="selectSubTabOption('taskCreate', '任务创建')">
  50. <text>任务创建</text>
  51. </view>
  52. </template>
  53. <!-- 最新发起选项 -->
  54. <template v-else-if="currentPopupType === 'unfinished'">
  55. <view class="sub-tab-option"
  56. :class="{ active: selectedSubTabOptions['unfinished'] === 'unfinished' }"
  57. @click="selectSubTabOption('unfinished', '最新发起')">
  58. <text>最新发起</text>
  59. </view>
  60. <view class="sub-tab-option"
  61. :class="{ active: selectedSubTabOptions['unfinished'] === 'finished' }"
  62. @click="selectSubTabOption('finished', '最早发起')">
  63. <text>最早发起</text>
  64. </view>
  65. </template>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 列表内容 -->
  70. <scroll-view class="content" scroll-y refresher-enabled :refresher-triggered="refreshing"
  71. @refresherrefresh="onRefresh" @refresherrestore="onRestore">
  72. <view class="task-card" v-for="(item, index) in currentList" :key="index" @click="goDetails(item)">
  73. <!-- 任务/审核标题+状态 -->
  74. <view class="card-top">
  75. <text class="card-task-title">{{ getAuditNameDisplay(item.auditName) }}</text>
  76. <view class="status-tag" :class="item.statusClass">{{ item.statusText }}</view>
  77. </view>
  78. <view class="card-content">
  79. <!-- 时间信息 -->
  80. <view class="time-info">
  81. <view class="time-item">
  82. <text class="title">任务名称:</text>
  83. <text>{{ item.auditName || '未设置' }}</text>
  84. </view>
  85. <view class="time-item">
  86. <text class="title">任务类型:</text>
  87. <text>{{ getBusinessTypeLabel(item.businessType) }}</text>
  88. </view>
  89. </view>
  90. <!-- 操作按钮 -->
  91. <view class="operate-btn-group">
  92. <view class="share-btn" @click="handleShare(item)">
  93. <image class="share-icon"
  94. src="https://ysrw-1395926010.cos.ap-chengdu.myqcloud.com/image/icon_user.png"
  95. mode="widthFix"></image>
  96. <text>{{ item.initiatorName || '未命名' }}</text>
  97. </view>
  98. <view class="date">
  99. {{ formatTime(item.createTime) }}
  100. </view>
  101. </view>
  102. </view>
  103. </view>
  104. </scroll-view>
  105. <!-- 筛选弹窗 -->
  106. <view class="filter-popup" v-if="showFilter" @click="closeFilter">
  107. <view class="filter-content" @click.stop>
  108. <view class="filter-header">
  109. <view class="filter-title">筛选</view>
  110. <image class="filter-close-btn"
  111. src="https://ysrw-1395926010.cos.ap-chengdu.myqcloud.com/image/icon_close.png"
  112. @click="closeFilter" mode="widthFix"></image>
  113. </view>
  114. <view class="filter-form">
  115. <!-- 任务申请时间 -->
  116. <view class="filter-section">
  117. <view class="section-label">任务申请时间</view>
  118. <view class="time-range">
  119. <input class="time-input" placeholder="开始时间" v-model="filters.applyTimeStart" />
  120. <view class="time-separator">-</view>
  121. <input class="time-input" placeholder="结束时间" v-model="filters.applyTimeEnd" />
  122. </view>
  123. </view>
  124. <!-- 任务完成时间:改为判断index:1对应已办 -->
  125. <view class="filter-section" v-if="currentTopTab === 1">
  126. <view class="section-label">任务完成时间</view>
  127. <view class="time-range">
  128. <input class="time-input" placeholder="开始时间" v-model="filters.finishTimeStart" />
  129. <view class="time-separator">-</view>
  130. <input class="time-input" placeholder="结束时间" v-model="filters.finishTimeEnd" />
  131. </view>
  132. </view>
  133. <!-- 完成审核时间:改为判断index:1对应已办 -->
  134. <view class="filter-section" v-if="currentTopTab === 1">
  135. <view class="section-label">完成审核时间</view>
  136. <view class="time-range">
  137. <input class="time-input" placeholder="开始时间" v-model="filters.auditTimeStart" />
  138. <view class="time-separator">-</view>
  139. <input class="time-input" placeholder="结束时间" v-model="filters.auditTimeEnd" />
  140. </view>
  141. </view>
  142. <!-- 任务归属 -->
  143. <view class="filter-section">
  144. <view class="section-label">任务归属</view>
  145. <view class="btn-group">
  146. <view class="filter-btn" :class="{ active: filters.taskBelong === 'my' }"
  147. @click="filters.taskBelong = 'my'">我的任务</view>
  148. <view class="filter-btn" :class="{ active: filters.taskBelong === 'dept' }"
  149. @click="filters.taskBelong = 'dept'">部门任务</view>
  150. </view>
  151. </view>
  152. <!-- 完结状态:改为判断index:1对应已办 -->
  153. <view class="filter-section" v-if="currentTopTab === 1">
  154. <view class="section-label">完结状态</view>
  155. <view class="btn-group">
  156. <view class="filter-btn" :class="{ active: filters.finishStatus === 'unfinished' }"
  157. @click="filters.finishStatus = 'unfinished'">未完结</view>
  158. <view class="filter-btn" :class="{ active: filters.finishStatus === 'finished' }"
  159. @click="filters.finishStatus = 'finished'">已完结</view>
  160. </view>
  161. </view>
  162. <!-- 归属类型 -->
  163. <view class="filter-section">
  164. <view class="section-label">归属类型</view>
  165. <view class="btn-group">
  166. <view class="filter-btn" :class="{ active: filters.belongType === 'inner' }"
  167. @click="filters.belongType = 'inner'">院内</view>
  168. <view class="filter-btn" :class="{ active: filters.belongType === 'outer' }"
  169. @click="filters.belongType = 'outer'">院外</view>
  170. </view>
  171. </view>
  172. </view>
  173. <view class="filter-actions">
  174. <view class="reset-btn" @click="resetFilters">重置</view>
  175. <view class="confirm-btn" @click="confirmFilters">确定</view>
  176. </view>
  177. </view>
  178. </view>
  179. </view>
  180. </template>
  181. <script>
  182. import utils from '@/utils/common.js'
  183. import TabPopup from '@/components/tab-popup.vue'
  184. import {
  185. getPendingAuditList,
  186. searchCompanyUser
  187. } from '@/api/audit.js'
  188. export default {
  189. components: {
  190. TabPopup
  191. },
  192. data() {
  193. return {
  194. processTemplate: null,
  195. popShow: false,
  196. showFilter: false,
  197. selectedTabItem: '',
  198. searchKeywords: '',
  199. searchTimer: null,
  200. filters: {
  201. applyTimeStart: '',
  202. applyTimeEnd: '',
  203. finishTimeStart: '',
  204. finishTimeEnd: '',
  205. auditTimeStart: '',
  206. auditTimeEnd: '',
  207. taskBelong: 'my',
  208. finishStatus: '',
  209. belongType: ''
  210. },
  211. refreshing: false,
  212. showSubTabPopup: false,
  213. currentPopupType: 'all',
  214. // 为每种类型的弹窗分别存储选中的选项
  215. selectedSubTabOptions: {
  216. all: 'all',
  217. unfinished: 'unfinished'
  218. },
  219. selectedSubTabLabels: {
  220. all: '全部审核',
  221. unfinished: '最新发起'
  222. },
  223. // 顶部选项卡数组
  224. topTabs: [{
  225. label: '待办',
  226. value: 'todo',
  227. badge: 0
  228. },
  229. {
  230. label: '已办',
  231. value: 'done',
  232. badge: 0
  233. },
  234. {
  235. label: '我发起的',
  236. value: 'myInitiate',
  237. badge: 0
  238. }
  239. ],
  240. currentTopTab: 0,
  241. currentSubTab: 'all',
  242. // 子标签数组
  243. taskSubTabs: [{
  244. label: '全部审批',
  245. value: 'all'
  246. },
  247. {
  248. label: '最新发起',
  249. value: 'unfinished'
  250. }
  251. ],
  252. tabsList: ['全部审批', '任务创建'],
  253. tabsList2: ['最新发起', '最早发起'],
  254. // auditSubTabs: [{
  255. // label: '全部',
  256. // value: 'all'
  257. // },
  258. // {
  259. // label: '驳回',
  260. // value: 'rejected'
  261. // }
  262. // ],
  263. // 加载状态
  264. loading: {
  265. todo: false,
  266. done: false,
  267. myInitiate: false
  268. },
  269. // 列表数据
  270. todoList: [], // 待办列表(index=0)
  271. doneList: [], // 已办列表(index=1)
  272. myInitiateList: [], // 我发起的列表(index=2)
  273. // 初始化/加载状态控制,避免初次进入重复请求
  274. isInitializing: true,
  275. hasLoadedOnce: false
  276. }
  277. },
  278. computed: {
  279. currentSubTabsList() {
  280. return this.taskSubTabs;
  281. },
  282. currentList() {
  283. if (this.currentTopTab === 0) return this.todoList;
  284. if (this.currentTopTab === 1) return this.doneList;
  285. if (this.currentTopTab === 2) return this.myInitiateList;
  286. }
  287. },
  288. onLoad() {
  289. // 页面加载时的初始化操作
  290. this.loadData();
  291. utils.getDicts("FLOW_TEMPLATE").then(res => {
  292. this.processTemplate = res;
  293. console.log("流程模板", this.processTemplate)
  294. });
  295. },
  296. methods: {
  297. onRefresh() {
  298. this.refreshing = true;
  299. this.loadData().finally(() => {
  300. setTimeout(() => {
  301. this.refreshing = false;
  302. }, 500);
  303. });
  304. },
  305. onRestore() {
  306. this.refreshing = false;
  307. },
  308. openSubTabPopup(type) {
  309. this.currentPopupType = type;
  310. this.showSubTabPopup = true;
  311. },
  312. selectSubTabOption(value, label) {
  313. // 根据当前弹窗类型更新对应类型的选中选项
  314. this.selectedSubTabOptions[this.currentPopupType] = value;
  315. this.selectedSubTabLabels[this.currentPopupType] = label;
  316. this.currentSubTab = value;
  317. this.showSubTabPopup = false;
  318. // 这里可以添加根据选择的子标签过滤数据的逻辑
  319. },
  320. selectTabItem(item) {
  321. this.selectedTabItem = item;
  322. this.popShow = false;
  323. },
  324. goDetails(item) {
  325. console.log("跳转参数", item)
  326. uni.navigateTo({
  327. url: `/pages_task/approvalTaskDetail?taskId=${item.id}&businessType=${item.businessType}`
  328. })
  329. },
  330. switchTopTab(index) {
  331. this.currentTopTab = index;
  332. this.currentSubTab = 'all';
  333. // 重置所有子标签选项
  334. this.selectedSubTabOptions = {
  335. all: 'all',
  336. unfinished: 'unfinished'
  337. };
  338. this.selectedSubTabLabels = {
  339. all: '全部审核',
  340. unfinished: '最新发起'
  341. };
  342. this.popShow = false;
  343. this.showSubTabPopup = false;
  344. // 切换标签仅切换展示数据
  345. },
  346. closeFilter() {
  347. this.showFilter = false
  348. },
  349. resetFilters() {
  350. this.filters = {
  351. applyTimeStart: '',
  352. applyTimeEnd: '',
  353. finishTimeStart: '',
  354. finishTimeEnd: '',
  355. auditTimeStart: '',
  356. auditTimeEnd: '',
  357. taskBelong: 'my',
  358. finishStatus: '',
  359. belongType: ''
  360. }
  361. },
  362. confirmFilters() {
  363. this.showFilter = false
  364. console.log('筛选条件:', this.filters)
  365. // 筛选后重新加载数据
  366. this.loadData()
  367. },
  368. getAuditNameDisplay(auditName) {
  369. if (!auditName) return '未设置'
  370. switch (auditName) {
  371. case '完成任务审核':
  372. return '"完成任务"审核'
  373. case '定级审核':
  374. return '"讲者定级"审核'
  375. default:
  376. // 对于其他审核类型,在最开始和审核前都加双引号
  377. const prefix = auditName.replace('审核', '')
  378. return '"' + prefix + '"审核'
  379. }
  380. },
  381. handleShare(item) {
  382. uni.showToast({
  383. title: '分享功能待实现',
  384. icon: 'none'
  385. })
  386. },
  387. handleSearchInput() {
  388. // 初次进入时不触发重新加载,避免重复请求
  389. if (this.isInitializing) return;
  390. // 防抖处理,避免频繁请求
  391. if (this.searchTimer) {
  392. clearTimeout(this.searchTimer)
  393. }
  394. this.searchTimer = setTimeout(async () => {
  395. if (this.searchKeywords.trim()) {
  396. try {
  397. // 调用搜索发起人接口
  398. const res = await searchCompanyUser({
  399. keywords: this.searchKeywords.trim(),
  400. pageNum: 1,
  401. pageSize: 10
  402. })
  403. if (res.code === 200 && res.rows) {
  404. // 根据当前标签页更新对应列表
  405. if (this.currentTopTab === 0) {
  406. this.todoList = res.rows.map(item => ({
  407. auditName: item.auditName || '未知审核',
  408. statusText: '待审核',
  409. statusClass: 'status-createPending',
  410. id: item.id,
  411. status: item.status,
  412. auditType: item.auditType,
  413. businessId: item.businessId,
  414. businessType: item.businessType,
  415. createTime: item.createTime,
  416. initiatorName: item.initiatorName
  417. }))
  418. } else if (this.currentTopTab === 1) {
  419. this.doneList = res.rows.map(item => ({
  420. auditName: item.auditName || '未知审核',
  421. statusText: item.status === 1 ? '已通过' : '已驳回',
  422. statusClass: item.status === 1 ? 'status-finish' :
  423. 'status-rejected',
  424. id: item.id,
  425. status: item.status,
  426. auditType: item.auditType,
  427. businessId: item.businessId,
  428. businessType: item.businessType,
  429. createTime: item.createTime,
  430. initiatorName: item.initiatorName
  431. }))
  432. } else if (this.currentTopTab === 2) {
  433. this.myInitiateList = res.rows.map(item => ({
  434. auditName: item.auditName || '未知审核',
  435. statusText: '待审核',
  436. statusClass: 'status-createPending',
  437. id: item.id,
  438. status: item.status,
  439. auditType: item.auditType,
  440. businessId: item.businessId,
  441. businessType: item.businessType,
  442. createTime: item.createTime,
  443. initiatorName: item.initiatorName
  444. }))
  445. }
  446. } else {
  447. // 搜索结果为空,清空对应列表
  448. if (this.currentTopTab === 0) {
  449. this.todoList = []
  450. } else if (this.currentTopTab === 1) {
  451. this.doneList = []
  452. } else if (this.currentTopTab === 2) {
  453. this.myInitiateList = []
  454. }
  455. }
  456. } catch (e) {
  457. console.error('搜索失败', e)
  458. uni.showToast({
  459. title: '搜索失败',
  460. icon: 'none'
  461. })
  462. }
  463. } else {
  464. // 搜索框为空,重新加载默认数据
  465. if (this.hasLoadedOnce) {
  466. this.loadData()
  467. }
  468. }
  469. }, 300)
  470. },
  471. // 加载审批数据
  472. async loadData() {
  473. try {
  474. const userInfoStr = uni.getStorageSync('userInfo')
  475. const userInfo = userInfoStr ? (typeof userInfoStr === 'string' ? JSON.parse(userInfoStr) :
  476. userInfoStr) : {}
  477. // 一次请求获取全部列表与badge
  478. const params = {
  479. initiatorName: userInfo.nickName || '',
  480. initiatorPhone: userInfo.phone || '',
  481. userId: userInfo.userId || '',
  482. companyId: userInfo.companyId || 0
  483. }
  484. const res = await getPendingAuditList(params)
  485. if (res.code === 200 && res.data) {
  486. const map = res.data.map || {}
  487. // 更新badge
  488. this.topTabs[0].badge = res.data.pendingDataCount || 0
  489. this.topTabs[1].badge = res.data.completedDataCount || 0
  490. this.topTabs[2].badge = res.data.myInitiationsCount || 0
  491. // 标准化映射函数
  492. const mapPending = (arr = []) => arr.map(item => ({
  493. auditName: item.auditName || '未知审核',
  494. statusText: '待审核',
  495. statusClass: 'status-createPending',
  496. id: item.id,
  497. status: item.status,
  498. auditType: item.auditType,
  499. businessId: item.businessId,
  500. businessType: item.businessType,
  501. createTime: item.createTime,
  502. initiatorName: item.initiatorName
  503. }))
  504. const mapCompleted = (arr = []) => arr.map(item => ({
  505. auditName: item.auditName || '未知审核',
  506. statusText: item.status === 1 ? '已通过' : '已驳回',
  507. statusClass: item.status === 1 ? 'status-finish' : 'status-rejected',
  508. id: item.id,
  509. status: item.status,
  510. auditType: item.auditType,
  511. businessId: item.businessId,
  512. businessType: item.businessType,
  513. createTime: item.createTime,
  514. initiatorName: item.initiatorName
  515. }))
  516. const mapMyInitiations = (arr = []) => arr.map(item => ({
  517. auditName: item.auditName || '未知审核',
  518. statusText: '待审核',
  519. statusClass: 'status-createPending',
  520. id: item.id,
  521. status: item.status,
  522. auditType: item.auditType,
  523. businessId: item.businessId,
  524. businessType: item.businessType,
  525. createTime: item.createTime,
  526. initiatorName: item.initiatorName
  527. }))
  528. // 更新三个列表
  529. this.loading.todo = true
  530. this.loading.done = true
  531. this.loading.myInitiate = true
  532. const pendingRaw = map.pendingData || map['pendingData'] || map['pendingData '] || []
  533. const completedRaw = map.completedData || map['completedData'] || map['completedData '] || []
  534. const myInitiationsRaw = map.myInitiations || map['myInitiations'] || map['myInitiations '] || []
  535. this.todoList = mapPending(pendingRaw)
  536. this.doneList = mapCompleted(completedRaw)
  537. this.myInitiateList = mapMyInitiations(myInitiationsRaw)
  538. this.loading.todo = false
  539. this.loading.done = false
  540. this.loading.myInitiate = false
  541. this.hasLoadedOnce = true
  542. this.isInitializing = false
  543. } else {
  544. uni.showToast({
  545. title: '获取审核列表失败',
  546. icon: 'none'
  547. })
  548. this.isInitializing = false
  549. }
  550. } catch (e) {
  551. console.error('加载数据失败', e)
  552. uni.showToast({
  553. title: '加载数据失败',
  554. icon: 'none'
  555. })
  556. // 重置加载状态
  557. this.loading.todo = false
  558. this.loading.done = false
  559. this.loading.myInitiate = false
  560. this.isInitializing = false
  561. }
  562. },
  563. // 格式化时间
  564. formatTime(timeStr) {
  565. if (!timeStr) return ''
  566. const date = new Date(timeStr)
  567. const year = date.getFullYear()
  568. const month = String(date.getMonth() + 1).padStart(2, '0')
  569. const day = String(date.getDate()).padStart(2, '0')
  570. const hours = String(date.getHours()).padStart(2, '0')
  571. const minutes = String(date.getMinutes()).padStart(2, '0')
  572. return `${year}-${month}-${day} ${hours}:${minutes}`
  573. },
  574. getBusinessTypeLabel(businessType) {
  575. if (!businessType || !this.processTemplate) {
  576. return '未设置'
  577. }
  578. const dictItem = this.processTemplate.find(item => item.dictValue === businessType)
  579. return dictItem ? dictItem.dictLabel : businessType
  580. }
  581. }
  582. }
  583. </script>
  584. <style lang="scss" scoped>
  585. .container {
  586. min-height: 100vh;
  587. background: #F7F8FA;
  588. position: relative;
  589. // 搜索栏
  590. .top-box {
  591. padding: 20rpx 32rpx;
  592. display: flex;
  593. align-items: center;
  594. background: #fff;
  595. position: relative;
  596. z-index: 101;
  597. .input-item {
  598. display: flex;
  599. align-items: center;
  600. flex: 1;
  601. height: 72rpx;
  602. background: #F7F8FA;
  603. border-radius: 38rpx;
  604. .search-icon {
  605. width: 26rpx;
  606. height: 26rpx;
  607. margin: 0 10rpx 0 28rpx;
  608. }
  609. input {
  610. flex: 1;
  611. font-size: 28rpx;
  612. color: #333;
  613. }
  614. }
  615. }
  616. // 顶部选项卡
  617. .top-tabs {
  618. display: flex;
  619. background: #fff;
  620. position: relative;
  621. z-index: 101;
  622. .top-tab-item {
  623. flex: 1;
  624. text-align: center;
  625. padding: 24rpx 0;
  626. font-size: 28rpx;
  627. color: #999;
  628. transition: all 0.2s;
  629. &.active {
  630. color: #333;
  631. font-weight: 500;
  632. position: relative;
  633. &::after {
  634. content: '';
  635. position: absolute;
  636. bottom: 0;
  637. left: 50%;
  638. transform: translateX(-50%);
  639. border-radius: 3rpx;
  640. width: 80rpx;
  641. height: 6rpx;
  642. background: #388BFF;
  643. }
  644. }
  645. }
  646. }
  647. // 核心修改:子标签容器(定位参考)
  648. .sub-tabs-wrapper {
  649. position: relative; // 作为popup-box的定位参考
  650. z-index: 101; // 确保在遮罩层之上
  651. // 子标签栏
  652. .sub-tabs {
  653. display: flex;
  654. justify-content: space-between;
  655. background: #fff;
  656. padding: 16rpx 32rpx;
  657. gap: 24rpx;
  658. .sub-tab-item {
  659. flex: 1;
  660. height: 64rpx;
  661. display: flex;
  662. border-radius: 70rpx;
  663. border: 2rpx solid #F2F2F2;
  664. justify-content: center;
  665. align-items: center;
  666. font-size: 28rpx;
  667. color: #666;
  668. transition: all 0.2s;
  669. .icon {
  670. width: 28rpx;
  671. height: 28rpx;
  672. margin-left: 12rpx;
  673. }
  674. }
  675. }
  676. }
  677. /* 子标签弹窗样式 */
  678. .sub-tab-popup-overlay {
  679. position: absolute;
  680. top: 100%;
  681. height: 100vh;
  682. left: 0;
  683. right: 0;
  684. bottom: -1000rpx;
  685. background: rgba(0, 0, 0, 0.5);
  686. z-index: 8;
  687. }
  688. .sub-tab-popup {
  689. position: absolute;
  690. top: 100%;
  691. left: 0;
  692. right: 0;
  693. background: #fff;
  694. border-radius: 0;
  695. padding: 32rpx;
  696. width: 100%;
  697. box-sizing: border-box;
  698. box-shadow: none;
  699. z-index: 9;
  700. }
  701. .sub-tab-options {
  702. display: flex;
  703. flex-wrap: wrap;
  704. gap: 24rpx;
  705. border-radius: 70rpx 70rpx 70rpx 70rpx;
  706. justify-content: flex-start;
  707. }
  708. .sub-tab-option {
  709. padding: 24rpx 14rpx;
  710. border-radius: 70rpx;
  711. font-size: 28rpx;
  712. color: #666666;
  713. background: #ffffff;
  714. border: 1rpx solid #e8e8e8;
  715. transition: all 0.2s ease;
  716. text-align: center;
  717. min-width: 214rpx;
  718. box-sizing: border-box;
  719. &.active {
  720. background: rgba(56, 139, 255, 0.15);
  721. color: #ffffff;
  722. color: #388BFF;
  723. border: 1rpx solid #388BFF;
  724. }
  725. }
  726. // 列表区域
  727. .content {
  728. padding: 24rpx;
  729. box-sizing: border-box;
  730. min-height: calc(100vh - 300rpx);
  731. position: relative;
  732. z-index: 98; // 低于遮罩层
  733. .task-card {
  734. background: #fff;
  735. border-radius: 24rpx;
  736. margin-bottom: 20rpx;
  737. border: 2rpx solid #E9F2FF;
  738. position: relative;
  739. padding-bottom: 24rpx;
  740. .card-top {
  741. display: flex;
  742. justify-content: space-between;
  743. align-items: center;
  744. background: linear-gradient(90deg, #E8F1FF 0%, #FFFFFF 100%);
  745. border-radius: 24rpx 24rpx 0 0;
  746. padding: 26rpx 24rpx;
  747. margin-bottom: 16rpx;
  748. .card-task-title {
  749. font-size: 32rpx;
  750. font-weight: 600;
  751. color: #333;
  752. }
  753. .status-tag {
  754. padding: 8rpx 16rpx;
  755. border-radius: 20rpx;
  756. font-size: 24rpx;
  757. &.status-wait {
  758. background: #E3F2FD;
  759. color: #2196F3;
  760. }
  761. &.status-finish {
  762. background: #E6FAEF;
  763. color: #07C160;
  764. }
  765. &.status-rejected {
  766. background: #FFF4F5;
  767. color: #CF3546;
  768. }
  769. &.status-createPending {
  770. background: #FFF8E6;
  771. color: #FF9500;
  772. }
  773. }
  774. }
  775. .card-content {
  776. padding: 0 24rpx;
  777. .time-info {
  778. font-size: 28rpx;
  779. color: #666;
  780. display: flex;
  781. flex-direction: column;
  782. gap: 8rpx;
  783. margin-bottom: 24rpx;
  784. .time-item {
  785. display: flex;
  786. font-size: 28rpx;
  787. color: #333;
  788. .title {
  789. color: #666;
  790. }
  791. }
  792. }
  793. .operate-btn-group {
  794. display: flex;
  795. align-items: center;
  796. justify-content: space-between;
  797. .share-btn {
  798. display: flex;
  799. align-items: center;
  800. font-size: 28rpx;
  801. color: #666;
  802. .share-icon {
  803. width: 36rpx;
  804. height: 36rpx;
  805. margin-right: 8rpx;
  806. }
  807. }
  808. .date {
  809. font-size: 24rpx;
  810. color: #999;
  811. }
  812. }
  813. }
  814. }
  815. }
  816. // 筛选弹窗
  817. .filter-popup {
  818. position: fixed;
  819. top: 0;
  820. left: 0;
  821. right: 0;
  822. bottom: 0;
  823. background: rgba(0, 0, 0, 0.5);
  824. z-index: 999;
  825. display: flex;
  826. align-items: flex-end;
  827. .filter-content {
  828. width: 100%;
  829. background: #fff;
  830. border-radius: 24rpx 24rpx 0 0;
  831. padding: 32rpx;
  832. .filter-header {
  833. display: flex;
  834. align-items: center;
  835. justify-content: center;
  836. margin-bottom: 32rpx;
  837. position: relative;
  838. .filter-title {
  839. font-size: 32rpx;
  840. font-weight: bold;
  841. color: #333;
  842. }
  843. .filter-close-btn {
  844. position: absolute;
  845. right: 0;
  846. width: 44rpx;
  847. height: 44rpx;
  848. }
  849. }
  850. .filter-form {
  851. padding: 0 0 24rpx 0;
  852. .filter-section {
  853. margin-bottom: 32rpx;
  854. .section-label {
  855. font-weight: 500;
  856. font-size: 28rpx;
  857. color: #333;
  858. margin-bottom: 24rpx;
  859. }
  860. .time-range {
  861. display: flex;
  862. align-items: center;
  863. gap: 16rpx;
  864. .time-input {
  865. flex: 1;
  866. height: 72rpx;
  867. text-align: center;
  868. background: #F7F8FA;
  869. border-radius: 8rpx;
  870. padding: 0 16rpx;
  871. font-size: 26rpx;
  872. color: #333;
  873. }
  874. .time-separator {
  875. font-size: 24rpx;
  876. color: #999;
  877. }
  878. }
  879. .btn-group {
  880. display: flex;
  881. gap: 16rpx;
  882. .filter-btn {
  883. width: 214rpx;
  884. height: 72rpx;
  885. background: #F7F8FA;
  886. border-radius: 70rpx;
  887. line-height: 72rpx;
  888. text-align: center;
  889. font-size: 28rpx;
  890. transition: all 0.2s;
  891. color: #333;
  892. &.active {
  893. background: rgba(56, 139, 255, 0.15);
  894. color: #388BFF;
  895. }
  896. }
  897. }
  898. }
  899. }
  900. .filter-actions {
  901. display: flex;
  902. gap: 24rpx;
  903. margin-top: 40rpx;
  904. .reset-btn,
  905. .confirm-btn {
  906. flex: 1;
  907. height: 80rpx;
  908. line-height: 80rpx;
  909. text-align: center;
  910. border-radius: 200rpx;
  911. font-size: 28rpx;
  912. transition: all 0.2s;
  913. }
  914. .reset-btn {
  915. background: #fff;
  916. color: #388BFF;
  917. border: 2rpx solid #388BFF;
  918. &:active {
  919. background: #EBF3FF;
  920. }
  921. }
  922. .confirm-btn {
  923. background: #388BFF;
  924. color: #fff;
  925. &:active {
  926. background: #2A78E5;
  927. }
  928. }
  929. }
  930. }
  931. }
  932. }
  933. </style>