浏览代码

📦 村社首页整理

快乐的梦鱼 1 天之前
父节点
当前提交
de58d8d67e

+ 0 - 31
AI功能相关需要修改.md

@@ -1,31 +0,0 @@
-* 建村简介需要加接口,临时保存已生成过的AI简介(由前端上传):
-  * 保存简介:输入 area_code, desc
-  * 获取简介: 输入 area_code ,查询已有 desc
-    
-* 增加乡源果兑换ai对话次数功能
-
-  ![](https://xy.wenlvti.net/uploads/20260713/da53bc047f4cfd1a096046ebb9bce924.png)
-
-  提供给小程序 接口 **同升级/升级管理员** 需要有获取套餐和下单接口 ,不同的是购买成功的兑现需要调用 [AI接入微服务](https://update-server1.imengyu.top/api-docs) 增加余额:
-
-```php
-//订单支付成功后调用接口为指定用户增加余额
-$ch = curl_init("https://update-server1.imengyu.top/content/agent/cost-settings/external/add-ballance");
-curl_setopt($ch, CURLOPT_POST, true);
-curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json', 'Content-Type: application/json', 'Authorization: {"apiKey":"i7JRrzFrrCjcTQ87tTeh2Mte3rRMB8QM","apiSecret":"JKkESyfRNQFiwdmJT7ydse4rSZfYePYAhcMjdBZk5EZCDzbd7rYdT6XQjPmQXPiR"}']);
-curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
-  'externalUserId' => 1,//乡源用户ID
-  'ballance' => 1000 //增加的TOKEN数, 平均一轮对话1000token左右
-]));
-curl_exec($ch);
-curl_close($ch);
-```
-
-  * [加余额接口文档](https://update-server1.imengyu.top/api-docs#tag/%E5%86%85%E5%AE%B9aiai%E5%AF%B9%E8%AF%9D/POST/content/agent/cost-settings/external/add-ballance)
-  * [获取余额文档(若需要查询)](https://update-server1.imengyu.top/api-docs#tag/%E5%86%85%E5%AE%B9aiai%E5%AF%B9%E8%AF%9D/GET/content/agent/cost-settings/external/get-cost-settings)
-
-套餐建议设置:
-1. token: 10000 约10次对话 20乡源果
-2. token: 20000 约20次对话 30乡源果
-3. token: 50000 约50次对话 68乡源果
-4. token: 100000 约100次对话 99乡源果

+ 1 - 1
src/components/layout/BaseView.ts

@@ -6,10 +6,10 @@ import { ObjectUtils } from "@imengyu/imengyu-utils";
 
 export function useBaseViewStyleBuilder(props: FlexProps) {
   
-  const styleObj : Record<string, any> = {}; 
 
   const themeContext = useTheme();
   const commonStyle = computed(() => {
+    const styleObj : Record<string, any> = {}; 
     styleObj.display = 'flex';
     styleObj.flexDirection = props.direction;
     styleObj.flexBasis = props.flexBasis;

+ 3 - 3
src/pages/home/components/VillageUserRankList.vue

@@ -90,9 +90,9 @@ const first3 = computed(() => {
     }
   }
   //移动第一名到中间
-  const first = list[0];
-  ArrayUtils.removeAt(list, 0);
-  ArrayUtils.insert(list, 1, first);
+  const first = list.shift();
+  list.splice(1, 0, first!);
+
   return list;
 })
 

+ 71 - 228
src/pages/home/village/introd/card.vue

@@ -112,51 +112,44 @@
                 />
 
               </FlexCol>
-              <template v-else-if="node.type === 'Card:Level'">
-
-                <!-- 状态与申请 -->
-                <FlexRow 
-                  backgroundColor="background.tertiary" 
-                  radius="radius.md" 
-                  padding="space.md" 
-                  justify="space-between" 
-                >
-                  <FlexRow align="center">
-                    <FlexCol gap="gap.md"> 
-                      <FlexRow gap="gap.lg" align="center">
-                        <Icon v-if="villageInfoLoader.content.value?.vipLevel === 4" name="https://xy.wenlvti.net/app_static/images/village/IconGov.png" :size="70" />
-                        <Text :text="`${villageInfoLoader.content.value?.vipLevelText || '默认级别'}`" fontConfig="secondText" />  
-                      </FlexRow>
-                      <Text v-if="villageInfoLoader.content.value?.sizeLimit" :text="`存储空间:${villageInfoLoader.content.value?.sizeText || ''}`" :fontSize="22" fontConfig="secondText" />
-                      <Text v-else text="无限存储空间" :fontSize="22" fontConfig="secondText" />
-                    </FlexCol>
-                  </FlexRow>
-                  <FlexRow align="center" gap="gap.md">
-                    <IconButton icon="help-filling" @click="navTo('/pages/article/details', { id: 7021, modelId: 18, showRecommend: false })" />
-                    <Button 
-                      v-if="(villageInfoLoader.content.value?.vipLevel || 0) < 4" 
-                      icon="https://xy.wenlvti.net/app_static/images/village/IconUser.png" 
-                      radius="radius.lgr" 
-                      :padding="[10, 30]" 
-                      backgroundColor="white" 
-                      text="升级村社"
-                      @click="upgradeRef?.show()"
-                    />
-                  </FlexRow>
+              <!-- 状态与申请 -->
+              <FlexRow 
+                v-else-if="node.type === 'Card:Level'"
+                backgroundColor="background.tertiary" 
+                radius="radius.md" 
+                padding="space.md" 
+                justify="space-between" 
+              >
+                <FlexRow align="center">
+                  <FlexCol gap="gap.md"> 
+                    <FlexRow gap="gap.lg" align="center">
+                      <Icon v-if="villageInfoLoader.content.value?.vipLevel === 4" name="https://xy.wenlvti.net/app_static/images/village/IconGov.png" :size="70" />
+                      <Text :text="`${villageInfoLoader.content.value?.vipLevelText || '默认级别'}`" fontConfig="secondText" />  
+                    </FlexRow>
+                    <Text v-if="villageInfoLoader.content.value?.sizeLimit" :text="`存储空间:${villageInfoLoader.content.value?.sizeText || ''}`" :fontSize="22" fontConfig="secondText" />
+                    <Text v-else text="无限存储空间" :fontSize="22" fontConfig="secondText" />
+                  </FlexCol>
                 </FlexRow>
-
-              </template>
-              <template v-else-if="node.type === 'Card:Gallery'">
-                
-                <!-- 图片 -->
-                <VillageGallery
-                  v-if="villageInfoLoader.content.value"
-                  ref="villageGalleryRef"
-                  :villageId="villageStore.currentVillage?.id ?? 0" 
-                  @goGallery="handleGoGallery"
-                />
-
-              </template>
+                <FlexRow align="center" gap="gap.md">
+                  <IconButton icon="help-filling" @click="navTo('/pages/article/details', { id: 7021, modelId: 18, showRecommend: false })" />
+                  <Button 
+                    v-if="(villageInfoLoader.content.value?.vipLevel || 0) < 4" 
+                    icon="https://xy.wenlvti.net/app_static/images/village/IconUser.png" 
+                    radius="radius.lgr" 
+                    :padding="[10, 30]" 
+                    backgroundColor="white" 
+                    text="升级村社"
+                    @click="upgradeRef?.show()"
+                  />
+                </FlexRow>
+              </FlexRow>
+              <!-- 图片相册 -->
+              <VillageGallery
+                v-else-if="node.type === 'Card:Gallery' && villageInfoLoader.content.value"
+                ref="villageGalleryRef"
+                :villageId="villageStore.currentVillage?.id ?? 0" 
+                @goGallery="handleGoGallery"
+              />
               <template v-else-if="node.type === 'Card:AddressAndMap'">
                 
                 <!-- 地图 -->
@@ -235,124 +228,30 @@
                 </FlexRow>
 
               </template>
-              <template v-else-if="node.type === 'Block:Rank'">
-
-                <!-- 排行榜 -->
-                <HomeTitle 
-                  :title="node.props?.title" 
-                  showMore 
-                  @moreClicked="navTo('/pages/home/village/rank/volunteer', {
-                    villageId: villageStore.currentVillage?.id ?? undefined,
-                  })" 
-                />
-                <Height height="gap.md" />
-                <VillageUserRankList 
-                  :list="villageUserRankListLoader.content.value ?? []" 
-                  scoreSuffix="文化积分"
-                  @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
-                />
-
-              </template>
-              <template v-else-if="node.type === 'Block:Collect'">
-
-                <!-- 魅力乡源 -->
-                <HomeTitle :title="node.props?.title">
-                  <template #right>
-                    <FlexRow align="center" gap="gap.md">
-                      <FrameButton
-                        text="管理" 
-                        size="small" 
-                        primary
-                        @click="handleGoDigManage()"
-                      />
-                      <FrameButton
-                        text="共编村史" 
-                        icon="https://xy.wenlvti.net/app_static/images/village/IconHistory.png"
-                        size="small"
-                        @click="handleGoCollect()"
-                      />
-                      <Width :size="15" />
-                    </FlexRow>
-                  </template>
-                </HomeTitle>
-                <Height height="gap.md" />
-                <Grid :borderGrid="false" :mainAxisCount="4">
-                  <GridItem title="村社概况" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeIntrod.png" touchable @click="handleGoCollect(11, '村社概况')" />
-                  <GridItem title="自然风光" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeEnvirounment.png" touchable @click="handleGoCollect(13, '自然风光')" />
-                  <GridItem title="历史沿革" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png" touchable @click="handleGoCollect(2, '历史沿革')" />
-                  <GridItem title="特色产业" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeIndustry.png" touchable @click="handleGoCollect(9, '特色产业'  )"    />
-                  <GridItem title="文艺活动" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeActivity.png" touchable @click="handleGoCollect(12, '文艺活动')" />
-                  <GridItem title="非遗展示" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeShow.png" touchable @click="handleGoCollect(10, '非遗展示')" />
-                  <GridItem title="民俗风采" icon="https://xy.wenlvti.net/app_static/images/village/IconLargeFolkloreVibe.png" touchable @click="handleGoCollect(8, '民俗风采')" />
-                  <GridItem title="历史人物" icon="https://xy.wenlvti.net/app_static/images/village/IconGoods.png" touchable @click="handleGoCollect(7, '历史人物')" />
-                </Grid>
-
-              </template>
-              <template v-else-if="node.type === 'Block:Games'">
-
-                <!-- 活力乡源 -->
-                <HomeTitle :title="node.props?.title" />
-                <Height height="gap.md" />
-                <Grid :borderGrid="false" :mainAxisCount="4">
-                  <GridItem 
-                    v-for="item in node.props?.items" 
-                    :key="item.title"
-                    :title="item.title"
-                    :icon="item.icon"
-                    touchable 
-                    @click="() => context.getScriptEngine().execute(item.handler, node.getNodeScriptContext?.())" 
-                  />
-                </Grid>
-
-              </template>
-              <template v-else-if="node.type === 'Block:Official'">
-
-                <!-- 文脉乡源 -->
-                <FlexCol>
-                  <FlexCol gap="gap.md" padding="padding.lg">
-                    <HomeTitle :title="node.props?.title" :showTopMargin="false">
-                      <template #right>
-                        <FlexRow align="center" gap="gap.md">
-                          <BubbleTip
-                            v-model:show="showManageTip"
-                            position="bottom"
-                            crossPosition="right"
-                            arrowOffsetX="180rpx"
-                            content="村社贴图太乱?点这里整理"
-                            @contentClick="handleCloseManageTip(true)"
-                            @close="handleCloseManageTip(false)"
-                          >
-                            <FrameButton
-                              text="管理" 
-                              size="small" 
-                              primary
-                              @click="handleGoOfficalManage()"
-                            />
-                          </BubbleTip>
-                          <FrameButton 
-                            text="乡源AI帮你写" 
-                            size="small" 
-                            icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
-                            @click="handleGoPublish()" 
-                          />
-                        </FlexRow>
-                      </template>
-                    </HomeTitle>
-                    <Height height="gap.md" />
-                    <RoundTags 
-                      v-model:active="listActiveTag" 
-                      :tags="tagsLoader.content.value || []" 
-                    />
-                  </FlexCol>
-                  <OfficialAccountPublishWrap
-                    :topic="recommendTagName" 
-                    :path="recommendPath"
-                    @publishsuccess="onPublishSuccess"
-                    @empty="isOfficialEmpty=true"
-                  />
-                </FlexCol>
-
-              </template>
+              <!-- 排行榜 -->
+              <Rank 
+                v-else-if="node.type === 'Block:Rank'" 
+                :title="node.props?.title"
+                :villageId="villageStore.currentVillage?.id ?? 0"
+              />
+              <!-- 4网格 -->
+              <GridButtonBlock4 
+                v-else-if="node.type === 'Block:GridButtonBlock4'"
+                :title="node.props?.title"
+                :titleRightButtons="node.props?.titleRightButtons"
+                :items="node.props?.items"
+                @click="(script) => context.getScriptEngine().execute(script, node.getNodeScriptContext?.())"
+              />
+              <!-- 文脉乡源 -->
+              <OfficialAccountPublish 
+                ref="officialAccountPublishRef"
+                v-else-if="node.type === 'Block:Official'"
+                :villageId="villageStore.currentVillage?.id ?? 0"
+                :villageTitle="villageStore.currentVillage?.name ?? ''"
+                :title="node.props?.title"
+                :tags="tagsLoader.content.value || []"
+                @goOfficalManage="handleGoOfficalManage"
+              />
             </template>
           </DynamicXNodeChild>
         </template>
@@ -419,7 +318,6 @@
 <script setup lang="ts">
 import { computed, inject, reactive, ref, watch, type Ref } from 'vue';
 import { useUserTools } from '@/common/composeabe/UserTools';
-import { useOfficialAccount } from '../../composeabe/OfficialAccount';
 import { useAuthStore } from '@/store/auth';
 import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import { useVillageStore } from '@/store/village';
@@ -432,7 +330,6 @@ import { alert, confirm } from '@/components/dialog/CommonRoot';
 import { isDevEnv } from '@/common/config/AppCofig';
 import { defaultTags } from './data/DefaultTag';
 import DefaultCard from './data/DefaultCard.json';
-import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
@@ -441,13 +338,7 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import VillageMiniMap from '../../components/VillageMiniMap.vue';
 import Divider from '@/components/display/Divider.vue';
-import RoundTags from '@/common/components/parts/RoundTags.vue';
-import VillageUserRankList from '../../components/VillageUserRankList.vue';
 import ProvideVar from '@/components/theme/ProvideVar.vue';
-import Grid from '@/components/layout/grid/Grid.vue';
-import GridItem from '@/components/layout/grid/GridItem.vue';
-import LightVillageApi from '@/api/light/LightVillageApi';
-import OfficialAccountPublishWrap from '@/common/components/OfficialAccountPublishWrap.vue';
 import UpgradeDialog from '../dialogs/UpgradeDialog.vue';
 import VillageGallery from '../components/VillageGallery.vue';
 import WxButton from '@/components/basic/WxButton.vue';
@@ -472,6 +363,9 @@ import DynamicXNodeChild from '@/components/dynamic/x/DynamicXNodeChild.vue';
 import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
 import HomeLargeTitle from '@/common/components/parts/HomeLargeTitle.vue';
 import SkinApi from '@/api/light/SkinApi';
+import OfficialAccountPublish from './card/blocks/OfficialAccountPublish.vue';
+import Rank from './card/blocks/Rank.vue';
+import GridButtonBlock4 from './card/blocks/GridButtonBlock4.vue';
 
 const emit = defineEmits<{
   (e: 'goTree'): void;
@@ -479,19 +373,11 @@ const emit = defineEmits<{
 }>();
 
 const authStore = useAuthStore();
+const villageStore = useVillageStore();
 const { getIsVolunteer } = useUserTools();
 const { requireLogin } = useRequireLogin();
-const { onPublishSuccess, makeOfficialPublishLinkPathAndParams } = useOfficialAccount(() => {
-  return {
-    villageId: villageStore.currentVillage?.id ?? 0,
-    userId: authStore.userId,
-    tag: recommendTagName.value,
-  };
-});
-const villageStore = useVillageStore();
 const { isFollowed, onFollow, onUnFollow } = useFollow();
 const { getIsJoinedVillage } = useUserTools();
-const isOfficialEmpty = ref(false);
 const isJoined = ref(false);
 const villageInfoLoader = useSimpleDataLoader(async () => {
   const village = villageStore.currentVillage;  
@@ -503,10 +389,9 @@ const villageInfoLoader = useSimpleDataLoader(async () => {
     isJoined.value = false;
   }
   handleShowFollowTip();
-  isOfficialEmpty.value = false;
   showUnLight.value = !villageStore.currentVillage?.isLight;
   setTimeout(() => {
-    handleManageFollowTip();
+    officialAccountPublishRef.value?.handleManageFollowTip();
   }, 3000);
   return {
     title: village?.name || '',
@@ -541,22 +426,8 @@ const isStaff = computed(() => staffInfo.value?.type === 'staff');
 const shareQRDialog = ref<InstanceType<typeof ShareQRDialog>>();
 
 const descOpen = ref(false);
-const listActiveTag = ref('广场');
 
-const villageUserRankListLoader = useSimpleDataLoader(async () => {
-  const res = (await LightVillageApi.getVolunteerRankList({ 
-    num: 3,
-    village_id: villageStore.currentVillage?.id ?? undefined,
-  }))
-    .map((item, i) => ({
-      id: item.id,
-      image: item.avatar ?? '',
-      title: item.name,
-      rank: i + 1,
-      score: item.points,
-    }));
-  return res
-});
+
 
 const showFollowTipTimeout = new MemoryTimeOut('FollowTip', 1000 * 3600);//1h
 const showFollowTip = ref(false);
@@ -571,23 +442,6 @@ function handleCloseFollowTip() {
 }
 
 const showUnLight = ref(false);
-const showManageTipTimeout = new MemoryTimeOut('VillageManageTip', 1000 * 3600 * 20);//30h
-const showManageTip = ref(false);
-
-function handleManageFollowTip() {
-  if (showManageTipTimeout.isTimeout() && !isOfficialEmpty.value)
-    showManageTip.value = true;
-}
-function handleCloseManageTip(goComp: boolean) {
-  showManageTipTimeout.recordTime();
-}
-
-const recommendTagName = computed(() => {
-  return '亮乡源·' + villageInfoLoader.content.value?.title + '·' + listActiveTag.value;
-});
-const recommendPath = computed(() => {
-  return makeOfficialPublishLinkPathAndParams('/pages/index');
-});
 
 function handleGoJoin() {
   if (isJoined.value)
@@ -615,14 +469,6 @@ function handleGoCollect(collectModuleId?: number, title?: string) {
     title: title,
   });
 }
-function handleGoPublish() {
-  requireLogin(async () => {
-    navTo('/pages/home/chat/dependent/post/publish', {
-      tag: recommendTagName.value,
-      villageId: villageStore.currentVillage?.id ?? undefined,
-    });
-  }, '登录后以便发布文章使用更多功能哦!');
-}
 function handleGoGallery() {
   navTo('/pages/home/village/gallery/index', { 
     villageId: villageStore.currentVillage?.id ?? 0 ,
@@ -672,13 +518,13 @@ async function checkIsStaff(needTopic?: boolean) {
   return true;
 }
 
-async function handleGoOfficalManage() {
+async function handleGoOfficalManage(recommendTagName: string) {
   assertNotNull(villageStore.currentVillage?.id)
   if (!await checkIsStaff(true)) 
     return;
   navTo('/pages/home/village/post/management-list', {
     villageId: villageStore.currentVillage.id,
-    topic: recommendTagName.value,
+    topic: recommendTagName,
   });
 }
 async function handleGoEdit() {
@@ -704,6 +550,7 @@ const { currentNoticeContent, noticeListLoader } = useGetNotice(() => villageSto
 const upgradeRef = ref<InstanceType<typeof UpgradeDialog>>();
 const villageGalleryRef = ref<InstanceType<typeof VillageGallery>>();
 const changeNickRef = ref<InstanceType<typeof ChangeNickDialog>>();
+const officialAccountPublishRef = ref<InstanceType<typeof OfficialAccountPublish>>();
 
 const pageScriptGlobal = reactive({
   handleGoGallery,
@@ -741,8 +588,6 @@ watch(() => villageStore.currentVillage, async () => {
   await waitTimeOut(100);
   await villageInfoLoader.reload();
   await waitTimeOut(100);
-  await villageUserRankListLoader.reload();
-  await waitTimeOut(100);
   await noticeListLoader.reload();
 }, { immediate: true });
 
@@ -751,9 +596,7 @@ defineExpose({
     if (name === 'refreshVillageGrallery') {
       villageGalleryRef.value?.refresh();
     } else if (name === 'refreshOfficialAccount') {
-      const tag = listActiveTag.value;
-      listActiveTag.value = '';
-      setTimeout(() => listActiveTag.value = tag, 100);
+      officialAccountPublishRef.value?.refresh();
     } else if (name === 'showVillageUpgrade') {
       upgradeRef.value?.show();
     } else if (name === 'backShowVillageJoin') {

+ 7 - 0
src/pages/home/village/introd/card/blocks/ContentMap.vue

@@ -0,0 +1,7 @@
+<template>
+
+</template>
+
+<script setup lang="ts">
+
+</script>

+ 59 - 0
src/pages/home/village/introd/card/blocks/GridButtonBlock4.vue

@@ -0,0 +1,59 @@
+<template>
+  <!-- 标题+4网格按钮+带图标按钮 -->
+  <HomeTitle :title="title">
+    <template v-if="titleRightButtons" #right>
+      <FlexRow align="center" gap="gap.md">
+        <FrameButton
+          v-for="item in titleRightButtons"
+          :key="item.text"
+          :text="item.text" 
+          :size="item.size || 'small'" 
+          :primary="item.primary || false"
+          @click="() => emit('click', item.handler)"
+        />
+        <Width :size="15" />
+      </FlexRow>
+    </template>
+  </HomeTitle>
+  <Height height="gap.md" />
+  <Grid :borderGrid="false" :mainAxisCount="4">
+    <GridItem 
+      v-for="item in items" 
+      :key="item.title"
+      :title="item.title"
+      :icon="item.icon"
+      touchable 
+      @click="() => emit('click', item.handler)" 
+    />
+  </Grid>
+</template>
+
+<script setup lang="ts">
+import FrameButton from '@/common/components/FrameButton.vue';
+import HomeTitle from '@/common/components/parts/HomeTitle.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Grid from '@/components/layout/grid/Grid.vue';
+import GridItem from '@/components/layout/grid/GridItem.vue';
+import Height from '@/components/layout/space/Height.vue';
+import Width from '@/components/layout/space/Width.vue';
+
+const props = defineProps<{
+  title: string,
+  titleRightButtons?: {
+    handler: string,
+    text?: string,
+    icon?: string,
+    primary?: boolean,
+    size?: "large" | "medium" | "small",
+  }[],
+  items: {
+    title: string,
+    icon: string,
+    handler: string,
+  }[],
+}>()
+
+const emit = defineEmits<{
+  (e: 'click', script: string): void
+}>()
+</script>

+ 127 - 0
src/pages/home/village/introd/card/blocks/OfficialAccountPublish.vue

@@ -0,0 +1,127 @@
+<template>
+  <!-- 文脉乡源 -->
+  <FlexCol>
+    <FlexCol gap="gap.md" padding="padding.lg">
+      <HomeTitle :title="title" :showTopMargin="false">
+        <template #right>
+          <FlexRow align="center" gap="gap.md">
+            <BubbleTip
+              v-model:show="showManageTip"
+              position="bottom"
+              crossPosition="right"
+              arrowOffsetX="180rpx"
+              content="村社贴图太乱?点这里整理"
+              @contentClick="handleCloseManageTip(true)"
+              @close="handleCloseManageTip(false)"
+            >
+              <FrameButton
+                text="管理" 
+                size="small" 
+                primary
+                @click="emit('goOfficalManage', recommendTagName)"
+              />
+            </BubbleTip>
+            <FrameButton 
+              text="乡源AI帮你写" 
+              size="small" 
+              icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
+              @click="handleGoPublish()" 
+            />
+          </FlexRow>
+        </template>
+      </HomeTitle>
+      <Height height="gap.md" />
+      <RoundTags 
+        v-model:active="listActiveTag" 
+        :tags="tags" 
+      />
+    </FlexCol>
+    <OfficialAccountPublishWrap
+      :topic="recommendTagName" 
+      :path="recommendPath"
+      @publishsuccess="onPublishSuccess"
+      @empty="isOfficialEmpty=true"
+    />
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { useRequireLogin } from '@/common/composeabe/RequireLogin';
+import { useOfficialAccount } from '@/pages/home/composeabe/OfficialAccount';
+import { useAuthStore } from '@/store/auth';
+import { useVillageStore } from '@/store/village';
+import { navTo } from '@/components/utils/PageAction';
+import OfficialAccountPublishWrap from '@/common/components/OfficialAccountPublishWrap.vue';
+import HomeTitle from '@/common/components/parts/HomeTitle.vue';
+import RoundTags from '@/common/components/parts/RoundTags.vue';
+import BubbleTip from '@/components/feedback/BubbleTip.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Height from '@/components/layout/space/Height.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import MemoryTimeOut from '@/components/composeabe/MemoryTimeOut';
+
+const props = defineProps<{
+  villageId: number;
+  villageTitle: string;
+  title: string;
+  tags: string[];
+}>();
+
+const emit = defineEmits<{
+  (e: 'goOfficalManage', recommendTagName: string): void;
+}>();
+
+const authStore = useAuthStore();
+const villageStore = useVillageStore();
+
+const isOfficialEmpty = ref(false);
+
+const { requireLogin } = useRequireLogin();
+const { onPublishSuccess, makeOfficialPublishLinkPathAndParams } = useOfficialAccount(() => {
+  return {
+    villageId: villageStore.currentVillage?.id ?? 0,
+    userId: authStore.userId,
+    tag: recommendTagName.value,
+  };
+});
+
+const showManageTipTimeout = new MemoryTimeOut('VillageManageTip', 1000 * 3600 * 20);//30h
+const showManageTip = ref(false);
+
+const listActiveTag = ref('广场');
+const recommendTagName = computed(() => {
+  return '亮乡源·' + props.villageTitle + '·' + listActiveTag.value;
+});
+const recommendPath = computed(() => {
+  return makeOfficialPublishLinkPathAndParams('/pages/index');
+});
+
+
+function handleManageFollowTip() {
+  if (showManageTipTimeout.isTimeout() && !isOfficialEmpty.value)
+    showManageTip.value = true;
+}
+function handleCloseManageTip(goComp: boolean) {
+  showManageTipTimeout.recordTime();
+}
+function handleGoPublish() {
+  requireLogin(async () => {
+    navTo('/pages/home/chat/dependent/post/publish', {
+      tag: recommendTagName.value,
+      villageId: villageStore.currentVillage?.id ?? undefined,
+    });
+  }, '登录后以便发布文章使用更多功能哦!');
+}
+
+defineExpose({
+  handleManageFollowTip,
+  handleCloseManageTip,
+  refresh() {
+    const tag = listActiveTag.value;
+    listActiveTag.value = '';
+    setTimeout(() => listActiveTag.value = tag, 100);
+  },
+});
+</script>

+ 7 - 0
src/pages/home/village/introd/card/blocks/Rank copy 4.vue

@@ -0,0 +1,7 @@
+<template>
+
+</template>
+
+<script setup lang="ts">
+
+</script>

+ 51 - 0
src/pages/home/village/introd/card/blocks/Rank.vue

@@ -0,0 +1,51 @@
+<template>
+  <!-- 排行榜 -->
+  <HomeTitle 
+    :title="title" 
+    showMore 
+    @moreClicked="navTo('/pages/home/village/rank/volunteer', {
+      villageId: villageStore.currentVillage?.id ?? undefined,
+    })" 
+  />
+  <Height height="gap.md" />
+  <VillageUserRankList 
+    :list="villageUserRankListLoader.content.value ?? []" 
+    scoreSuffix="文化积分"
+    @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
+  />
+</template>
+
+<script setup lang="ts">
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { navTo } from '@/components/utils/PageAction';
+import { useVillageStore } from '@/store/village';
+import { watch } from 'vue';
+import VillageUserRankList from '@/pages/home/components/VillageUserRankList.vue';
+import LightVillageApi from '@/api/light/LightVillageApi';
+import HomeTitle from '@/common/components/parts/HomeTitle.vue';
+import Height from '@/components/layout/space/Height.vue';
+
+const props = defineProps<{
+  title: string;
+  villageId: number;
+}>();
+const villageStore = useVillageStore();
+const villageUserRankListLoader = useSimpleDataLoader(async () => {
+  const res = (await LightVillageApi.getVolunteerRankList({ 
+    num: 3,
+    village_id: villageStore.currentVillage?.id ?? undefined,
+  }))
+    .map((item, i) => ({
+      id: item.id,
+      image: item.avatar ?? '',
+      title: item.name,
+      rank: i + 1,
+      score: item.points,
+    }));
+  return res
+});
+
+watch(() => props.villageId, () => {
+  villageUserRankListLoader.reload();
+});
+</script>

+ 60 - 4
src/pages/home/village/introd/data/DefaultCard.json

@@ -12,7 +12,7 @@
         "gap": "gap.lg"
       },
       "nodes": [
-         {
+        {
           "type": "flex",
           "props": {
             "direction": "column",
@@ -93,14 +93,70 @@
             },
             {
               "name": "VILLAGE:CONTENT:Collect",
-              "type": "Block:Collect",
+              "type": "Block:GridButtonBlock4",
               "props": {
-                "title": "魅力乡源"
+                "title": "魅力乡源",
+                "titleRightButtons": [
+                  {
+                    "text": "管理",
+                    "size": "small",
+                    "primary": true,
+                    "handler": "globalContext.handleGoDigManage()"
+                  },
+                  {
+                    "text": "共编村史",
+                    "size": "small",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconHistory.png",
+                    "handler": "globalContext.handleGoCollect()"
+                  }
+                ],
+                "items": [
+                  {
+                    "title": "村社概况",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIntrod.png",
+                    "handler": "globalContext.handleGoCollect(11, '村社概况')"
+                  },
+                  {
+                    "title": "自然风光",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeEnvirounment.png",
+                    "handler": "globalContext.handleGoCollect(13, '自然风光')"
+                  },
+                  {
+                    "title": "历史沿革",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png",
+                    "handler": "globalContext.handleGoCollect(2, '历史沿革')"
+                  },
+                  {
+                    "title": "特色产业",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIndustry.png",
+                    "handler": "globalContext.handleGoCollect(9, '特色产业')"
+                  },
+                  {
+                    "title": "文艺活动",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeActivity.png",
+                    "handler": "globalContext.handleGoCollect(12, '文艺活动')"
+                  },
+                  {
+                    "title": "非遗展示",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeShow.png",
+                    "handler": "globalContext.handleGoCollect(10, '非遗展示')"
+                  },
+                  {
+                    "title": "民俗风采",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeFolkloreVibe.png",
+                    "handler": "globalContext.handleGoCollect(8, '民俗风采')"
+                  },
+                  {
+                    "title": "历史人物",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconGoods.png",
+                    "handler": "globalContext.handleGoCollect(7, '历史人物')"
+                  }
+                ]
               }
             },
             {
               "name": "VILLAGE:CONTENT:Games",
-              "type": "Block:Games",
+              "type": "Block:GridButtonBlock4",
               "props": {
                 "title": "活力乡源",
                 "items": [

+ 59 - 3
src/pages/home/village/introd/data/TestCard.json

@@ -110,14 +110,70 @@
         },
         {
           "name": "VILLAGE:CONTENT:Collect",
-          "type": "Block:Collect",
+          "type": "Block:GridButtonBlock4",
           "props": {
-            "title": "魅力乡源"
+            "title": "魅力乡源",
+            "titleRightButtons": [
+              {
+                "text": "管理",
+                "size": "small",
+                "primary": true,
+                "handler": "globalContext.handleGoDigManage()"
+              },
+              {
+                "text": "共编村史",
+                "size": "small",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconHistory.png",
+                "handler": "globalContext.handleGoCollect()"
+              }
+            ],
+            "items": [
+              {
+                "title": "村社概况",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIntrod.png",
+                "handler": "globalContext.handleGoCollect(11, '村社概况')"
+              },
+              {
+                "title": "自然风光",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeEnvirounment.png",
+                "handler": "globalContext.handleGoCollect(13, '自然风光')"
+              },
+              {
+                "title": "历史沿革",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png",
+                "handler": "globalContext.handleGoCollect(2, '历史沿革')"
+              },
+              {
+                "title": "特色产业",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIndustry.png",
+                "handler": "globalContext.handleGoCollect(9, '特色产业')"
+              },
+              {
+                "title": "文艺活动",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeActivity.png",
+                "handler": "globalContext.handleGoCollect(12, '文艺活动')"
+              },
+              {
+                "title": "非遗展示",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeShow.png",
+                "handler": "globalContext.handleGoCollect(10, '非遗展示')"
+              },
+              {
+                "title": "民俗风采",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeFolkloreVibe.png",
+                "handler": "globalContext.handleGoCollect(8, '民俗风采')"
+              },
+              {
+                "title": "历史人物",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconGoods.png",
+                "handler": "globalContext.handleGoCollect(7, '历史人物')"
+              }
+            ]
           }
         },
         {
           "name": "VILLAGE:CONTENT:Games",
-          "type": "Block:Games",
+          "type": "Block:GridButtonBlock4",
           "props": {
             "title": "活力乡源",
             "items": [

+ 0 - 550
村社名片换肤等功能需增加接口.md

@@ -1,550 +0,0 @@
-# 村社名片换肤等功能需增加接口
-
-> 本文档仅供后端参阅,仅列出接口名称和输入输出参数,不涉及数据库设计。本文档的接口URL为建议值,实际实现时可根据情况调整。
-
----
-
-## 功能界面参考
-
-![image](https://server.imengyu.top/uploads/media/A6RCfcbXkkPm.jpg)
-
-![image](https://server.imengyu.top/uploads/media/MjihNKtTirSn.jpg)
-
-![image](https://server.imengyu.top/uploads/media/n3zJsKD7nJXF.jpg)
-
-![image](https://server.imengyu.top/uploads/media/MinsPZNi8nG5.jpg)
-
-## 一、皮肤模块
-
-> **提示**:皮肤需要能在后台管理系统中进行增删改查(含皮肤图片上传、主题JSON配置等)。
-
-### 1.1 获取皮肤列表(皮肤中心)
-
-**用途**:分页获取所有可购买的皮肤列表(皮肤中心页面)。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/list` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| page | int | 否 | 页码,默认1 |
-| pageSize | int | 否 | 每页数量,默认20 |
-
-**输出**:
-
-```json
-{
-  "total": 10,
-  "list": [
-    {
-      "id": 1,
-      "title": "默认皮肤",
-      "desc": "默认的村社名片",
-      "image": "https://xxx/thumb.jpg",
-      "images": [ ... ],
-      "price": 0
-    }
-  ]
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| total | int | 总数 |
-| list[].id | int | 皮肤ID |
-| list[].title | string | 标题 |
-| list[].desc | string | 描述 |
-| list[].image | string | 封面缩略图URL |
-| list[].images | string[] | 皮肤预览图列表(多张可左右滑动) |
-| list[].price | int | 价格(乡源果),0=免费 |
-
----
-
-### 1.2 获取推荐/热门皮肤
-
-**用途**:主页换肤页顶部"热门皮肤"展示,仅展示少量推荐皮肤。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/recommend` |
-
-**输入参数**:无
-
-**输出**:
-
-```json
-{
-  "list": [
-    {
-      "id": 1,
-      "title": "默认皮肤",
-      "desc": "默认的村社名片",
-      "price": 99,
-      "image": "https://xxx/thumb.jpg",
-      "images": [ ... ]
-    }
-  ]
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| list[].id | int | 皮肤ID |
-| list[].title | string | 标题 |
-| list[].desc | string | 描述 |
-| list[].image | string | 封面缩略图URL |
-| list[].images | string[] | 皮肤预览图列表(多张可左右滑动) |
-
----
-
-### 1.3 获取皮肤详情
-
-**用途**:皮肤详情页,展示皮肤预览图和购买/使用按钮。需判断当前用户是否已购买。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/detail` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| id | int | 是 | 皮肤ID |
-| villageId | int | 是 | 村社ID(用于判断是否已购买) |
-
-**输出**:
-
-```json
-{
-  "id": 1,
-  "title": "默认皮肤",
-  "desc": "默认的村社名片",
-  "image": "https://xxx/thumb.jpg",
-  "images": [
-    "https://xxx/preview1.jpg",
-    "https://xxx/preview2.jpg"
-  ],
-  "price": 99,
-  "isBought": false
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| id | int | 皮肤ID |
-| title | string | 标题 |
-| desc | string | 描述 |
-| image | string | 封面缩略图URL |
-| images | string[] | 皮肤预览图列表(多张可左右滑动) |
-| price | int | 价格(乡源果) |
-| isBought | bool | 当前用户在当前村社是否已购买该皮肤 |
-
----
-
-### 1.4 购买皮肤
-
-**用途**:下单购买皮肤,返回订单信息和微信支付参数。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/buy` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| skinId | int | 是 | 皮肤ID |
-| villageId | int | 是 | 村社ID |
-
-**输出**:
-
-```json
-{
-  "order": {
-    "id": 100,
-    "orderNo": "SK202501010001",
-    "skinId": 1,
-    "villageId": 10,
-    "price": 99,
-    "status": 0,
-    "createtime": "2025-01-01 12:00:00"
-  },
-  "pay": {
-    "appId": "wxXXX",
-    "timeStamp": "1704096000",
-    "nonceStr": "abc123",
-    "package": "prepay_id=xxx",
-    "signType": "MD5",
-    "paySign": "signature"
-  }
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| order.id | int | 订单ID |
-| order.orderNo | string | 订单号 |
-| order.price | int | 支付金额 |
-| order.status | int | 状态:0=待支付,1=已支付 |
-| pay | object | 微信支付参数(若price=0则为null,直接购买成功) |
-
----
-
-### 1.5 使用皮肤
-
-**用途**:将已购买的皮肤应用到指定村社。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/apply` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| skinId | int | 是 | 皮肤ID |
-| villageId | int | 是 | 村社ID |
-
-**输出**:无特殊数据。
-
----
-
-### 1.6 获取我的已购皮肤
-
-**用途**:获取当前用户在指定村社下已购买的皮肤列表。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/myPurchased` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-
-**输出**:
-
-```json
-{
-  "list": [
-    {
-      "id": 1,
-      "title": "默认皮肤",
-      "desc": "默认的村社名片",
-      "image": "https://xxx/thumb.jpg",
-      "price": 0
-    }
-  ]
-}
-```
-
----
-
-### 1.7 获取皮肤订单列表
-
-**用途**:分页获取皮肤购买订单记录。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/orderList` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| page | int | 否 | 页码,默认1 |
-| pageSize | int | 否 | 每页数量,默认20 |
-| villageId | int | 否 | 村社ID(筛选) |
-| userId | int | 否 | 用户ID(筛选) |
-
-**输出**:
-
-```json
-{
-  "total": 5,
-  "list": [
-    {
-      "id": 100,
-      "skinTitle": "古风主题",
-      "villageName": "大庆村",
-      "price": 99,
-      "status": 1,
-      "statusText": "已支付",
-      "createtime": "2025-01-01 12:00:00"
-    }
-  ]
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| total | int | 总数 |
-| list[].id | int | 订单ID |
-| list[].skinTitle | string | 皮肤标题 |
-| list[].villageName | string | 村社名称 |
-| list[].price | int | 金额 |
-| list[].status | int | 状态 |
-| list[].statusText | string | 状态文本 |
-| list[].createtime | string | 创建时间 |
-
----
-
-### 1.8 获取村社当前皮肤数据
-
-**用途**:获取村社当前使用的名片主题/皮肤完整数据(JSON 结构)。
-
-> 该接口在进入村社名片主页时调用。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/skin/current` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-
-**输出**:返回一个 DynamicXPage JSON 对象,结构参考 `src/pages/home/village/introd/data/DefaultCard.json`。该 JSON 描述名片页面的布局、组件类型、颜色、按钮行为等。示例如下:
-
-```json
-{
-  "name": "VILLAGE",
-  "type": "page",
-  "props": {},
-  "nodes": [
-    {
-      "name": "VILLAGE:CONTENT",
-      "type": "flex",
-      "props": {
-        "direction": "column",
-        "padding": [30, 30, 0, 30],
-        "gap": "gap.lg"
-      },
-      "nodes": [
-        {
-          "type": "BackgroundBox",
-          "props": {
-            "color1": "#eecaa0",
-            "color2": "white",
-            "color2Position": "85%",
-            "radius": "radius.lg",
-            "direction": "column",
-            "padding": [35, 30],
-            "gap": "gap.lg"
-          },
-          "nodes": [
-            { "type": "Card:Basic" },
-            { "type": "Card:Level" },
-            { "type": "Card:Gallery" },
-            { "type": "Card:AddressAndMap" },
-            {
-              "type": "flex",
-              "props": { "direction": "row", "justify": "center", "gap": "gap.md" },
-              "nodes": [
-                { "type": "button", "props": { "text": "村社相册" }, "events": { "click": "globalContext.handleGoGallery()" } },
-                { "type": "button", "props": { "text": "编辑简介" }, "events": { "click": "globalContext.handleGoEdit()" } },
-                { "type": "button", "props": { "text": "主页换肤" }, "events": { "click": "globalContext.handleGoSkin()" } }
-              ]
-            },
-            { "type": "Card:Static" }
-          ]
-        },
-        { "type": "Block:Rank", "props": { "title": "排行榜" } },
-        { "type": "Block:Collect", "props": { "title": "魅力乡源" } },
-        { "type": "Block:Games", "props": { "title": "活力乡源", "items": [...] } },
-        { "type": "Block:Official", "props": { "title": "文脉乡源" } }
-      ]
-    }
-  ]
-}
-```
-
-> 每个皮肤需要存储一份这样的 JSON,村社使用某皮肤时即返回该皮肤对应的 JSON。后台管理系统创建/编辑皮肤时需要能编辑此 JSON 配置。
-
----
-
-## 二、管理主页栏目显示与顺序
-
-### 2.1 获取村社栏目配置
-
-**用途**:获取村社名片主页的栏目配置(栏目显示/隐藏、排列顺序)。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/column/getConfig` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-
-**输出**:返回栏目配置 JSON。具体结构由前端确定,每项包含栏目标识,栏目数据和顺序,后端只需要保存JSON即可。
-
-**与采集栏目关联**:待后端 采集栏目 相关接口设计完成后,此处下方 Block:Collect 与后端 采集栏目 管理并合并成最终列表。
-
-```json
-{
-  "columns": [
-    { "type": "Card:Basic", "visible": true, "sort": 1 },
-    { "type": "Card:Level", "visible": true, "sort": 2 },
-    { "type": "Card:Gallery", "visible": true, "sort": 3 },
-    { "type": "Block:Rank", "visible": true, "sort": 4 },
-    { 
-      "type": "Block:Collect", "visible": true, "sort": 5,
-      "props": { 
-        "title": "文脉乡源",
-        "items": [ ... ]
-      }
-    },
-    { 
-      "type": "Block:Games", "visible": true, "sort": 6,
-      "props": { 
-        "title": "魅力乡源" ,
-        "items": [ ... ]
-      } 
-    },
-    { 
-      "type": "Block:Official", "visible": true, "sort": 7,
-      "props": { 
-        "title": "文脉乡源" 
-      }
-    }
-  ]
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| * | ? | 由前端确定 |
-
----
-
-### 2.2 保存村社栏目配置
-
-**用途**:保存编辑后的村社名片栏目配置。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/column/saveConfig` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-| data | json | 是 | 栏目配置 JSON,结构同 2.1 输出 |
-
-**输出**:无特殊数据。
-
----
-
-## 三、管理贴图话题列表
-
-### 3.1 获取村社话题列表
-
-**用途**:获取村社贴图话题列表(用于话题标签展示和管理)。若未配置则使用默认话题。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/topic/getTopicTags` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-
-**输出**:
-
-```json
-{
-  "tags": [
-    { "title": "广场", "icon": "", "disabled": false, "sort": 1 },
-    { "title": "美食", "icon": "", "disabled": false, "sort": 2 },
-    { "title": "美景", "icon": "", "disabled": false, "sort": 3 }
-  ]
-}
-```
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| tags[].title | string | 话题名称 |
-| tags[].icon | string | 话题图标URL(可空) |
-| tags[].disabled | bool | 是否禁用 |
-| tags[].sort | int | 排序序号 |
-
-> 若村社未配置话题,返回空数组,前端将使用默认话题列表:`['广场', '美食', '美景', '故事', '老技艺', '老物件']`
-
----
-
-### 3.2 保存村社话题列表
-
-**用途**:保存编辑后的村社贴图话题列表(含排序和新增/删除)。
-
-| 项目 | 说明 |
-|------|------|
-| **方法** | `POST` |
-| **路径** | `/village/topic/saveTopicTags` |
-
-**输入参数**:
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| villageId | int | 是 | 村社ID |
-| tags | array | 是 | 话题标签数组,结构同 3.1 输出 |
-
-**输出**:无特殊数据。
-
----
-
-## 四、接口汇总清单
-
-| 序号 | 模块 | 接口名称 | 方法 | 路径 |
-|------|------|----------|------|------|
-| 1.1 | 皮肤 | 获取皮肤列表 | POST | `/village/skin/list` |
-| 1.2 | 皮肤 | 获取推荐皮肤 | POST | `/village/skin/recommend` |
-| 1.3 | 皮肤 | 获取皮肤详情 | POST | `/village/skin/detail` |
-| 1.4 | 皮肤 | 购买皮肤 | POST | `/village/skin/buy` |
-| 1.5 | 皮肤 | 使用皮肤 | POST | `/village/skin/apply` |
-| 1.6 | 皮肤 | 获取已购皮肤 | POST | `/village/skin/myPurchased` |
-| 1.7 | 皮肤 | 获取皮肤订单 | POST | `/village/skin/orderList` |
-| 1.8 | 皮肤 | 获取村社当前皮肤 | POST | `/village/skin/current` |
-| 2.1 | 栏目 | 获取栏目配置 | POST | `/village/column/getConfig` |
-| 2.2 | 栏目 | 保存栏目配置 | POST | `/village/column/saveConfig` |
-| 3.1 | 话题 | 获取话题列表 | POST | `/village/topic/getTopicTags` |
-| 3.2 | 话题 | 保存话题列表 | POST | `/village/topic/saveTopicTags` |
-
----
-
-## 五、前端 TODO 对照说明
-
-| 文件 | TODO 标记 | 对应接口 |
-|------|-----------|----------|
-| `card.vue:709` | `//TODO: 加载村社主题` | **1.8** 获取村社当前皮肤 |
-| `card.vue:713` | `//TODO: 加载村社话题` | **3.1** 获取话题列表 |
-| `config.vue:62-76` | `recommendLoader` 静态数据 | **1.2** 获取推荐皮肤 |
-| `skin/skin.vue:42` | `//TODO: 缺少接口` | **1.1** 获取皮肤列表 |
-| `skin/details.vue:57` | `//TODO: 缺少接口` | **1.3** 获取皮肤详情 |
-| `skin/details.vue:74` | `//TODO` (handleBuy) | **1.4** 购买皮肤 |
-| `skin/details.vue:78` | `//TODO` (handleUse) | **1.5** 使用皮肤 |
-| `skin/buied.vue:29` | `//TODO: 缺少接口` | **1.6** 获取已购皮肤 |
-| `skin/orders.vue:73` | `//TODO: 更换为获取皮肤订单接口` | **1.7** 获取皮肤订单 |
-| `config/config-tags.vue:79` | `//TODO: saveTags()` | **3.2** 保存话题列表 |
-| `config/config-tags.vue:87` | `//TODO: getTags()` | **3.1** 获取话题列表 |