monthly-list.vue 22 KB

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