| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- <template>
- <FlexCol>
- <FlexCol :gap="20" :padding="[30,30,0,30]" :innerStyle="{
- marginTop: '-80px',
- backgroundImage: `url(${appConfiguration?.banners.homeTop})`,
- backgroundSize: '100% auto',
- backgroundRepeat: 'no-repeat',
- backgroundPosition: 'top center',
- backgroundColor: themeContext.resolveThemeColor('background.primary'),
- }">
- <FlexCol position="absolute" :left="30" :top="0" :right="30">
- <StatusBarSpace />
- <FlexRow justify="space-between" align="flex-start">
- <Button
- @click="showCityPopup = true"
- icon="https://xy.wenlvti.net/app_static/images/home/IconSwitch.png"
- size="medium"
- :radius="40"
- :text="currentCity"
- />
- <Image
- :src="appConfiguration?.banners.homeTitle.url"
- :width="appConfiguration?.banners.homeTitle.width || 140"
- :height="appConfiguration?.banners.homeTitle.height || 75"
- :innerStyle="appConfiguration?.banners.homeTitle.innerStyle"
- />
- <Width :width="150" />
- </FlexRow>
- </FlexCol>
- <SimpleTransition name="fade" :show="currentScrollTop > 1000" :duration="300">
- <template #show="{ classNames }">
- <view
- :class="['top-nav-fade ',...classNames]"
- :style="{ backgroundColor: themeContext.resolveThemeColor('background.primary') }"
- :left="0" :top="0" :right="0" :zIndex="200"
- >
- <StatusBarSpace />
- </view>
- </template>
- </SimpleTransition>
- <Height height="80px" />
- <FlexCol :gap="20" align="center">
- <ImageSwiper
- :images="bannerLoader.content.value || []"
- :height="appConfiguration?.banners.home.height"
- :width="700"
- radius="radius.md"
- :innerStyle="{
- border: '1px solid #fff',
- overflow: 'hidden',
- clipPath: 'ellipse(100% 90% at 50% 0%)'
- }"
- />
- </FlexCol>
- <FlexRow justify="space-between" align="center" width="100%">
- <SearchBar
- v-model="searchKeywords"
- placeholder="搜索村社"
- :innerStyle="{
- backgroundColor: 'white',
- borderRadius: '20rpx',
- borderWidth: '1px',
- borderStyle: 'solid',
- borderColor: themeContext.resolveThemeColor('primary'),
- width: '630rpx',//490rpx
- }"
- @search="handleSearch"
- />
- <!-- <Button icon="ai-thinking" @click="handleGoAI" text="问AI" /> -->
- </FlexRow>
- <HomeTitle title="今日推荐" showMore @moreClicked="emit('goDiscover')" />
- <SimplePageContentLoader :loader="recommendLoader">
- <FlexRow gap="gap.lg" align="stretch" justify="space-between">
- <Touchable
- v-for="item in recommendLoader.content.value"
- :key="item.id"
- position="relative"
- radius="radius.md"
- shadow="default"
- gap="gap.lg"
- direction="row"
- align="center"
- overflow="hidden"
- border="1px solid #fff"
- :flexShrink="0"
- width="48%"
- @click="handleGoRecommendDetails(item)"
- >
- <Image :src="item.image" :width="400" :height="270" />
- <FlexCol position="absolute" :inset="{ b: -1, l: -1, r: -1 }" backgroundColor="background.primary" padding="padding.md" gap="gap.md">
- <Text fontConfig="contentSpeicalText" fontFamily="SongtiSCBlack" :lines="1" :wrap="false">{{ item.title }}</Text>
- <Text fontSize="fontSize.sm" :text="item.desc" :lines="1" :maxWidth="200" />
- </FlexCol>
- </Touchable>
- </FlexRow>
- </SimplePageContentLoader>
- <Height :height="10" />
- <LightMap
- small
- :city="currentCity"
- :cityCode="currentCityCode"
- :district="currentDistrict"
- :lonlat="currentLocation.currentLonlat.value"
- @getCurrentLonlat="currentLocation.getCurrentExactLocation"
- @mapClick="handleGoMap"
- >
- <NoticeBar
- v-if="currentNoticeContent"
- :content="currentNoticeContent"
- :innerStyle="{
- position: 'absolute',
- top: '20rpx',
- left: '20rpx',
- right: '20rpx',
- zIndex: 100,
- borderRadius: '30rpx',
- }"
- :textStyle="{
- fontSize: '26rpx',
- }"
- icon="https://xy.wenlvti.net/app_static/images/home/IconLightActive.png"
- :iconProps="{
- size: 34,
- }"
- textColor="#C9211F"
- backgroundColor="#D9492E10"
- />
- </LightMap>
- <FlexRow justify="space-between" :padding="[10, 16]" gap="gap.md">
- <Button
- icon="https://xy.wenlvti.net/app_static/images/home/IconSwitch.png"
- radius="radius.lg"
- :padding="[10, 30]"
- text="切换城市"
- :textProps="{ wrap: false }"
- @click="showCityPopup = true"
- />
- <Button
- icon="https://xy.wenlvti.net/app_static/images/home/IconFollow.png"
- radius="radius.lg" :padding="[10, 30]"
- @click="showMyFollowPopup = true"
- text="我的关注"
- :textProps="{ wrap: false }"
- />
- <BubbleTip
- v-model:show="showLightTip"
- content="没有加入村社?点击这里点亮村落"
- position="bottom"
- crossPosition="right"
- :arrowOffsetX="130"
- @contentClick="handleLightVillage"
- @close="handleCloseLightTip"
- >
- <Button
- icon="https://xy.wenlvti.net/app_static/images/home/IconLight.png"
- radius="radius.lg" :padding="[10, 30]"
- :textProps="{ wrap: false }"
- text="点亮村社"
- @click="handleLightVillage"
- />
- </BubbleTip>
- </FlexRow>
- <HomeTitle title="今日共建任务" showMore @moreClicked="navTo('/pages/home/village/task/index')" />
- <SimplePageContentLoader :loader="taskListLoader">
- <scroll-view scroll-x>
- <FlexRow gap="gap.lg" align="stretch">
- <Touchable
- v-for="item in taskListLoader.content.value"
- :key="item.id"
- backgroundColor="white"
- radius="radius.md"
- padding="padding.md"
- shadow="default"
- gap="gap.lg"
- direction="row"
- align="center"
- :flexShrink="0"
- :innerStyle="{ minWidth: '400rpx' }"
- @click="handleDoTask(item)"
- >
- <Icon :icon="item.image || 'space'" :size="100" />
- <FlexCol gap="gap.md">
- <Text fontConfig="contentSpeicalText" fontFamily="SongtiSCBlack">{{ item.name }}</Text>
- <Text fontSize="fontSize.sm" :text="item.desc" :lines="1" :maxWidth="400" />
- <Text v-if="item.points > 0" fontConfig="secondText">+{{ item.points }}文化积分</Text>
- <FlexRow>
- <Button v-if="item.rewardFruit > 0" icon="https://xy.wenlvti.net/app_static/images/village/IconFruit.png" size="mini" :text="`+${item.rewardFruit}乡源果`" />
- <Button v-if="item.rewardLight > 0" icon="https://xy.wenlvti.net/app_static/images/village/IconLight.png" size="mini" :text="`+${item.rewardLight}乡源光`" />
- <Button v-if="item.rewardWater > 0" icon="https://xy.wenlvti.net/app_static/images/village/IconWatering.png" size="mini" :text="`+${item.rewardWater}浇水次数`" />
- </FlexRow>
- </FlexCol>
- </Touchable>
- </FlexRow>
- </scroll-view>
- </SimplePageContentLoader>
- <HomeTitle title="最新动态" />
- <SimplePageContentLoader :loader="noticeListLoader">
- <FlexCol gap="gap.lg">
- <FlexRow
- v-for="item in noticeListLoader.content.value?.slice(0, 3) || [] "
- :key="item.item.id"
- backgroundColor="background.tertiary"
- radius="radius.md"
- :padding="[20, 30]"
- gap="gap.lg"
- align="center"
- >
- <Avatar
- :src="(item.item.avatar as string)"
- :size="80"
- :round="false"
- :radius="10"
- />
- <FlexCol>
- <Text :text="item.item.title" fontConfig="contentText" />
- <Text
- :text="`来自${item.item.villageName}`"
- fontConfig="secondText"
- />
- </FlexCol>
- </FlexRow>
- </FlexCol>
- </SimplePageContentLoader>
- <HomeTitle title="乡村排名" showMore @moreClicked="navTo('/pages/home/village/rank/village', {})" />
- <VillageRankList :list="villageRankListLoader.content.value ?? []" :jumpToSingle="false" @goDetails="handleGoVillageDetails" />
- <HomeTitle title="志愿者排名" showMore :lightCount="3" @moreClicked="navTo('/pages/home/village/rank/volunteer', {})" />
- <VillageUserRankList
- :list="villageUserRankListLoader.content.value ?? []"
- scoreSuffix="积分"
- @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
- />
- </FlexCol>
- <view class="recommmend-header" :style="stickyStyle">
- <FlexCol :padding="[0,30]">
- <HomeTitle title="精选记忆">
- <template #right>
- <FrameButton
- size="small"
- icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
- text="我也来写"
- @click="handleGoPublish()"
- />
- </template>
- </HomeTitle>
- </FlexCol>
- <Tabs
- v-model:currentIndex="recommendTabCurrent"
- :tabs="recommendTabs"
- />
- </view>
- <FlexCol :padding="30">
- <RecommendTab v-if="recommendTabCurrent === 0" @goDetails="handleGoRecommendDetails" />
- <GoodsTab v-else-if="recommendTabCurrent === 1" @goDetails="handleGoRecommendDetails" />
- <TravelTab v-else-if="recommendTabCurrent === 2" @goDetails="handleGoRecommendDetails" />
- <CulturalTab v-else-if="recommendTabCurrent === 3" @goDetails="handleGoRecommendDetails" />
- </FlexCol>
- <Height :height="200" />
- <Popup
- v-model:show="showCityPopup"
- closeable
- position="top"
- size="60vh"
- >
- <CitySelect @selectCity="handleSelectCity" />
- </Popup>
- <Popup
- v-model:show="showMyFollowPopup"
- closeable
- position="bottom"
- round
- size="80vh"
- >
- <VillageMyFollow @goDetails="handleGoVillageDetails" />
- </Popup>
- <PostIndex
- ref="postIndexRef"
- @goDig="emit('goDig')"
- />
- <IntroClamTip ref="introClamTipRef" @apply="handleLightVillage" />
- </FlexCol>
- </template>
- <script setup lang="ts">
- import { onMounted, ref, watch } from 'vue';
- import { useTheme } from '@/components/theme/ThemeDefine';
- import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
- import { useStorageVar } from '@/components/composeabe/StorageVar';
- import { useVillageStore } from '@/store/village';
- import { useGetCurrentLocation } from './composeabe/GetCurrentLocation';
- import { useAuthStore } from '@/store/auth';
- import { useRequireLogin } from '@/common/composeabe/RequireLogin';
- import { useUserTools } from '@/common/composeabe/UserTools';
- import { useGetNotice } from './village/composeabe/GetNotice';
- import { waitTimeOut } from '@imengyu/imengyu-utils';
- import { toast, loading, hideLoading } from '@/components/utils/DialogAction';
- import { navTo } from '@/components/utils/PageAction';
- import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
- import { DynamicColor, statusBarHeight } from '@/components/theme/ThemeTools';
- import Image from '@/components/basic/Image.vue';
- import FlexCol from '@/components/layout/FlexCol.vue';
- import FlexRow from '@/components/layout/FlexRow.vue';
- import Height from '@/components/layout/space/Height.vue';
- import SearchBar from '@/components/form/SearchBar.vue';
- import Button from '@/components/basic/Button.vue';
- import HomeTitle from '@/common/components/parts/HomeTitle.vue';
- import VillageRankList from './components/VillageRankList.vue';
- import VillageUserRankList from './components/VillageUserRankList.vue';
- import Popup from '@/components/dialog/Popup.vue';
- import CitySelect from './components/CitySelect.vue';
- import VillageMyFollow from './components/VillageMyFollow.vue';
- import LightMap from './components/LightMap.vue';
- import NoticeBar from '@/components/display/NoticeBar.vue';
- import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
- import LightVillageApi, { VillageListItem, type VillageMapItem } from '@/api/light/LightVillageApi';
- import Width from '@/components/layout/space/Width.vue';
- import IntroClamTip from './village/dialogs/IntroClamTip.vue';
- import MemoryTimeOut from '@/components/composeabe/MemoryTimeOut';
- import ImageSwiper from '@/common/components/parts/ImageSwiper.vue';
- import VillageInfoApi, { type CommonInfoModel } from '@/api/inhert/VillageInfoApi';
- import PostIndex from './village/dialogs/PostIndex.vue';
- import FrameButton from '@/common/components/FrameButton.vue';
- import type { RegionItem } from '@/api/map/RegionApi';
- import BubbleTip from '@/components/feedback/BubbleTip.vue';
- import BannerApi from '@/api/system/Banner';
- import Tabs from '@/components/nav/Tabs.vue';
- import SimpleTransition from '@/components/anim/SimpleTransition.vue';
- import RecommendTab from './recommend/RecommendTab.vue';
- import GoodsTab from './recommend/GoodsTab.vue';
- import TravelTab from './recommend/TravelTab.vue';
- import CulturalTab from './recommend/CulturalTab.vue';
- import Avatar from '@/components/display/Avatar.vue';
- import Text from '@/components/basic/Text.vue';
- import TreeApi from '@/api/light/TreeApi';
- import Touchable from '@/components/feedback/Touchable.vue';
- import Icon from '@/components/basic/Icon.vue';
- import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
- import { useTaskActions } from './village/composeabe/TaskActions';
- const props = defineProps({
- currentScrollTop: {
- type: Number,
- default: 0,
- },
- });
- const emit = defineEmits(['goVillage','goDig','goDiscover']);
- const authStore = useAuthStore();
- const villageStore = useVillageStore();
- const themeContext = useTheme();
- const { requireLogin } = useRequireLogin();
- const { getIsVolunteer } = useUserTools();
- const searchKeywords = ref('');
- const showCityPopup = ref(false);
- const showMyFollowPopup = ref(false);
- const introClamTipRef = ref();
- const introClamTipTimeout = new MemoryTimeOut('IntroClamTip', 1000 * 3600 * 30);//30h
- const postIndexRef = ref<InstanceType<typeof PostIndex>>();
- const appConfiguration = injectAppConfiguration();
- const { value: currentCity } = useStorageVar('currentCityName', '');
- const { value: currentCityCode } = useStorageVar('currentCityCode', 0);
- const { value: currentDistrict } = useStorageVar('currentDistrict', '');
- const currentLocation = useGetCurrentLocation({
- onCityChanged: (city, code, district) => {
- currentCity.value = city;
- currentCityCode.value = code;
- currentDistrict.value = district || '';
- },
- });
- const { currentNoticeContent, noticeListLoader } = useGetNotice();
- const bannerLoader = useSimpleDataLoader(async () => {
- return (await BannerApi.getBannerList({
- keywords: '亮乡源大众版首页头部',
- })).map(p => ({
- image: p.image,
- url: p.url,
- }));
- });
- const villageRankListLoader = useSimpleDataLoader(async () => {
- const res = await LightVillageApi.getVillageRankList({ num: 3 });
- return res.map((item, i) => ({
- image: item.image ?? '',
- title: item.name,
- rank: i + 1,
- id: item.id,
- }));
- });
- const villageUserRankListLoader = useSimpleDataLoader(async () => {
- const res = (await LightVillageApi.getVolunteerRankList({ num: 3 }))
- .map((item, i) => ({
- id: item.id,
- image: item.avatar ?? '',
- title: item.name,
- rank: i + 1,
- score: item.points,
- }));
- return res
- });
- const taskListLoader = useSimpleDataLoader(async () => {
- return (await TreeApi.getTaskList({
- page: 1,
- pageSize: 5,
- })).list;
- });
- const recommendLoader = useSimpleDataLoader(async () => {
- return (await VillageInfoApi.getListForDiscover({ page: 1, pageSize: 2 })).list;
- });
- const showLightTipTimeout = new MemoryTimeOut('LightTip', 1000 * 3600 * 2);//2h
- const showLightTip = ref(false);
- function handleShowLightTip() {
- if (showLightTipTimeout.isTimeout())
- showLightTip.value = true;
- }
- function handleCloseLightTip() {
- showLightTipTimeout.recordTime();
- }
- const { handleDoTask } = useTaskActions(() => villageStore.currentVillage?.id || 0, true);
- const recommendTabCurrent = ref(0);
- const recommendTabs = ref([
- {
- text: '推荐',
- value: 0,
- },
- {
- text: '好物',
- value: 1,
- },
- {
- text: '乡游',
- value: 2,
- },
- {
- text: '文化',
- value: 3,
- },
- ]);
- const stickyStyle = themeContext.useThemeStyle({
- position: 'sticky',
- top: (statusBarHeight - 10) + 'px',
- left: 0,
- right: 0,
- zIndex: '100',
- backgroundColor: DynamicColor('background.primary'),
- });
- function handleGoRecommendDetails(item: CommonInfoModel) {
- navTo(`/pages/home/discover/details`, { id: item.id });
- }
- async function handleGoVillageDetails(item: VillageListItem) {
- showMyFollowPopup.value = false;
- const details = await LightVillageApi.getVillageDetails(item.id);
- villageStore.setCurrentVillage(details);
- await waitTimeOut(100);
- emit('goVillage')
- }
- function handleSelectCity(city: RegionItem) {
- currentCity.value = city.name;
- currentCityCode.value = city.areaCode;
- showCityPopup.value = false;
- currentLocation.currentLonlat.value = {
- longitude: Number(city.longitude),
- latitude: Number(city.latitude),
- };
- }
- function handleGoAI() {
- requireLogin(async () => {
- navTo('/pages/chat/index');
- }, '暂时需要登录后才能使用AI助手');
- }
- async function handleGoPublish() {
- requireLogin(async () => {
- postIndexRef.value?.show();
- }, '欢迎您发布内容,登录后以便使用更多功能哦!');
- }
- async function handleLightVillage() {
- requireLogin(async () => navTo('/pages/home/light/create-village'), '登录后才能点亮村社哦!');
- }
- function handleGoMap() {
- navTo('/pages/home/light/map', {
- city: currentCity.value,
- cityCode: currentCityCode.value,
- district: currentDistrict.value,
- });
- }
- function handleSearch() {
- if (!searchKeywords.value) {
- toast('请输入搜索关键词');
- return;
- }
- navTo('/pages/home/search/index', {
- city: currentCity.value,
- cityCode: currentCityCode.value,
- searchValue: searchKeywords.value,
- });
- }
- async function loadInfo() {
- loading('载入数据中');
- //如果不是志愿者,则显示认领村庄提示
- const isVolunteer = await getIsVolunteer();
- if (!isVolunteer) {
- if (introClamTipTimeout.isTimeout()) {
- introClamTipRef.value?.show();
- introClamTipTimeout.recordTime();
- }
- }
- //加载我的关注村庄
- try {
- await villageStore.loadMyFollowVillages();
- } catch (error) {
- console.error(error);
- }
- try {
- await villageStore.loadMyJoinedVillages();
- } catch (error) {
- console.error(error);
- }
- handleShowLightTip();
- try {
- if (!villageStore.currentVillage && villageStore.myFollowVillages.length > 0) {
- const currentVillage = villageStore.loadCurrentVillage();
- const id = (villageStore.myFollowVillages.find(p => p.id === currentVillage) as VillageListItem || villageStore.myFollowVillages[0]).id;
- const res = await LightVillageApi.getVillageDetails(id)
- villageStore.setCurrentVillage(res);
- }
- } finally {
- hideLoading();
- }
- }
- watch(() => authStore.isLogged, async (newVal) => {
- if (newVal) {
- await loadInfo();
- }
- }, { immediate: true });
- onMounted(async () => {
- try {
- if (currentCity.value) {
- await currentLocation.setCurrentLocationWithCity(currentCity.value, currentDistrict.value);
- } else {
- await currentLocation.getCurrentFuzzyLocation();
- }
- } catch (error) {
- console.error(error);
- toast('获取当前位置失败,您可以手动选择城市');
- }
- });
- defineExpose({
- goMap() {
- handleGoMap();
- },
- onPageBack: (name: string, data: Record<string, unknown>) => {
- if (name === 'changeCurrentCity') {
- currentCity.value = data.city as string;
- currentCityCode.value = data.code as number;
- currentDistrict.value = '';
- currentLocation.currentLonlat.value = {
- longitude: Number(data.longitude),
- latitude: Number(data.latitude),
- };
- }
- }
- })
- </script>
- <style lang="scss">
- .top-nav-fade {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 200;
- transition: opacity 0.3s ease-in-out;
- &.fade-enter-active,
- &.fade-leave-active {
- opacity: 1;
- }
- &.fade-enter-from,
- &.fade-leave-to {
- opacity: 0;
- }
- }
- </style>
|