monthly-list.vue 22 KB

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