monthly-list.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  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-primary 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 { useConfirm } from '@/hooks/useConfirm'
  110. const confirm = useConfirm()
  111. const { t, locale } = useI18n()
  112. let { Code } = Config
  113. // ---------- 存储辅助函数(模拟原 Session) ----------
  114. const Session = {
  115. Get(key: string, parse = false) {
  116. const value = uni.getStorageSync(key)
  117. if (parse && value) {
  118. try {
  119. return JSON.parse(value)
  120. } catch {
  121. return value
  122. }
  123. }
  124. return value
  125. }
  126. }
  127. // ---------- 响应式数据 ----------
  128. const flag = ref(false)
  129. const reasons = ref({})
  130. const pictLoading = ref(false)
  131. const tableData = ref<any[]>([])
  132. const time = ref("")
  133. const loadingStates = ref<Record<string, string | null>>({}) // 用于跟踪每个任务的加载状态
  134. // 卡片数据
  135. const totalTasks = ref(0)
  136. const totalRewards = ref(0)
  137. const completionRate = ref("0")
  138. const inProgressTasks = ref(0)
  139. const completedTasks = ref(0)
  140. const expiredTasks = ref(0)
  141. const rejectedTasks = ref(0)
  142. const todayRewards = ref(0)
  143. const weekRewards = ref(0)
  144. const monthRewards = ref(0)
  145. const activityLevel = ref("0")
  146. // 礼物申请对话框相关
  147. const dialogGiftApplication = ref(false)
  148. const giftList = ref<any[]>([])
  149. const giftForm = ref({
  150. id: null as number | null,
  151. giveCode: "",
  152. giveName: "",
  153. giveAddress: "",
  154. givePhone: "",
  155. giveAcceptName: "",
  156. })
  157. const giftSubmitting = ref(false)
  158. // 原 watch 中依赖的 search(保留)
  159. const search = ref({ type: "" })
  160. // ---------- 计算属性 ----------
  161. const expireTime = computed(() => {
  162. return Session.Get("user", true)
  163. })
  164. const lang = computed(() => {
  165. return (Session.Get("lang") == "en" && document.body.clientWidth < 1330)
  166. })
  167. // 注意:document 在 uni-app 中不可用,此处保留原逻辑但需注意运行环境
  168. // 若需兼容,可改用 uni.getSystemInfoSync().windowWidth
  169. const lang1 = computed(() => {
  170. const user = Session.Get("user", true)
  171. return user?.customInfo?.country == "CN"
  172. })
  173. // ---------- 方法 ----------
  174. const getStatus = (status: number) => {
  175. if (status == 1) {
  176. return t("State.Ongoing")
  177. } else if (status == 2) {
  178. return t("State.Completed")
  179. } else if (status == 3) {
  180. return t("State.Cancelled")
  181. } else if (status == 4) {
  182. return t("State.expireTime")
  183. }
  184. }
  185. const getStatusText = (status: number) => {
  186. if (status == 1) {
  187. return t("State.InTask")
  188. } else if (status == 2) {
  189. return t("UtaskList.item6")
  190. } else if (status == 3) {
  191. return t("State.Cancelled")
  192. } else if (status == 4) {
  193. return t("State.Ended")
  194. }
  195. return ""
  196. }
  197. const getStatusStyle = (status: number) => {
  198. if (status == 1) {
  199. return "color: #ffd591;"
  200. } else if (status == 2) {
  201. return "color: #52c41a;"
  202. } else if (status == 3) {
  203. return "color: #999999;"
  204. } else if (status == 4) {
  205. return "color: #999999;"
  206. }
  207. return "color: var(--bs-heading-color);"
  208. }
  209. const getGiveStatusText = (giveStatus: number) => {
  210. if (giveStatus == 1) {
  211. return t("State.NotApply")
  212. } else if (giveStatus == 2) {
  213. return t("State.Applied")
  214. } else if (giveStatus == 3) {
  215. return t("State.Granted")
  216. }
  217. return ""
  218. }
  219. const getGiveStatusStyle = (giveStatus: number) => {
  220. if (giveStatus == 1) {
  221. return "color: #999999;"
  222. } else if (giveStatus == 2) {
  223. return "color: #1890ff;"
  224. } else if (giveStatus == 3) {
  225. return "color: #52c41a;"
  226. }
  227. return "color: var(--bs-heading-color);"
  228. }
  229. const canPerformAction = (task: any) => {
  230. return task.status === 1 || (task.status === 2 && task.withdrawStatus === 1)
  231. }
  232. const shouldShowCard = (el: any) => {
  233. const hasCancelButton = el.status === 1
  234. const hasGiftApplyButton = el.status === 2 && el.giveStatus === 1 && lang1.value
  235. return hasCancelButton || hasGiftApplyButton
  236. }
  237. // 恢复信用(原 completeTask)
  238. const completeTask = async (id: number) => {
  239. return new Promise(async (resolve) => {
  240. const resConfirm = await uni.showModal({
  241. title: t("Msg.SystemPrompt"),
  242. content: t("surplusList.item9"),
  243. confirmText: t("Btn.Confirm"),
  244. cancelText: t("Btn.Cancel"),
  245. })
  246. if (!resConfirm.confirm) return resolve(null)
  247. loadingStates.value[id] = "complete"
  248. try {
  249. const res = await activityApi.ActivitySurplusRecoverCredit({ id })
  250. if (res.code == Code.StatusOK) {
  251. // uni.showToast({ title: t("Msg.Success"), icon: "success" })
  252. searchFunc()
  253. } else {
  254. uni.showToast({ title: res.msg, icon: "none" })
  255. }
  256. } catch (error) {
  257. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  258. } finally {
  259. loadingStates.value[id] = null
  260. resolve(null)
  261. }
  262. })
  263. }
  264. // 提现
  265. const withdrawTask = async (id: number) => {
  266. return new Promise(async (resolve) => {
  267. const resConfirm = await uni.showModal({
  268. title: t("Msg.SystemPrompt"),
  269. content: t("UtaskList.item15"),
  270. confirmText: t("Btn.Confirm"),
  271. cancelText: t("Btn.Cancel"),
  272. })
  273. if (!resConfirm.confirm) return resolve(null)
  274. loadingStates.value[id] = "withdraw"
  275. try {
  276. const res = await activityApi.UcoinWithdraw({ id })
  277. if (res.code == Code.StatusOK) {
  278. searchFunc()
  279. } else {
  280. uni.showToast({ title: res.msg, icon: "none" })
  281. }
  282. } catch (error) {
  283. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  284. } finally {
  285. loadingStates.value[id] = null
  286. resolve(null)
  287. }
  288. })
  289. }
  290. // 取消任务
  291. const cancelTask = async (id: number) => {
  292. try {
  293. await confirm({
  294. title: t("Msg.SystemPrompt"),
  295. content: t("UtaskList.item8"),
  296. confirmText: t("Btn.Confirm"),
  297. cancelText: t("Btn.Cancel"),
  298. })
  299. const res = await activityApi.ActivityMonthlyCancel({ id })
  300. if (res.code == Code.StatusOK) {
  301. uni.showToast({ title: t("UtaskList.item9"), icon: "success" })
  302. searchFunc()
  303. } else {
  304. uni.showToast({ title: res.msg, icon: "none" })
  305. }
  306. } catch (error) {
  307. if (error?.msg) uni.showToast({ title: error.msg, icon: "none" })
  308. }
  309. }
  310. // 礼物申请 - 打开对话框并获取礼品列表
  311. const applyGift = async (id: number) => {
  312. // 重置表单
  313. giftForm.value = {
  314. id: id,
  315. giveCode: "",
  316. giveName: "",
  317. giveAddress: "",
  318. givePhone: "",
  319. giveAcceptName: "",
  320. }
  321. giftList.value = []
  322. // 获取礼品列表
  323. try {
  324. const res = await activityApi.ActivityMonthlyGiveList({ id })
  325. if (res.code == Code.StatusOK) {
  326. giftList.value = res.data || []
  327. dialogGiftApplication.value = true
  328. } else {
  329. uni.showToast({ title: res.msg, icon: "none" })
  330. }
  331. } catch (error) {
  332. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  333. }
  334. }
  335. // 处理礼品选择变化
  336. const handleGiftChange = (giveCode: string) => {
  337. const selectedGift = giftList.value.find(gift => gift.giveCode === giveCode)
  338. if (selectedGift) {
  339. giftForm.value.giveName = selectedGift.giveName
  340. }
  341. }
  342. // 提交礼物申请
  343. const submitGiftApply = async (form) => {
  344. // 表单验证(假设模板中有 ref="giftForm" 的 uni-forms 组件)
  345. // 这里需要适配实际验证方式,简化起见调用一个验证函数,若验证失败则返回
  346. // 由于 uni-app 中没有直接提供 validate,需根据实际组件实现,此处保留原逻辑结构
  347. // 实际使用时请配合 uni-forms 或自定义验证
  348. const valid = true // 占位,实际需调用 this.$refs.giftForm.validate()
  349. if (!valid) return
  350. const resConfirm = await uni.showModal({
  351. title: t("Msg.SystemPrompt"),
  352. content: t("MonthlyActivities.item10"),
  353. confirmText: t("Btn.Confirm"),
  354. cancelText: t("Btn.Cancel"),
  355. })
  356. if (!resConfirm.confirm) return
  357. giftSubmitting.value = true
  358. try {
  359. const res = await activityApi.ActivityMonthlyGiveApply({
  360. id: giftForm.value.id,
  361. giveCode: form.giveCode,
  362. giveName: form.giveName,
  363. giveAddress: form.giveAddress.trim(),
  364. givePhone: form.givePhone.trim(),
  365. })
  366. if (res.code == Code.StatusOK) {
  367. // uni.showToast({ title: t("Msg.Success"), icon: "success" })
  368. dialogGiftApplication.value = false
  369. searchFunc()
  370. } else {
  371. uni.showToast({ title: res.msg, icon: "none" })
  372. }
  373. } catch (error) {
  374. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  375. } finally {
  376. giftSubmitting.value = false
  377. }
  378. }
  379. // 查看任务进度
  380. const viewTaskProgress = async (id: number) => {
  381. loadingStates.value[id] = "progress"
  382. try {
  383. const res = await activityApi.UcoinProgress()
  384. if (res.code == Code.StatusOK) {
  385. uni.showToast({ title: res.msg, icon: "none" })
  386. } else {
  387. uni.showToast({ title: res.msg, icon: "none" })
  388. }
  389. } catch (error) {
  390. uni.showToast({ title: t("Msg.Fail"), icon: "none" })
  391. } finally {
  392. loadingStates.value[id] = null
  393. }
  394. }
  395. // 计算卡片数据(保留原方法体)
  396. const calculateCardData = () => {
  397. // 可根据需求实现
  398. }
  399. // 返回活动
  400. const backActivity = () => {
  401. uni.navigateBack()
  402. }
  403. // 获取列表
  404. const searchFunc = async () => {
  405. if (flag.value) return
  406. flag.value = true
  407. pictLoading.value = true
  408. let res = await activityApi.ActivityMonthlyTaskList()
  409. if (res.code == Code.StatusOK) {
  410. tableData.value = res.data
  411. calculateCardData()
  412. // uni.showToast({ title: t("Msg.SearchSuccess"), icon: "success" })
  413. pictLoading.value = false
  414. flag.value = false
  415. } else {
  416. uni.showToast({ title: res.msg, icon: "none" })
  417. pictLoading.value = false
  418. flag.value = false
  419. }
  420. }
  421. // ---------- 生命周期与监听 ----------
  422. onMounted(() => {
  423. searchFunc()
  424. })
  425. // 原 watch(保留)
  426. watch(
  427. () => search.value.type,
  428. () => {
  429. searchFunc()
  430. }
  431. )
  432. // 若需要 uni-app 页面生命周期可额外添加 onShow 等
  433. // onShow(() => {})
  434. </script>
  435. <style lang="scss" scoped>
  436. @import "@/uni.scss";
  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>