monthly-list.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. <template>
  2. <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
  3. <cwg-header :title="t('wallet.item52')" />
  4. <view id="custom_history" class="">
  5. <view class="main-content">
  6. <!-- 无任务列表提示 -->
  7. <view class="list-empty-state" v-if="!tableData || tableData.length === 0">
  8. <cwg-empty-state title="UtaskList.item12" />
  9. </view>
  10. <!-- 数据卡片展示 -->
  11. <view class="outer-card" v-for="(item, index) in tableData" :key="index"
  12. v-show="tableData && tableData.length > 0">
  13. <view class="data-cards">
  14. <!-- 第一行:总数据 -->
  15. <view class="total-data-row">
  16. <view class="data-card total-card">
  17. <view class="card-content">
  18. <view class="card-title">{{ t("UtaskList.item13") }}</view>
  19. <view class="card-value" style="color: #ff4d4f">
  20. {{ item.depositAmount || 0 }}
  21. </view>
  22. </view>
  23. </view>
  24. <view class="data-card total-card">
  25. <view class="card-content">
  26. <view class="card-title">{{ t("UtaskList.item3") }}</view>
  27. <view class="card-value" style="color: #ff4d4f">
  28. {{ item.completeVolume || 0 }}
  29. </view>
  30. </view>
  31. </view>
  32. <view class="data-card total-card">
  33. <view class="card-content">
  34. <view class="card-title">{{ t("Label.State") }}</view>
  35. <view class="card-value" :style="getStatusStyle(item.status)">
  36. {{ getStatusText(item.status) }}
  37. </view>
  38. </view>
  39. </view>
  40. <view class="data-card total-card" v-if="item.status === 2">
  41. <view class="card-content">
  42. <view class="card-title">
  43. {{ t("MonthlyActivities.item4") }}
  44. </view>
  45. <view class="card-value" :style="getGiveStatusStyle(item.giveStatus)">
  46. {{ getGiveStatusText(item.giveStatus) }}
  47. </view>
  48. </view>
  49. </view>
  50. </view>
  51. <!-- 第二行:分数据 -->
  52. <view class="sub-data-row">
  53. <view class="data-card">
  54. <view class="card-content">
  55. <view class="card-value">{{ item.endDate }}</view>
  56. <view class="card-desc">{{ t("UtaskList.item10") }}</view>
  57. </view>
  58. </view>
  59. <view class="data-card" v-if="item.status === 2">
  60. <view class="card-content">
  61. <view class="card-value">{{ item.applyTime }}</view>
  62. <view class="card-desc">{{ t("MonthlyActivities.item6") }}</view>
  63. </view>
  64. </view>
  65. <view class="data-card" v-if="item.status === 2">
  66. <view class="card-content">
  67. <view class="card-value">{{ item.grantTime }}</view>
  68. <view class="card-desc">{{ t("MonthlyActivities.item7") }}</view>
  69. </view>
  70. </view>
  71. <view class="data-card" v-if="item.logisticsOrder">
  72. <view class="card-content">
  73. <view class="card-value">{{ item.logisticsOrder }}</view>
  74. <view class="card-desc">{{ t("MonthlyActivities.item9") }}</view>
  75. </view>
  76. </view>
  77. <view class="data-card btn-card" v-show="shouldShowCard(item, item)">
  78. <!-- 取消按钮 - status为1时显示 -->
  79. <button v-if="item.status === 1"
  80. class="btn btn-dark btn-sm waves-effect waves-light btn-outline-dark1"
  81. @click="cancelTask(item.id)" :loading="loadingStates[item.id] === 'cancel'">
  82. {{ t("Btn.Cancel") }}
  83. </button>
  84. <!-- 礼物申请按钮 - status为2且giveStatus为1时显示 -->
  85. <button class="btn btn-danger btn-sm waves-effect waves-light"
  86. v-if="item.status === 2 && item.giveStatus === 1 && lang1 && !isGiftApplyPastDeadline(item)"
  87. @click="applyGift(item.id)" :loading="loadingStates[item.id] === 'applyGift'">
  88. {{ t("Btn.Application") }}
  89. </button>
  90. </view>
  91. </view>
  92. <!-- 提示信息 -->
  93. <view class="tip-info" style="padding: 10px 0; color: #909399; font-size: 14px">
  94. {{ t("MonthlyActivities.item10") }}
  95. </view>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 礼物申请对话框 -->
  100. <GiftApplicationPopup v-model:visible="dialogGiftApplication" :title="t('Btn.Application')"
  101. :giftList="giftList" :giftForm="giftForm" @confirm="submitGiftApply" />
  102. </view>
  103. <cwg-confirm-popup />
  104. </cwg-page-wrapper>
  105. </template>
  106. <script setup lang="ts">
  107. import { ref, computed, onMounted, watch } from 'vue'
  108. import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
  109. import { useI18n } from 'vue-i18n'
  110. import { activityApi } from "@/service/activity"
  111. import Config from "@/config/index"
  112. import GiftApplicationPopup from "./components/GiftApplicationPopup.vue"
  113. import useUserStore from "@/stores/use-user-store";
  114. const userStore = useUserStore();
  115. import { useConfirm } from '@/hooks/useConfirm'
  116. const confirm = useConfirm()
  117. const { t, locale } = useI18n()
  118. let { Code } = Config
  119. // ---------- 响应式数据 ----------
  120. const flag = ref(false)
  121. const reasons = ref({})
  122. const pictLoading = ref(false)
  123. const tableData = ref<any[]>([])
  124. const time = ref("")
  125. const loadingStates = ref<Record<string, string | null>>({}) // 用于跟踪每个任务的加载状态
  126. // 卡片数据
  127. const totalTasks = ref(0)
  128. const totalRewards = ref(0)
  129. const completionRate = ref("0")
  130. const inProgressTasks = ref(0)
  131. const completedTasks = ref(0)
  132. const expiredTasks = ref(0)
  133. const rejectedTasks = ref(0)
  134. const todayRewards = ref(0)
  135. const weekRewards = ref(0)
  136. const monthRewards = ref(0)
  137. const activityLevel = ref("0")
  138. // 礼物申请对话框相关
  139. const dialogGiftApplication = ref(false)
  140. const giftList = ref<any[]>([])
  141. const giftForm = ref({
  142. id: null as number | null,
  143. giveCode: "",
  144. giveName: "",
  145. giveAddress: "",
  146. givePhone: "",
  147. giveAcceptName: "",
  148. })
  149. const giftSubmitting = ref(false)
  150. // 原 watch 中依赖的 search(保留)
  151. const search = ref({ type: "" })
  152. // ---------- 计算属性 ----------
  153. // 注意:document 在 uni-app 中不可用,此处保留原逻辑但需注意运行环境
  154. // 若需兼容,可改用 uni.getSystemInfoSync().windowWidth
  155. const lang1 = computed(() => {
  156. return userStore?.userInfo?.customInfo?.country == "CN"
  157. })
  158. // ---------- 方法 ----------
  159. const getStatus = (status: number) => {
  160. if (status == 1) {
  161. return t("State.Ongoing")
  162. } else if (status == 2) {
  163. return t("State.Completed")
  164. } else if (status == 3) {
  165. return t("State.Cancelled")
  166. } else if (status == 4) {
  167. return t("State.expireTime")
  168. }
  169. }
  170. const getStatusText = (status: number) => {
  171. if (status == 1) {
  172. return t("State.InTask")
  173. } else if (status == 2) {
  174. return t("UtaskList.item6")
  175. } else if (status == 3) {
  176. return t("State.Cancelled")
  177. } else if (status == 4) {
  178. return t("State.Ended")
  179. }
  180. return ""
  181. }
  182. const getStatusStyle = (status: number) => {
  183. if (status == 1) {
  184. return "color: #ffd591;"
  185. } else if (status == 2) {
  186. return "color: #52c41a;"
  187. } else if (status == 3) {
  188. return "color: #999999;"
  189. } else if (status == 4) {
  190. return "color: #999999;"
  191. }
  192. return "color: var(--bs-heading-color);"
  193. }
  194. const getGiveStatusText = (giveStatus: number) => {
  195. if (giveStatus == 1) {
  196. return t("State.NotApply")
  197. } else if (giveStatus == 2) {
  198. return t("State.Applied")
  199. } else if (giveStatus == 3) {
  200. return t("State.Granted")
  201. }
  202. return ""
  203. }
  204. const getGiveStatusStyle = (giveStatus: number) => {
  205. if (giveStatus == 1) {
  206. return "color: #999999;"
  207. } else if (giveStatus == 2) {
  208. return "color: #1890ff;"
  209. } else if (giveStatus == 3) {
  210. return "color: #52c41a;"
  211. }
  212. return "color: var(--bs-heading-color);"
  213. }
  214. const canPerformAction = (task: any) => {
  215. return task.status === 1 || (task.status === 2 && task.withdrawStatus === 1)
  216. }
  217. const shouldShowCard = (el: any) => {
  218. const hasCancelButton = el.status === 1
  219. const hasGiftApplyButton = el.status === 2 && el.giveStatus === 1 && lang1.value && !isGiftApplyPastDeadline(el)
  220. return hasCancelButton || hasGiftApplyButton
  221. }
  222. // 恢复信用(原 completeTask)
  223. const completeTask = async (id: number) => {
  224. return new Promise(async (resolve) => {
  225. const resConfirm = await uni.showModal({
  226. title: t("Msg.SystemPrompt"),
  227. content: t("surplusList.item9"),
  228. confirmText: t("Btn.Confirm"),
  229. cancelText: t("Btn.Cancel"),
  230. })
  231. if (!resConfirm.confirm) return resolve(null)
  232. loadingStates.value[id] = "complete"
  233. try {
  234. const res = await activityApi.ActivitySurplusRecoverCredit({ id })
  235. if (res.code == Code.StatusOK) {
  236. // uni.showToast({ title: t("Msg.Success"), icon: "success" })
  237. searchFunc()
  238. } else {
  239. uni.showToast({ title: res.msg, icon: "none" })
  240. }
  241. } catch (error) {
  242. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  243. } finally {
  244. loadingStates.value[id] = null
  245. resolve(null)
  246. }
  247. })
  248. }
  249. // 提现
  250. const withdrawTask = async (id: number) => {
  251. return new Promise(async (resolve) => {
  252. const resConfirm = await uni.showModal({
  253. title: t("Msg.SystemPrompt"),
  254. content: t("UtaskList.item15"),
  255. confirmText: t("Btn.Confirm"),
  256. cancelText: t("Btn.Cancel"),
  257. })
  258. if (!resConfirm.confirm) return resolve(null)
  259. loadingStates.value[id] = "withdraw"
  260. try {
  261. const res = await activityApi.UcoinWithdraw({ id })
  262. if (res.code == Code.StatusOK) {
  263. searchFunc()
  264. } else {
  265. uni.showToast({ title: res.msg, icon: "none" })
  266. }
  267. } catch (error) {
  268. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  269. } finally {
  270. loadingStates.value[id] = null
  271. resolve(null)
  272. }
  273. })
  274. }
  275. // 取消任务
  276. const cancelTask = async (id: number) => {
  277. try {
  278. await confirm({
  279. title: t("Msg.SystemPrompt"),
  280. content: t("UtaskList.item8"),
  281. confirmText: t("Btn.Confirm"),
  282. cancelText: t("Btn.Cancel"),
  283. })
  284. const res = await activityApi.ActivityMonthlyCancel({ id })
  285. if (res.code == Code.StatusOK) {
  286. uni.showToast({ title: t("UtaskList.item9"), icon: "success" })
  287. searchFunc()
  288. } else {
  289. uni.showToast({ title: res.msg, icon: "none" })
  290. }
  291. } catch (error) {
  292. if (error?.msg) uni.showToast({ title: error.msg, icon: "none" })
  293. }
  294. }
  295. // 活动结束时间超过15天后不再显示礼物申请(endTime / endDate 与接口字段对齐)
  296. const isGiftApplyPastDeadline = (item: any) => {
  297. const raw = item.endTime || item.endDate;
  298. if (!raw) return false;
  299. const endMs =
  300. typeof raw === "number"
  301. ? raw
  302. : new Date(String(raw).replace(/-/g, "/")).getTime();
  303. if (Number.isNaN(endMs)) return false;
  304. const deadlineMs = endMs + 15 * 24 * 60 * 60 * 1000;
  305. return Date.now() > deadlineMs;
  306. }
  307. // 礼物申请 - 打开对话框并获取礼品列表
  308. const applyGift = async (id: number) => {
  309. // 重置表单
  310. giftForm.value = {
  311. id: id,
  312. giveCode: "",
  313. giveName: "",
  314. giveAddress: "",
  315. givePhone: "",
  316. giveAcceptName: "",
  317. }
  318. giftList.value = []
  319. // 获取礼品列表
  320. try {
  321. const res = await activityApi.ActivityMonthlyGiveList({ id })
  322. if (res.code == Code.StatusOK) {
  323. giftList.value = res.data || []
  324. dialogGiftApplication.value = true
  325. } else {
  326. uni.showToast({ title: res.msg, icon: "none" })
  327. }
  328. } catch (error) {
  329. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  330. }
  331. }
  332. // 处理礼品选择变化
  333. const handleGiftChange = (giveCode: string) => {
  334. const selectedGift = giftList.value.find(gift => gift.giveCode === giveCode)
  335. if (selectedGift) {
  336. giftForm.value.giveName = selectedGift.giveName
  337. }
  338. }
  339. // 提交礼物申请
  340. const submitGiftApply = async (form) => {
  341. // 表单验证(假设模板中有 ref="giftForm" 的 uni-forms 组件)
  342. // 这里需要适配实际验证方式,简化起见调用一个验证函数,若验证失败则返回
  343. // 由于 uni-app 中没有直接提供 validate,需根据实际组件实现,此处保留原逻辑结构
  344. // 实际使用时请配合 uni-forms 或自定义验证
  345. const valid = true // 占位,实际需调用 this.$refs.giftForm.validate()
  346. if (!valid) return
  347. await confirm({
  348. title: t("Msg.SystemPrompt"),
  349. content: t("MonthlyActivities.item10"),
  350. confirmText: t("Btn.Confirm"),
  351. cancelText: t("Btn.Cancel"),
  352. })
  353. giftSubmitting.value = true
  354. try {
  355. const res = await activityApi.ActivityMonthlyGiveApply({
  356. id: giftForm.value.id,
  357. giveCode: form.giveCode.trim(),
  358. giveName: form.giveName.trim(),
  359. giveAddress: form.giveAddress.trim(),
  360. givePhone: form.givePhone.trim(),
  361. giveAcceptName: form.giveAcceptName.trim(),
  362. })
  363. if (res.code == Code.StatusOK) {
  364. // uni.showToast({ title: t("Msg.Success"), icon: "success" })
  365. dialogGiftApplication.value = false
  366. searchFunc()
  367. } else {
  368. uni.showToast({ title: res.msg, icon: "none" })
  369. }
  370. } catch (error) {
  371. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  372. } finally {
  373. giftSubmitting.value = false
  374. }
  375. }
  376. // 查看任务进度
  377. const viewTaskProgress = async (id: number) => {
  378. loadingStates.value[id] = "progress"
  379. try {
  380. const res = await activityApi.UcoinProgress()
  381. if (res.code == Code.StatusOK) {
  382. uni.showToast({ title: res.msg, icon: "none" })
  383. } else {
  384. uni.showToast({ title: res.msg, icon: "none" })
  385. }
  386. } catch (error) {
  387. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  388. } finally {
  389. loadingStates.value[id] = null
  390. }
  391. }
  392. // 计算卡片数据(保留原方法体)
  393. const calculateCardData = () => {
  394. // 可根据需求实现
  395. }
  396. // 返回活动
  397. const backActivity = () => {
  398. uni.navigateBack()
  399. }
  400. // 获取列表
  401. const searchFunc = async () => {
  402. if (flag.value) return
  403. flag.value = true
  404. pictLoading.value = true
  405. let res = await activityApi.ActivityMonthlyTaskList()
  406. if (res.code == Code.StatusOK) {
  407. tableData.value = res.data
  408. calculateCardData()
  409. // uni.showToast({ title: t("Msg.SearchSuccess"), icon: "success" })
  410. pictLoading.value = false
  411. flag.value = false
  412. } else {
  413. uni.showToast({ title: res.msg, icon: "none" })
  414. pictLoading.value = false
  415. flag.value = false
  416. }
  417. }
  418. // ---------- 生命周期与监听 ----------
  419. onMounted(() => {
  420. searchFunc()
  421. })
  422. // 原 watch(保留)
  423. watch(
  424. () => search.value.type,
  425. () => {
  426. searchFunc()
  427. }
  428. )
  429. // 若需要 uni-app 页面生命周期可额外添加 onShow 等
  430. // onShow(() => {})
  431. </script>
  432. <style lang="scss" scoped>
  433. @import "@/uni.scss";
  434. .tip-info {
  435. line-height: px2rpx(18);
  436. }
  437. #custom_history {
  438. width: 100%;
  439. height: 100%;
  440. .no-data-container {
  441. display: flex;
  442. justify-content: center;
  443. align-items: center;
  444. height: px2rpx(300);
  445. .no-data-content {
  446. text-align: center;
  447. color: var(--bs-heading-color);
  448. i {
  449. font-size: px2rpx(48);
  450. margin-bottom: px2rpx(16);
  451. display: block;
  452. }
  453. p {
  454. font-size: px2rpx(16);
  455. margin: 0;
  456. }
  457. }
  458. }
  459. .main-content {
  460. width: 100%;
  461. height: calc(100% - 50px);
  462. // @include bg_white();
  463. padding: px2rpx(20);
  464. box-sizing: border-box;
  465. overflow: hidden;
  466. overflow-y: auto;
  467. }
  468. .state.btn {
  469. background: #eb3f57;
  470. color: white;
  471. padding: px2rpx(3) px2rpx(15);
  472. border-radius: px2rpx(4);
  473. }
  474. .action-buttons {
  475. display: flex;
  476. flex-direction: column;
  477. gap: px2rpx(8);
  478. align-items: center;
  479. .el-button {
  480. min-width: px2rpx(80);
  481. font-size: px2rpx(12);
  482. }
  483. .status-text {
  484. font-size: px2rpx(14);
  485. color: var(--bs-heading-color);
  486. font-weight: 500;
  487. }
  488. }
  489. // 外层卡片样式
  490. .outer-card {
  491. background: #ffffff;
  492. border-radius: px2rpx(16);
  493. padding: px2rpx(24);
  494. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  495. border: 1px solid #f0f0f0;
  496. margin-bottom: px2rpx(30);
  497. }
  498. // 数据卡片样式
  499. .data-cards {
  500. display: flex;
  501. flex-direction: column;
  502. gap: px2rpx(20);
  503. .total-data-row {
  504. display: grid;
  505. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  506. gap: px2rpx(15);
  507. }
  508. .sub-data-row {
  509. display: grid;
  510. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  511. gap: px2rpx(15);
  512. }
  513. .data-card {
  514. background: #ffffff;
  515. border-radius: px2rpx(12);
  516. padding: px2rpx(24);
  517. // box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  518. border: 1px solid #f0f0f0;
  519. transition: all 0.3s ease;
  520. text-align: center;
  521. &:hover {
  522. transform: translateY(-2px);
  523. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
  524. }
  525. // 根据状态设置背景色
  526. &.status-1 {
  527. background: #fff7e6; // 任务中 - 黄色背景
  528. border-color: #ffd591;
  529. }
  530. &.status-2 {
  531. background: #f6ffed; // 已完成 - 绿色背景
  532. border-color: #b7eb8f;
  533. }
  534. &.status-3,
  535. &.status-4 {
  536. background: #f5f5f5; // 已取消/已结束 - 灰色背景
  537. border-color: #d9d9d9;
  538. }
  539. &.total-card {
  540. background: #ffffff;
  541. color: var(--bs-heading-color);
  542. // border: 1px solid gray;
  543. .card-value {
  544. color: var(--bs-heading-color);
  545. }
  546. .card-desc {
  547. color: var(--bs-heading-color);
  548. }
  549. // 状态卡片的特殊样式
  550. &.status-1 {
  551. background: #fff7e6;
  552. border-color: #ffd591;
  553. }
  554. &.status-2 {
  555. background: #f6ffed;
  556. border-color: #b7eb8f;
  557. }
  558. &.status-3,
  559. &.status-4 {
  560. background: #f5f5f5;
  561. border-color: #d9d9d9;
  562. }
  563. }
  564. &.sub-card {
  565. background: #ffffff;
  566. border-left: px2rpx(4) solid #667eea;
  567. &:nth-child(2) {
  568. border-left-color: #52c41a;
  569. }
  570. &:nth-child(3) {
  571. border-left-color: #faad14;
  572. }
  573. &:nth-child(4) {
  574. border-left-color: #ff4d4f;
  575. }
  576. }
  577. .card-content {
  578. .card-title {
  579. font-size: px2rpx(16);
  580. color: var(--bs-heading-color);
  581. margin-bottom: px2rpx(8);
  582. font-weight: 500;
  583. }
  584. .card-value {
  585. font-size: px2rpx(18);
  586. font-weight: 700;
  587. color: var(--bs-heading-color);
  588. margin-bottom: px2rpx(4);
  589. line-height: 1;
  590. }
  591. .card-desc {
  592. font-size: px2rpx(12);
  593. color: var(--bs-heading-color);
  594. line-height: 1.4;
  595. }
  596. }
  597. }
  598. .btn-card {
  599. display: flex;
  600. justify-content: center;
  601. align-items: center;
  602. flex-direction: column;
  603. gap: px2rpx(10);
  604. button {
  605. width: 100%;
  606. }
  607. }
  608. }
  609. // 响应式设计
  610. @media (max-width: 768px) {
  611. .data-cards {
  612. .total-data-row,
  613. .sub-data-row {
  614. grid-template-columns: 1fr;
  615. }
  616. .data-card {
  617. padding: px2rpx(16);
  618. .card-content {
  619. .card-value {
  620. font-size: px2rpx(24);
  621. }
  622. }
  623. }
  624. }
  625. }
  626. }
  627. </style>