tree.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <template>
  2. <FlexCol v-if="currentVillage">
  3. <FlexCol :margin="[10,0,0,0]">
  4. <Text textAlign="center" text="一人添果,全村增光;乡源树茂,故土名扬" fontConfig="primaryTitle" fontSize="35rpx" />
  5. </FlexCol>
  6. <VillageTree
  7. ref="villageTreeRef"
  8. :treeImage="currentVillage.treeImage"
  9. :treeName="currentVillage.treeName"
  10. :treeAnimProps="currentVillage.treeImageAnimProps"
  11. @fruitPick="handlePick"
  12. />
  13. <FlexCol :padding="30">
  14. <FlexCol>
  15. <FlexRow center>
  16. <Progress
  17. :value="treeProgress"
  18. :backgroundStyle="{
  19. background: 'linear-gradient(to bottom, #280502, #a44e17)',
  20. }"
  21. :progressStyle="{
  22. borderImageSource: 'url(https://xy.wenlvti.net/app_static/images/village/ImageProgress.png)',
  23. borderImageSlice: `5 25 5 25 fill`,
  24. borderImageWidth: `0px 15px`,
  25. borderImageRepeat: 'stretch',
  26. borderRadius: '10px',
  27. overflow: 'hidden',
  28. }"
  29. :width="300"
  30. :height="30"
  31. />
  32. </FlexRow>
  33. <Height height="space.lg" />
  34. <FlexRow center gap="gap.md">
  35. <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconLight.png" :size="50" />
  36. <Text :text="`乡源光 ${currentVillage?.lightTotal || 0} ${currentVillage.treeName}`" fontConfig="contentText" fontSize="30rpx" color="#E79412" />
  37. </FlexRow>
  38. <Height height="space.md" />
  39. <Text textAlign="center" :text="`还差 ${treeNextLevelLight} 乡源光即可升级至 ${currentVillage?.nextTreeName || ''}`" fontConfig="secondText" />
  40. </FlexCol>
  41. <Height height="space.xl" />
  42. <FlexRow justify="space-around" :padding="[0, 30]">
  43. <Touchable center direction="column" flexBasis="22%" @click="handlePick">
  44. <Image src="https://xy.wenlvti.net/app_static/images/village/IconCollect.png" :width="130" mode="widthFix" />
  45. <Text text="拾果" fontConfig="contentText" />
  46. </Touchable>
  47. <Touchable center direction="column" flexBasis="22%" @click="handleFertilize">
  48. <Image src="https://xy.wenlvti.net/app_static/images/village/IconFertilization.png" :width="130" mode="widthFix" />
  49. <Text text="施肥" fontConfig="contentText" />
  50. </Touchable>
  51. <Touchable center direction="column" flexBasis="22%" @click="handleWater">
  52. <Image src="https://xy.wenlvti.net/app_static/images/village/IconWatering.png" :width="130" mode="widthFix" />
  53. <Text text="浇水" fontConfig="contentText" textAlign="center" />
  54. </Touchable>
  55. <Touchable center direction="column" flexBasis="22%" @click="handleGoBless">
  56. <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="130" mode="widthFix" />
  57. <Text text="赐福" fontConfig="contentText" textAlign="center" />
  58. </Touchable>
  59. </FlexRow>
  60. <HomeTitle title="最新动态" />
  61. <SimplePageContentLoader :loader="activityLoader" :emptyView="{ text: '冷冷清清,等你来添光加彩' }">
  62. <FlexCol gap="gap.lg">
  63. <FlexRow
  64. v-for="item in activityLoader.content.value" :key="item.id"
  65. backgroundColor="background.tertiary"
  66. radius="radius.md"
  67. :padding="[20, 30]"
  68. gap="gap.lg"
  69. align="center"
  70. >
  71. <Avatar
  72. :url="item.head"
  73. defaultAvatar="https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteer.png"
  74. :size="80"
  75. radius="50%"
  76. />
  77. <FlexCol flex="1">
  78. <Text :text="item.nickname" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
  79. <Text :text="item.content" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
  80. </FlexCol>
  81. <BackgroundBox
  82. backgroundImage="https://xy.wenlvti.net/app_static/images/village/TagNormal.png"
  83. :backgroundCutBorder="[10, 10, 10, 10]"
  84. :backgroundCutBorderSize="[10, 10, 10, 10]"
  85. :padding="[15, 30]"
  86. >
  87. <Text :text="item.levelText" fontConfig="contentText" />
  88. </BackgroundBox>
  89. </FlexRow>
  90. </FlexCol>
  91. </SimplePageContentLoader>
  92. <HomeTitle
  93. title="乡源赐福"
  94. showMore
  95. moreText="我的订单"
  96. @moreClicked="handleGoBlessOrders"
  97. />
  98. <FlexRow wrap>
  99. <Touchable
  100. v-for="(item, index) in blessingInfoLoader.content.value"
  101. :key="item.id"
  102. center
  103. flexBasis="30%"
  104. gap="gap.sm"
  105. direction="column"
  106. :margin="[10,0,0,0]"
  107. @click="handleBuyBless(item)"
  108. >
  109. <BackgroundBox
  110. backgroundImage="https://xy.wenlvti.net/app_static/images/village/ImageBlessingCount.png"
  111. backgroundPosition="center"
  112. :padding="[10, 20]"
  113. >
  114. <Text :text="index + 1" color="white" />
  115. </BackgroundBox>
  116. <Image :src="item.image" :width="210" :height="250" radius="radius.md" mode="aspectFill" />
  117. <BackgroundBox
  118. backgroundImage="https://xy.wenlvti.net/app_static/images/village/ImageBlessingBar.png"
  119. :padding="10"
  120. :width="210"
  121. >
  122. <Text textAlign="center" :text="item.name" fontConfig="contentText" fontFamily="SongtiSCBlack" color="white" />
  123. </BackgroundBox>
  124. </Touchable>
  125. </FlexRow>
  126. </FlexCol>
  127. </FlexCol>
  128. <BlessBuyDialog
  129. ref="blessBuyDialogRef"
  130. :currentBless="currentBless"
  131. @buyBless="handleBuyBlessConfirm"
  132. />
  133. <BlessSuccessDialog
  134. ref="blessSuccessDialogRef"
  135. />
  136. </template>
  137. <script setup lang="ts">
  138. import { computed, onBeforeMount, onMounted, ref, watch } from 'vue';
  139. import { useVillageStore } from '@/store/village';
  140. import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
  141. import { useRequireLogin } from '@/common/composeabe/RequireLogin';
  142. import { showError } from '@/common/composeabe/ErrorDisplay';
  143. import { toast } from '@/components/utils/DialogAction';
  144. import { navTo } from '@/components/utils/PageAction';
  145. import { RequestApiError, SimpleTimer, waitTimeOut } from '@imengyu/imengyu-utils';
  146. import { useAuthStore } from '@/store/auth';
  147. import HomeTitle from '@/common/components/parts/HomeTitle.vue';
  148. import Text from '@/components/basic/Text.vue';
  149. import FlexCol from '@/components/layout/FlexCol.vue';
  150. import VillageTree from '../components/VillageTree.vue';
  151. import FlexRow from '@/components/layout/FlexRow.vue';
  152. import Icon from '@/components/basic/Icon.vue';
  153. import Touchable from '@/components/feedback/Touchable.vue';
  154. import Image from '@/components/basic/Image.vue';
  155. import Height from '@/components/layout/space/Height.vue';
  156. import Avatar from '@/components/display/Avatar.vue';
  157. import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
  158. import Progress from '@/components/display/Progress.vue';
  159. import TreeApi, { type BlessPackageItem, type GrowthLogFeedItem } from '@/api/light/TreeApi';
  160. import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
  161. import BlessBuyDialog from '../dialogs/BlessBuyDialog.vue';
  162. import BlessSuccessDialog from '../dialogs/BlessSuccessDialog.vue';
  163. const GROWTH_FEED_COUNT = 6;
  164. const DEFAULT_AVATAR = 'https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteer.png';
  165. const villageStore = useVillageStore();
  166. const authStore = useAuthStore();
  167. const { requireLoginAsync } = useRequireLogin();
  168. const blessBuyDialogRef = ref<InstanceType<typeof BlessBuyDialog>>();
  169. const blessSuccessDialogRef = ref<InstanceType<typeof BlessSuccessDialog>>();
  170. const villageTreeRef = ref<InstanceType<typeof VillageTree>>();
  171. const currentBless = ref<BlessPackageItem>();
  172. const activityLoader = useSimpleDataLoader(async () => {
  173. const villageId = villageStore.currentVillage?.id;
  174. if (!villageId) return [];
  175. function mapGrowthFeedToInfoItem(feed: GrowthLogFeedItem) {
  176. const { item, logType } = feed;
  177. let content = item.remark?.trim() || '';
  178. let levelText = '';
  179. if (logType === 'task') {
  180. levelText = item.taskName || '任务';
  181. if (!content) {
  182. content = item.taskName ? `完成任务「${item.taskName}」` : '任务动态';
  183. }
  184. } else if (logType === 'fruit') {
  185. levelText = item.typeText || item.type || '乡源果';
  186. if (!content) {
  187. const delta = item.fruit > 0 ? `+${item.fruit}` : `${item.fruit}`;
  188. content = `${levelText} ${delta} 乡源果`;
  189. }
  190. } else {
  191. levelText = item.typeText || item.type || '乡源光';
  192. if (!content) {
  193. const delta = item.light > 0 ? `+${item.light}` : `${item.light}`;
  194. content = `${levelText} ${delta} 乡源光`;
  195. }
  196. }
  197. return {
  198. id: `${logType}-${item.id}`,
  199. head: DEFAULT_AVATAR,
  200. nickname: item.nickname,
  201. content,
  202. levelText,
  203. };
  204. }
  205. const { list } = await TreeApi.getRandomGrowthLogFeed(GROWTH_FEED_COUNT, { villageId });
  206. const res = list.map(mapGrowthFeedToInfoItem);
  207. if (res.length > 0)
  208. return res;
  209. return [
  210. {
  211. id: 'default',
  212. head: DEFAULT_AVATAR,
  213. content: '冷冷清清,等你来添光加彩',
  214. nickname: '',
  215. levelText: '第一条',
  216. }
  217. ];
  218. });
  219. watch(() => villageStore.currentVillage?.id, () => {
  220. activityLoader.reload();
  221. getFruits();
  222. });
  223. const blessingInfoLoader = useSimpleDataLoader(async () => {
  224. const res = await TreeApi.getBlessList({ page: 1, pageSize: 18 });
  225. return res.list;
  226. });
  227. const currentVillage = computed(() => {
  228. return villageStore.currentVillage;
  229. });
  230. const treeProgress = computed(() => {
  231. if (!villageStore.currentVillage)
  232. return 0;
  233. const v = villageStore.currentVillage;
  234. return Math.floor(v.lightTotal - v.treeLight) / (v.nextTreeLight - v.treeLight) * 100;
  235. });
  236. const treeNextLevelLight = computed(() => {
  237. if (!villageStore.currentVillage)
  238. return 0;
  239. const v = villageStore.currentVillage;
  240. return v.nextTreeLight - v.treeLight;
  241. });
  242. function handleBuyBless(bless: BlessPackageItem) {
  243. currentBless.value = bless;
  244. blessBuyDialogRef.value?.show();
  245. }
  246. async function handleGoBlessOrders() {
  247. if (!await requireLoginAsync('登录后查看我的赐福订单'))
  248. return;
  249. navTo('/pages/home/village/bless/my-orders', {
  250. villageId: villageStore.currentVillage?.id
  251. });
  252. }
  253. async function handleBuyBlessConfirm() {
  254. if (!currentBless.value || !villageStore.currentVillage?.id)
  255. return;
  256. if (!await requireLoginAsync('登录后为村社赐福,留下你的大名吧'))
  257. return;
  258. try {
  259. uni.showLoading({
  260. title: '请稍后...',
  261. });
  262. const payInfo = await TreeApi.createBlessOrder(villageStore.currentVillage?.id, currentBless.value.id);
  263. if (payInfo) {
  264. uni.requestPayment({
  265. provider: 'wxpay',
  266. appId: payInfo.pay.appId,
  267. timeStamp: payInfo.pay.timeStamp,
  268. nonceStr: payInfo.pay.nonceStr,
  269. package: payInfo.pay.package,
  270. signType: payInfo.pay.signType,
  271. paySign: payInfo.pay.paySign,
  272. success: () => {
  273. blessSuccessDialogRef.value?.show();
  274. handleBlessPaySuccessRefresh();
  275. },
  276. fail: (err) => {
  277. showError(err);
  278. },
  279. });
  280. }
  281. } catch (error) {
  282. showError(error);
  283. } finally {
  284. uni.hideLoading();
  285. }
  286. }
  287. function handleGoBless() {
  288. uni.pageScrollTo({
  289. scrollTop: 1000,
  290. duration: 300,
  291. });
  292. }
  293. async function handleBlessPaySuccessRefresh() {
  294. refreshVillageTreeInfo();
  295. await waitTimeOut(600);
  296. authStore.refreshUserInfo();
  297. await waitTimeOut(2000);
  298. activityLoader.reload();
  299. }
  300. function handleFertilize() {
  301. handlePickOrWaterOrFertilize('fertilize');
  302. }
  303. function handlePick() {
  304. handlePickOrWaterOrFertilize('pick');
  305. }
  306. function handleWater() {
  307. handlePickOrWaterOrFertilize('water');
  308. }
  309. async function handlePickOrWaterOrFertilize(action: 'pick' | 'water' | 'fertilize') {
  310. if (!villageStore.currentVillage?.id)
  311. return;
  312. switch (action) {
  313. case 'pick':
  314. if (!await requireLoginAsync('登录后就可以为乡源树拾果了'))
  315. return;
  316. break;
  317. case 'water':
  318. if (!await requireLoginAsync('登录后就可以为乡源树浇水了'))
  319. return;
  320. break;
  321. case 'fertilize':
  322. if (!await requireLoginAsync('登录后就可以为乡源树施肥了'))
  323. return;
  324. break;
  325. }
  326. try {
  327. uni.showLoading({
  328. title: '请稍后...',
  329. });
  330. switch (action) {
  331. case 'pick':
  332. const res = await TreeApi.pick(villageStore.currentVillage.id);
  333. villageTreeRef.value?.playStateAnimation('collect');
  334. toast(res);
  335. break;
  336. case 'water':
  337. await TreeApi.water(villageStore.currentVillage.id);
  338. villageTreeRef.value?.playStateAnimation('water');
  339. toast('浇水成功!感谢您的贡献');
  340. break;
  341. case 'fertilize':
  342. await TreeApi.fertilize(villageStore.currentVillage.id);
  343. villageTreeRef.value?.playStateAnimation('fertilize');
  344. toast('施肥成功!感谢您的贡献');
  345. break;
  346. }
  347. refreshVillageTreeInfo();
  348. uni.hideLoading();
  349. } catch (e) {
  350. uni.hideLoading();
  351. if (e instanceof RequestApiError && typeof e.data === 'string') {
  352. toast(e.data);
  353. return;
  354. }
  355. showError(e);
  356. }
  357. }
  358. async function getFruits() {
  359. if (!villageStore.currentVillage?.id)
  360. return;
  361. const res = await TreeApi.dropFruit(villageStore.currentVillage.id);
  362. if (res) {
  363. villageTreeRef.value?.createFruits(res.fruitRemain);
  364. }
  365. }
  366. async function refreshVillageTreeInfo() {
  367. await villageStore.reloadVillageInfo();
  368. }
  369. const refreshFruitTimer = new SimpleTimer(undefined, () => getFruits(), 15000);
  370. onMounted(() => {
  371. refreshFruitTimer.start();
  372. });
  373. onBeforeMount(() => {
  374. refreshFruitTimer.stop();
  375. });
  376. defineExpose({
  377. onPageBack: (name: string, data: Record<string, unknown>) => {
  378. if (name === 'blessPaySuccessRefresh') {
  379. handleBlessPaySuccessRefresh();
  380. }
  381. },
  382. });
  383. </script>