index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <template>
  2. <FlexCol>
  3. <FlexCol :gap="20" :padding="[30,30,0,30]" :innerStyle="{
  4. marginTop: '-80px',
  5. backgroundImage: `url(${appConfiguration?.banners.homeTop})`,
  6. backgroundSize: '100% auto',
  7. backgroundRepeat: 'no-repeat',
  8. backgroundPosition: 'top center',
  9. backgroundColor: themeContext.resolveThemeColor('background.primary'),
  10. }">
  11. <FlexCol position="absolute" :left="30" :top="0" :right="30">
  12. <StatusBarSpace />
  13. <FlexRow justify="space-between" align="center">
  14. <Button
  15. @click="showCityPopup = true"
  16. icon="https://xy.wenlvti.net/app_static/images/home/IconSwitch.png"
  17. size="small"
  18. :text="currentCity"
  19. />
  20. <Image
  21. :src="appConfiguration?.banners.homeTitle"
  22. :width="140"
  23. :height="75"
  24. />
  25. <Width :width="150" />
  26. </FlexRow>
  27. </FlexCol>
  28. <Height height="150px" />
  29. <FlexCol :gap="20" align="center">
  30. <FlexRow justify="space-between" align="center" width="100%">
  31. <SearchBar
  32. v-model="searchKeywords"
  33. placeholder="搜索"
  34. :innerStyle="{
  35. backgroundColor: 'white',
  36. borderRadius: '20rpx',
  37. borderWidth: '1px',
  38. borderStyle: 'solid',
  39. borderColor: themeContext.resolveThemeColor('primary'),
  40. width: '630rpx',//490rpx
  41. }"
  42. @search="handleSearch"
  43. />
  44. <!-- <Button icon="ai-thinking" @click="handleGoAI" text="问AI" /> -->
  45. </FlexRow>
  46. <ImageSwiper
  47. :images="appConfiguration?.banners.home.images"
  48. :height="appConfiguration?.banners.home.height"
  49. :width="700"
  50. radius="radius.md"
  51. :innerStyle="{
  52. border: '1px solid #fff',
  53. overflow: 'hidden',
  54. clipPath: 'ellipse(100% 90% at 50% 0%)'
  55. }"
  56. />
  57. </FlexCol>
  58. <LightMap
  59. small
  60. :city="currentCity"
  61. :cityCode="currentCityCode"
  62. v-model:district="currentDistrict"
  63. :lonlat="currentLocation.currentLonlat.value"
  64. @getCurrentLonlat="currentLocation.getCurrentExactLocation"
  65. @selectVillage="handleGoVillageDetails"
  66. @changeCity="showCityPopup=true"
  67. >
  68. <NoticeBar
  69. v-if="currentNoticeContent"
  70. :content="currentNoticeContent"
  71. :innerStyle="{
  72. position: 'absolute',
  73. top: '20rpx',
  74. left: '20rpx',
  75. right: '20rpx',
  76. zIndex: 100,
  77. borderRadius: '30rpx',
  78. }"
  79. :textStyle="{
  80. fontSize: '26rpx',
  81. }"
  82. icon="https://xy.wenlvti.net/app_static/images/home/IconLightActive.png"
  83. :iconProps="{
  84. size: 34,
  85. }"
  86. textColor="#C9211F"
  87. backgroundColor="#D9492E10"
  88. />
  89. </LightMap>
  90. <FlexRow justify="space-between" :padding="[10, 16]" gap="gap.md">
  91. <Button
  92. icon="https://xy.wenlvti.net/app_static/images/home/IconSwitch.png"
  93. radius="radius.lg"
  94. :padding="[10, 30]"
  95. @click="showCityPopup = true"
  96. >
  97. 切换城市
  98. </Button>
  99. <Button
  100. icon="https://xy.wenlvti.net/app_static/images/home/IconFollow.png"
  101. radius="radius.lg" :padding="[10, 30]"
  102. @click="showMyFollowPopup = true"
  103. >
  104. 我的关注
  105. </Button>
  106. <Button
  107. icon="https://xy.wenlvti.net/app_static/images/home/IconLight.png"
  108. radius="radius.lg" :padding="[10, 30]"
  109. @click="handleLightVillage"
  110. >
  111. 点亮村社
  112. </Button>
  113. </FlexRow>
  114. <!-- <HomeTitle title="最新动态" />
  115. <Construction text="测试数据,没有接口!">
  116. <FlexCol gap="gap.lg">
  117. <FlexRow
  118. v-for="item in activityLoader.content.value" :key="item.id"
  119. backgroundColor="background.tertiary"
  120. radius="radius.md"
  121. :padding="[20, 30]"
  122. gap="gap.lg"
  123. align="center"
  124. >
  125. <Avatar
  126. :url="item.head"
  127. :size="80"
  128. :round="false"
  129. :radius="10"
  130. />
  131. <Text :text="item.content" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
  132. </FlexRow>
  133. </FlexCol>
  134. </Construction> -->
  135. <HomeTitle title="乡村排名" showMore @moreClicked="navTo('/pages/home/village/rank/village', {})" />
  136. <VillageRankList :list="villageRankListLoader.content.value ?? []" :jumpToSingle="false" @goDetails="handleGoVillageDetails" />
  137. <HomeTitle title="志愿者排名" showMore :lightCount="3" @moreClicked="navTo('/pages/home/village/rank/volunteer', {})" />
  138. <VillageUserRankList
  139. :list="villageUserRankListLoader.content.value ?? []"
  140. scoreSuffix="积分"
  141. @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
  142. />
  143. <HomeTitle title="精选记忆">
  144. <template #right>
  145. <Touchable
  146. :padding="[15, 20]"
  147. :innerStyle="{ marginRight: '20rpx' }"
  148. direction="row"
  149. center
  150. gap="gap.md"
  151. @click="handleGoPublish()"
  152. >
  153. <Icon name="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png" :size="30" />
  154. <Text text="我也来写" fontConfig="contentText" />
  155. </Touchable>
  156. </template>
  157. </HomeTitle>
  158. <SimplePageListLoader :loader="recommendLoader">
  159. <MasonryGrid>
  160. <MasonryGridItem
  161. v-for="(item, i) in recommendLoader.list.value"
  162. :key="i"
  163. :width="340"
  164. >
  165. <IndexCommonImageItem
  166. :image="item.image"
  167. :title="item.title"
  168. :desc="item.content ?? ''"
  169. :userName="item.villageVolunteerName ?? ''"
  170. :likes="0"
  171. :isLike="false"
  172. @click="handleGoRecommendDetails(item)"
  173. />
  174. </MasonryGridItem>
  175. </MasonryGrid>
  176. </SimplePageListLoader>
  177. </FlexCol>
  178. <Height :height="200" />
  179. <Popup
  180. v-model:show="showCityPopup"
  181. closeable
  182. position="top"
  183. size="60vh"
  184. >
  185. <CitySelect @selectCity="handleSelectCity" />
  186. </Popup>
  187. <Popup
  188. v-model:show="showMyFollowPopup"
  189. closeable
  190. position="bottom"
  191. round
  192. size="80vh"
  193. >
  194. <VillageMyFollow @goDetails="handleGoVillageDetails" />
  195. </Popup>
  196. <PostIndex
  197. ref="postIndexRef"
  198. @goDig="emit('goDig')"
  199. />
  200. <IntroClamTip ref="introClamTipRef" @apply="handleLightVillage" />
  201. </FlexCol>
  202. </template>
  203. <script setup lang="ts">
  204. import { onBeforeMount, onMounted, ref, watch } from 'vue';
  205. import { useTheme } from '@/components/theme/ThemeDefine';
  206. import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
  207. import { useStorageVar } from '@/components/composeabe/StorageVar';
  208. import { useVillageStore } from '@/store/village';
  209. import { useGetCurrentLocation } from './composeabe/GetCurrentLocation';
  210. import { useAuthStore } from '@/store/auth';
  211. import { useRequireLogin } from '@/common/composeabe/RequireLogin';
  212. import { useUserTools } from '@/common/composeabe/UserTools';
  213. import { ArrayUtils, waitTimeOut } from '@imengyu/imengyu-utils';
  214. import { toast } from '@/components/utils/DialogAction';
  215. import { navTo } from '@/components/utils/PageAction';
  216. import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
  217. import Image from '@/components/basic/Image.vue';
  218. import FlexCol from '@/components/layout/FlexCol.vue';
  219. import FlexRow from '@/components/layout/FlexRow.vue';
  220. import Height from '@/components/layout/space/Height.vue';
  221. import SearchBar from '@/components/form/SearchBar.vue';
  222. import Button from '@/components/basic/Button.vue';
  223. import HomeTitle from '@/common/components/parts/HomeTitle.vue';
  224. import VillageRankList from './components/VillageRankList.vue';
  225. import VillageUserRankList from './components/VillageUserRankList.vue';
  226. import Popup from '@/components/dialog/Popup.vue';
  227. import CitySelect from './components/CitySelect.vue';
  228. import VillageMyFollow from './components/VillageMyFollow.vue';
  229. import MapApi from '@/api/map/MapApi';
  230. import LightMap from './components/LightMap.vue';
  231. import NoticeBar from '@/components/display/NoticeBar.vue';
  232. import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
  233. import LightVillageApi, { VillageListItem } from '@/api/light/LightVillageApi';
  234. import type { RegionItem } from '@/api/map/RegionApi';
  235. import Width from '@/components/layout/space/Width.vue';
  236. import MasonryGrid from '@/components/layout/masonry/MasonryGrid.vue';
  237. import MasonryGridItem from '@/components/layout/masonry/MasonryGridItem.vue';
  238. import IndexCommonImageItem from '@/common/components/parts/IndexCommonImageItem.vue';
  239. import IntroClamTip from './village/dialogs/IntroClamTip.vue';
  240. import MemoryTimeOut from '@/components/composeabe/MemoryTimeOut';
  241. import Touchable from '@/components/feedback/Touchable.vue';
  242. import Icon from '@/components/basic/Icon.vue';
  243. import Text from '@/components/basic/Text.vue';
  244. import ImageSwiper from '@/common/components/parts/ImageSwiper.vue';
  245. import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
  246. import VillageInfoApi, { type CommonInfoModel } from '@/api/inhert/VillageInfoApi';
  247. import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
  248. import PostIndex from './village/dialogs/PostIndex.vue';
  249. import TreeApi from '@/api/light/TreeApi';
  250. import { SimpleTimer } from '@/components/utils/Timer';
  251. import { useGetNotice } from './village/composeabe/GetNotice';
  252. const emit = defineEmits(['goVillage','goDig']);
  253. const authStore = useAuthStore();
  254. const villageStore = useVillageStore();
  255. const themeContext = useTheme();
  256. const { requireLogin } = useRequireLogin();
  257. const { getIsVolunteer } = useUserTools();
  258. const searchKeywords = ref('');
  259. const showCityPopup = ref(false);
  260. const showMyFollowPopup = ref(false);
  261. const introClamTipRef = ref();
  262. const introClamTipTimeout = new MemoryTimeOut('IntroClamTip', 1000 * 3600 * 30);//30h
  263. const postIndexRef = ref<InstanceType<typeof PostIndex>>();
  264. const appConfiguration = injectAppConfiguration();
  265. const { value: currentCity } = useStorageVar('currentCityName', '');
  266. const { value: currentCityCode } = useStorageVar('currentCityCode', 0);
  267. const { value: currentDistrict } = useStorageVar('currentDistrict', '');
  268. const currentLocation = useGetCurrentLocation({
  269. onCityChanged: (city, code, district) => {
  270. currentCity.value = city;
  271. currentCityCode.value = code;
  272. currentDistrict.value = district || '';
  273. },
  274. });
  275. const { currentNoticeContent } = useGetNotice();
  276. const villageRankListLoader = useSimpleDataLoader(async () => {
  277. const res = await LightVillageApi.getVillageRankList({ num: 3 });
  278. return res.map((item, i) => ({
  279. image: item.image ?? '',
  280. title: item.name,
  281. rank: i + 1,
  282. id: item.id,
  283. }));
  284. });
  285. const villageUserRankListLoader = useSimpleDataLoader(async () => {
  286. const res = (await LightVillageApi.getVolunteerRankList({ num: 3 }))
  287. .map((item, i) => ({
  288. id: item.id,
  289. image: item.image ?? '',
  290. title: item.name,
  291. rank: i + 1,
  292. score: item.points,
  293. }));
  294. if (res.length >= 3) {
  295. //移动第一名到中间
  296. const first = res[0];
  297. ArrayUtils.removeAt(res, 0);
  298. ArrayUtils.insert(res, 1, first);
  299. }
  300. return res
  301. });
  302. const activityLoader = useSimpleDataLoader(async () => {
  303. return [
  304. {
  305. id: 1,
  306. head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
  307. content: '测试数据,没有接口!:福泽乡里 为全村加成+10%乡源果,可持续24小时',
  308. levelText: '一级',
  309. },
  310. {
  311. id: 2,
  312. head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
  313. content: '福泽乡里 为全村加成+10%乡源果,可持续24小时',
  314. levelText: '五级',
  315. },
  316. {
  317. id: 3,
  318. head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
  319. content: '福泽乡里 为全村加成+10%乡源果,可持续24小时',
  320. levelText: '十级',
  321. },
  322. ];
  323. });
  324. const recommendLoader = useSimplePageListLoader(20, async (page, pageSize) => {
  325. return await VillageInfoApi.getListForDiscover(page, pageSize);
  326. }, true);
  327. function handleGoRecommendDetails(item: CommonInfoModel) {
  328. navTo(`/pages/home/discover/details`, { id: item.id });
  329. }
  330. async function handleGoVillageDetails(item: VillageListItem) {
  331. showMyFollowPopup.value = false;
  332. const details = await LightVillageApi.getVillageDetails(item.id);
  333. villageStore.setCurrentVillage(details);
  334. await waitTimeOut(100);
  335. emit('goVillage')
  336. }
  337. function handleSelectCity(city: RegionItem) {
  338. currentCity.value = city.name;
  339. currentCityCode.value = city.areaCode;
  340. showCityPopup.value = false;
  341. currentLocation.currentLonlat.value = {
  342. longitude: Number(city.longitude),
  343. latitude: Number(city.latitude),
  344. };
  345. }
  346. function handleGoAI() {
  347. requireLogin(async () => {
  348. navTo('/pages/chat/index');
  349. }, '暂时需要登录后才能使用AI助手');
  350. }
  351. async function handleGoPublish() {
  352. requireLogin(async () => {
  353. postIndexRef.value?.show();
  354. }, '欢迎您发布内容,登录后以便使用更多功能哦!');
  355. }
  356. async function handleLightVillage() {
  357. requireLogin(async () => navTo('/pages/home/light/submit-map', {
  358. city: currentCity.value,
  359. cityCode: currentCityCode.value,
  360. district: currentDistrict.value,
  361. }), '登录后才能点亮村社哦!');
  362. }
  363. function handleSearch() {
  364. if (!searchKeywords.value) {
  365. toast('请输入搜索关键词');
  366. return;
  367. }
  368. navTo('/pages/home/search/index', {
  369. city: currentCity.value,
  370. cityCode: currentCityCode.value,
  371. searchValue: searchKeywords.value,
  372. });
  373. }
  374. async function loadInfo() {
  375. //如果不是志愿者,则显示认领村庄提示
  376. const isVolunteer = await getIsVolunteer();
  377. if (!isVolunteer) {
  378. if (introClamTipTimeout.isTimeout()) {
  379. introClamTipRef.value?.show();
  380. introClamTipTimeout.recordTime();
  381. }
  382. }
  383. //加载我的关注村庄
  384. try {
  385. await villageStore.loadMyFollowVillages();
  386. await villageStore.loadMyJoinedVillages();
  387. } catch (error) {
  388. console.error(error);
  389. }
  390. if (villageStore.myFollowVillages.length > 0) {
  391. const currentVillage = villageStore.loadCurrentVillage();
  392. const id = (villageStore.myFollowVillages.find(p => p.id === currentVillage) as VillageListItem || villageStore.myFollowVillages[0]).id;
  393. const res = await LightVillageApi.getVillageDetails(id)
  394. villageStore.setCurrentVillage(res);
  395. }
  396. }
  397. watch(() => authStore.isLogged, async (newVal) => {
  398. if (newVal) {
  399. await loadInfo();
  400. }
  401. });
  402. onMounted(async () => {
  403. try {
  404. if (currentCity.value) {
  405. await currentLocation.setCurrentLocationWithCity(currentCity.value, currentDistrict.value);
  406. } else {
  407. await currentLocation.getCurrentFuzzyLocation();
  408. }
  409. } catch (error) {
  410. console.error(error);
  411. toast('获取当前位置失败,您可以手动选择城市');
  412. }
  413. });
  414. defineExpose({
  415. onPageBack: (name: string, data: Record<string, unknown>) => {
  416. if (name === 'changeCurrentCity') {
  417. currentCity.value = data.city as string;
  418. currentCityCode.value = data.code as number;
  419. currentDistrict.value = '';
  420. currentLocation.currentLonlat.value = {
  421. longitude: Number(data.longitude),
  422. latitude: Number(data.latitude),
  423. };
  424. }
  425. }
  426. })
  427. </script>