快乐的梦鱼 недель назад: 3
Родитель
Сommit
65489caf98

+ 2 - 2
src/common/components/parts/HomeTitle.vue

@@ -14,8 +14,8 @@
     </template>
     <template #title> 
       <FlexRow align="center">
-        <Text :text="titleBegin2" fontConfig="primaryTitle" color="#55989a" />
-        <Text v-if="titleEnd" :text="titleEnd" fontConfig="primaryTitle" color="#5f3f2c" />
+        <Text :text="titleBegin2" fontConfig="primaryTitle" color="text.titleLight" />
+        <Text v-if="titleEnd" :text="titleEnd" fontConfig="primaryTitle" color="text.title" />
       </FlexRow>
     </template>
     <template #right>

+ 4 - 0
src/components/dynamic/x/DynamicXNode.vue

@@ -50,6 +50,8 @@
     <Icon v-else-if="node.type === 'icon'" :innerStyle="node.style" v-bind="mergedProps" />
     <Button v-else-if="node.type === 'button'" :innerStyle="node.style" v-bind="mergedProps" />
     <IconButton v-else-if="node.type === 'iconButton'" :innerStyle="node.style" v-bind="mergedProps" />
+    <Height v-else-if="node.type === 'height'" :innerStyle="node.style" v-bind="mergedProps" />
+    <Width v-else-if="node.type === 'width'" :innerStyle="node.style" v-bind="mergedProps" />
     <NodeRender v-else :node="node" :mergedProps="mergedProps">
       <slot name="nodeRender" :node="node" />
     </NodeRender>
@@ -69,6 +71,8 @@ import Icon from '@/components/basic/Icon.vue';
 import Button from '@/components/basic/Button.vue';
 import IconButton from '@/components/basic/IconButton.vue';
 import Text from '@/components/basic/Text.vue';
+import Height from '@/components/layout/space/Height.vue';
+import Width from '@/components/layout/space/Width.vue';
 
 const props = withDefaults(defineProps<{
   node: DynamicXNode;

+ 0 - 19
src/components/theme/ProvideColor.vue

@@ -1,19 +0,0 @@
-<template>
-  <slot />
-</template>
-
-<script setup lang="ts">
-import { provideSomeThemeColor } from './ThemeDefine';
-
-defineOptions({
-  options: {
-    virtualHost: true
-  }
-})
-
-const props = defineProps({
-  colors: Object,
-})
-
-provideSomeThemeColor(props.colors || {});
-</script>

+ 3 - 1
src/components/theme/ProvideVar.vue

@@ -3,7 +3,7 @@
 </template>
 
 <script setup lang="ts">
-import { provideSomeThemeVar } from './ThemeDefine';
+import { provideSomeThemeColor, provideSomeThemeVar } from './ThemeDefine';
 
 defineOptions({
   options: {
@@ -13,7 +13,9 @@ defineOptions({
 
 const props = defineProps({
   vars: Object,
+  colors: Object
 })
 
 provideSomeThemeVar(props.vars || {});
+provideSomeThemeColor(props.colors || {});
 </script>

+ 51 - 9
src/components/theme/ThemeDefine.ts

@@ -52,16 +52,61 @@ export function propGetThemeVar2(cb: (getVar: (key: string, defaultValue: any) =
 }
 
 
+/**
+ * 将覆盖记录应用到源对象上,支持点号分隔的路径键以进行深度设置。
+ *
+ * 支持三种覆盖方式:
+ * 1. 点号路径键:`{ "background.page": "#faaaaa" }` → 深度设置 `obj.background.page`
+ * 2. 对象值键:`{ "background": { "page": "#faaaaa" } }` → 与已有子对象深度合并
+ * 3. 普通键值:`{ "someKey": "someValue" }` → 直接替换
+ *
+ * 该函数始终保持不可变性,返回一个全新的对象。
+ */
+function applyOverrides<T extends Record<string, any>>(
+  source: T,
+  overrides: Record<string, any>,
+): T {
+  let result = { ...source } as Record<string, any>;
+
+  for (const [key, value] of Object.entries(overrides)) {
+    if (key.includes('.')) {
+      // 点号路径:逐层克隆并设值
+      const parts = key.split('.');
+      // 从根开始,逐层向下克隆
+      let current: Record<string, any> = result;
+      for (let i = 0; i < parts.length - 1; i++) {
+        const part = parts[i];
+        // 克隆当前层,保证不可变性
+        if (current[part] && typeof current[part] === 'object') {
+          current[part] = { ...current[part] };
+        } else {
+          current[part] = {};
+        }
+        current = current[part];
+      }
+      current[parts[parts.length - 1]] = value;
+    } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+      // 对象值:与已有子对象深度合并
+      result[key] = {
+        ...(result[key] || {}),
+        ...value,
+      };
+    } else {
+      // 普通值:直接替换
+      result[key] = value;
+    }
+  }
+
+  return result as T;
+}
+
 export function provideSomeThemeColor(record: Record<string, any>) {
- const topTheme = inject<Ref<ThemeConfig>>(ThemeKey);
+  const topTheme = inject<Ref<ThemeConfig>>(ThemeKey);
   const newTheme = computed(() => {
     const topThemeValue = topTheme?.value ?? DefaultTheme;
     const v = {
       ...topThemeValue,
-      colorConfigs: {
-        ...topThemeValue.colorConfigs,
-        ...record
-      }
+      colorConfigs: applyOverrides(topThemeValue.colorConfigs, record),
     } as ThemeConfig;
     return v;
   });
@@ -73,10 +118,7 @@ export function provideSomeThemeVar(record: Record<string, any>) {
     const topThemeValue = topTheme?.value ?? DefaultTheme;
     const v = {
       ...topThemeValue,
-      varOverrides: {
-        ...topThemeValue.varOverrides,
-        ...record
-      }
+      varOverrides: applyOverrides(topThemeValue.varOverrides, record),
     } as ThemeConfig;
     return v;
   });

+ 2 - 0
src/pages/home/index.vue

@@ -357,6 +357,7 @@ const introClamTipRef = ref();
 const introClamTipTimeout = new MemoryTimeOut('IntroClamTip', 1000 * 3600 * 30);//30h
 const postIndexRef = ref<InstanceType<typeof PostIndex>>();
 const appConfiguration = injectAppConfiguration();
+const topBanner = ref('');
 
 const { value: currentCity } = useStorageVar('currentCityName', '');
 const { value: currentCityCode } = useStorageVar('currentCityCode', 0);
@@ -566,6 +567,7 @@ onMounted(async () => {
     console.error(error);
     toast('获取当前位置失败,您可以手动选择城市');
   }
+  topBanner.value = appConfiguration.value.banners.homeTop || '';
 });
 defineExpose({
   goMap() {

+ 1 - 10
src/pages/home/village/index.vue

@@ -32,16 +32,6 @@
     <Height :height="40" />
     <template v-if="delayShow">
       <FlexCol v-if="villageStore.currentVillage" gap="gap.md">
-        <FlexRow center :padding="[0,30]">
-          <HomeLargeTitle title="村社名片" :active="tab === 'card'" @click="tab = 'card'" />
-          <Width :width="30" />
-          <HomeLargeTitle title="乡源树" titleColorAnim :active="tab === 'tree'" @click="tab = 'tree'">
-            <template #icon>
-              <Image src="https://xy.wenlvti.net/app_static/images/village/TreeIconAmin2.gif" :width="34" :height="50" mode="heightFix" />
-            </template>
-          </HomeLargeTitle>
-          <Width :width="30" />
-        </FlexRow>
         <Card 
           v-if="tab === 'card'" 
           ref="cardRef"
@@ -50,6 +40,7 @@
         />
         <Tree 
           v-if="tab === 'tree'" 
+          @goCard="tab = 'card'"
           ref="treeRef"
         />
         <Height :height="150" />

+ 353 - 330
src/pages/home/village/introd/card.vue

@@ -1,355 +1,372 @@
 <template>
   <SimplePageContentLoader :loader="pageThemeLoader">
-    <DynamicX 
-      v-if="pageThemeLoader.content.value"
-      :page="pageThemeLoader.content.value"
-      :injectScriptGlobal="pageScriptGlobal"
+    <ProvideVar 
+      v-if="pageThemeLoader.content.value" 
+      :vars="pageThemeLoader.content.value.props?.varOvrride"
+      :colors="pageThemeLoader.content.value.props?.colorOvrride"
     >
-      <template #default="{ page, context }">
-        
-        <DynamicXNodeChild :node="page">
-          <template #nodeRender="{ node }">
-
-            <FlexCol v-if="node.type === 'Card:Basic'" gap="gap.md">
-              <!-- 标题 -->
-              <FlexRow justify="space-between" gap="gap.lg" wrap align="center" width="100%">
-                <FlexRow gap="gap.md">
-                  <Text 
-                    :text="title" 
-                    :lines="1" 
-                    :fontSize="40"
-                    wordBreak="break-all"
-                    fontConfig="primaryTitle"
-                  />
-                </FlexRow>
-                <FlexRow flex="1" justify="space-between" align="center" gap="gap.lg">
-                  <IconButton icon="cascades" :size="40" @click="shareQRDialog?.show()" />
-                  <FlexRow align="center" gap="gap.lg">
-                    <BubbleTip
-                      v-model:show="showFollowTip"
-                      content="关注我,方便下次进入"
-                      @contentClick="isFollowed ? undefined : onFollow()"
-                      @close="handleCloseFollowTip"
-                    >
+  
+      <FlexRow center :padding="[0,30]">
+        <HomeLargeTitle title="村社名片" :active="true" />
+        <Width :width="30" />
+        <HomeLargeTitle title="乡源树" titleColorAnim @click="emit('goTree')">
+          <template #icon>
+            <Image src="https://xy.wenlvti.net/app_static/images/village/TreeIconAmin2.gif" :width="34" :height="50" mode="heightFix" />
+          </template>
+        </HomeLargeTitle>
+        <Width :width="30" />
+      </FlexRow>
+
+      <DynamicX 
+        :page="pageThemeLoader.content.value"
+        :injectScriptGlobal="pageScriptGlobal"
+      >
+        <template #default="{ page, context }">
+          
+          <DynamicXNodeChild :node="page">
+            <template #nodeRender="{ node }">
+
+              <FlexCol v-if="node.type === 'Card:Basic'" gap="gap.md">
+                <!-- 标题 -->
+                <FlexRow justify="space-between" gap="gap.lg" wrap align="center" width="100%">
+                  <FlexRow gap="gap.md">
+                    <Text 
+                      :text="title" 
+                      :lines="1" 
+                      :fontSize="40"
+                      wordBreak="break-all"
+                      fontConfig="primaryTitle"
+                    />
+                  </FlexRow>
+                  <FlexRow flex="1" justify="space-between" align="center" gap="gap.lg">
+                    <IconButton icon="cascades" :size="40" @click="shareQRDialog?.show()" />
+                    <FlexRow align="center" gap="gap.lg">
+                      <BubbleTip
+                        v-model:show="showFollowTip"
+                        content="关注我,方便下次进入"
+                        @contentClick="isFollowed ? undefined : onFollow()"
+                        @close="handleCloseFollowTip"
+                      >
+                        <Button 
+                          :icon="isFollowed ? 'success' : 'https://xy.wenlvti.net/app_static/images/village/IconJoin.png'" 
+                          radius="radius.lgr"
+                          :text="isFollowed ? (title.length < 5 ? '已关注' : '') : '关注'"
+                          :padding="[15, 10]"
+                          :iconProps="{ size: 38 }"
+                          @click="isFollowed ? onUnFollow() : onFollow()"
+                        />
+                      </BubbleTip>
+                      <FlexRow 
+                        v-if="isJoined"
+                        gap="gap.md"
+                        radius="radius.lgr"
+                        backgroundColor="background.tertiary"
+                      >
+                        <Avatar 
+                          :src="authStore.userInfo?.avatar" 
+                          :size="65"
+                          defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
+                        />
+                        <FlexCol position="relative" gap="gap.sm" :padding="[0,30,0,0]">
+                          <FlexRow align="center" :gap="10">
+                            <Text :text="displayMyName" fontSize="23" :lines="1" :maxWidth="150" fontConfig="lightImportantTitle" />
+                            <Button 
+                              v-if="isStaff" 
+                              :innerStyle="{ position: 'absolute', right: 0, top: 0, transform: 'translate(5%,-50%)' }"
+                              text="管理" size="mini" radius="radius.md" type="danger" 
+                              @click="staffInfoRef?.show()" 
+                            />
+                          </FlexRow>
+                          <IconButton icon="edit-filling" size="26" @click="changeNickRef?.show()">
+                            <Text text="改昵称" :wrap="false" fontSize="22" fontConfig="subText" />
+                          </IconButton>
+                        </FlexCol>
+                      </FlexRow>
                       <Button 
-                        :icon="isFollowed ? 'success' : 'https://xy.wenlvti.net/app_static/images/village/IconJoin.png'" 
+                        v-else
+                        icon="https://xy.wenlvti.net/app_static/images/village/IconFollow.png"
                         radius="radius.lgr"
-                        :text="isFollowed ? (title.length < 5 ? '已关注' : '') : '关注'"
-                        :padding="[15, 10]"
-                        :iconProps="{ size: 38 }"
-                        @click="isFollowed ? onUnFollow() : onFollow()"
-                      />
-                    </BubbleTip>
-                    <FlexRow 
-                      v-if="isJoined"
-                      gap="gap.md"
-                      radius="radius.lgr"
-                      backgroundColor="background.tertiary"
-                    >
-                      <Avatar 
-                        :src="authStore.userInfo?.avatar" 
-                        :size="65"
-                        defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
+                        text="加入"
+                        @click="handleGoJoin()"
                       />
-                      <FlexCol position="relative" gap="gap.sm" :padding="[0,30,0,0]">
-                        <FlexRow align="center" :gap="10">
-                          <Text :text="displayMyName" fontSize="23" :lines="1" :maxWidth="150" fontConfig="lightImportantTitle" />
-                          <Button 
-                            v-if="isStaff" 
-                            :innerStyle="{ position: 'absolute', right: 0, top: 0, transform: 'translate(5%,-50%)' }"
-                            text="管理" size="mini" radius="radius.md" type="danger" 
-                            @click="staffInfoRef?.show()" 
-                          />
-                        </FlexRow>
-                        <IconButton icon="edit-filling" size="26" @click="changeNickRef?.show()">
-                          <Text text="改昵称" :wrap="false" fontSize="22" fontConfig="subText" />
-                        </IconButton>
-                      </FlexCol>
                     </FlexRow>
-                    <Button 
-                      v-else
-                      icon="https://xy.wenlvti.net/app_static/images/village/IconFollow.png"
-                      radius="radius.lgr"
-                      text="加入"
-                      @click="handleGoJoin()"
-                    />
                   </FlexRow>
                 </FlexRow>
-              </FlexRow>
-
-              <!-- 简介 -->
-              <TextEllipsis 
-                fontConfig="secondText"
-                :outerProps="{ direction: 'row', width: '490rpx' }"
-                :lines="1" 
-                :expandable="(villageInfoLoader.content.value?.desc as string || '').length > 20"
-                :text="(villageInfoLoader.content.value?.desc as string) || '暂无简介,欢迎您来编写完善!'"
-                wordBreak="break-all"
-                @expand="descOpen = true;"
-                @collapse="descOpen = false;"
-              />
-
-            </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>
 
-            </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>
-            <template v-else-if="node.type === 'Card:AddressAndMap'">
-              
-              <!-- 地图 -->
-              <VillageMiniMap 
-                v-if="villageInfoLoader.content.value"
-                :lonlat="{ 
-                  longitude: villageInfoLoader.content.value.longitude, 
-                  latitude: villageInfoLoader.content.value.latitude 
-                }" 
-                :currentNoticeContent="currentNoticeContent"
-              >
-                <FlexRow position="absolute" :inset="{ l: 13, r: 13, b: 13 }" center :zIndex="100">
-                  <FlexRow align="center" gap="gap.sm" backgroundColor="background.tertiary" radius="radius.lg" padding="padding.sm">
-                    <Icon name="https://xy.wenlvti.net/app_static/images/village/IconMap.png" size="fontSize.md" />
-                    <Text :text="villageInfoLoader.content.value?.address" :maxWidth="550" fontConfig="contentText" fontSize="fontSize.sm" />
+                <!-- 简介 -->
+                <TextEllipsis 
+                  fontConfig="secondText"
+                  :outerProps="{ direction: 'row', width: '490rpx' }"
+                  :lines="1" 
+                  :expandable="(villageInfoLoader.content.value?.desc as string || '').length > 20"
+                  :text="(villageInfoLoader.content.value?.desc as string) || '暂无简介,欢迎您来编写完善!'"
+                  wordBreak="break-all"
+                  @expand="descOpen = true;"
+                  @collapse="descOpen = false;"
+                />
+
+              </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>
-              </VillageMiniMap>
 
-            </template>
-            <template v-else-if="node.type === 'Card:Static'">
-              
-              <!-- 村社状态信息 -->
-              <FlexRow justify="space-between" align="center">
-                <FlexRow center gap="gap.lg" flexBasis="50%">
-                  <Text text="村社排名" fontConfig="contentText" />
-                  <Text text="No." fontConfig="lightTitle" />
-                  <Text :text="villageInfoLoader.content.value?.rankText" fontConfig="primaryTitle" />
-                </FlexRow>
-                <FlexRow center gap="gap.lg" flexBasis="50%">
-                  <Text text="村社等级" fontConfig="contentText" />
-                  <Text :text="`${villageInfoLoader.content.value?.level}级`" fontConfig="primaryTitle" />
+              </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>
+              <template v-else-if="node.type === 'Card:AddressAndMap'">
+                
+                <!-- 地图 -->
+                <VillageMiniMap 
+                  v-if="villageInfoLoader.content.value"
+                  :lonlat="{ 
+                    longitude: villageInfoLoader.content.value.longitude, 
+                    latitude: villageInfoLoader.content.value.latitude 
+                  }" 
+                  :currentNoticeContent="currentNoticeContent"
+                >
+                  <FlexRow position="absolute" :inset="{ l: 13, r: 13, b: 13 }" center :zIndex="100">
+                    <FlexRow align="center" gap="gap.sm" backgroundColor="background.tertiary" radius="radius.lg" padding="padding.sm">
+                      <Icon name="https://xy.wenlvti.net/app_static/images/village/IconMap.png" size="fontSize.md" />
+                      <Text :text="villageInfoLoader.content.value?.address" :maxWidth="550" fontConfig="contentText" fontSize="fontSize.sm" />
+                    </FlexRow>
+                  </FlexRow>
+                </VillageMiniMap>
+
+              </template>
+              <template v-else-if="node.type === 'Card:Static'">
+                
+                <!-- 村社状态信息 -->
+                <FlexRow justify="space-between" align="center">
+                  <FlexRow center gap="gap.lg" flexBasis="50%">
+                    <Text text="村社排名" fontConfig="contentText" />
+                    <Text text="No." fontConfig="lightTitle" />
+                    <Text :text="villageInfoLoader.content.value?.rankText" fontConfig="primaryTitle" />
+                  </FlexRow>
+                  <FlexRow center gap="gap.lg" flexBasis="50%">
+                    <Text text="村社等级" fontConfig="contentText" />
+                    <Text :text="`${villageInfoLoader.content.value?.level}级`" fontConfig="primaryTitle" />
+                  </FlexRow>
                 </FlexRow>
-              </FlexRow>
-              
-              <Height height="gap.md" />
-
-              <FlexRow backgroundColor="background.tertiary" radius="radius.md" :padding="[30, 20]">
-                <Touchable direction="column" center gap="gap.sm" flexBasis="25%" @click="navTo('/pages/home/village/task/index')">
-                  <Text text="乡源光" fontConfig="secondText" fontSize="fontSize.sm" />
-                  <Text :text="villageInfoLoader.content.value?.light || '0'" fontConfig="importantTitle" />
-                  <Button type="text" size="mini" text="做任务" @click="navTo('/pages/home/village/task/index')" />
-                </Touchable>
-                <Divider type="vertical" />
-                <FlexCol center gap="gap.sm" flexBasis="25%">
-                  <Touchable direction="column" center @click="navTo('/pages/home/village/volunteer/list', { villageId: villageStore.currentVillage?.id ?? undefined })">
-                    <Text text="乡源人数" fontConfig="contentText" fontSize="fontSize.sm" />
-                    <Text :text="villageInfoLoader.content.value?.memberCount|| '0'" fontConfig="importantTitle" />
+                
+                <Height height="gap.md" />
+
+                <FlexRow backgroundColor="background.tertiary" radius="radius.md" :padding="[30, 20]">
+                  <Touchable direction="column" center gap="gap.sm" flexBasis="25%" @click="navTo('/pages/home/village/task/index')">
+                    <Text text="乡源光" fontConfig="secondText" fontSize="fontSize.sm" />
+                    <Text :text="villageInfoLoader.content.value?.light || '0'" fontConfig="importantTitle" />
+                    <Button type="text" size="mini" text="做任务" @click="navTo('/pages/home/village/task/index')" />
                   </Touchable>
-                  <WxButton openType="share">
-                    <Button type="text" size="mini" text="邀请加入" @click="navTo('/pages/home/village/task/index')" />
-                  </WxButton>
-                </FlexCol>
-                <Divider type="vertical" />
-                <Touchable direction="column" center gap="gap.sm" flexBasis="25%" @click="navTo('/pages/home/village/follow/list', {
-                  villageId: villageStore.currentVillage?.id ?? undefined,
-                })">
-                  <Text text="关注人数" fontConfig="contentText" fontSize="fontSize.sm" />
-                  <Text :text="villageInfoLoader.content.value?.followerCount|| '0'" fontConfig="importantTitle" />
-                  <Height :size="36" />
-                </Touchable>
-                <Divider type="vertical" />
-                <Button 
-                  :padding="0" 
-                  type="text" 
-                  size="small" 
-                  textColor="text.title"
-                  rightIcon="arrow-right" 
-                  @click="handleGoNew()"
-                >
-                  <FlexCol>
-                    <Text text="新手" fontConfig="contentText" />
-                    <Text text="上路" fontConfig="contentText" />
+                  <Divider type="vertical" />
+                  <FlexCol center gap="gap.sm" flexBasis="25%">
+                    <Touchable direction="column" center @click="navTo('/pages/home/village/volunteer/list', { villageId: villageStore.currentVillage?.id ?? undefined })">
+                      <Text text="乡源人数" fontConfig="contentText" fontSize="fontSize.sm" />
+                      <Text :text="villageInfoLoader.content.value?.memberCount|| '0'" fontConfig="importantTitle" />
+                    </Touchable>
+                    <WxButton openType="share">
+                      <Button type="text" size="mini" text="邀请加入" @click="navTo('/pages/home/village/task/index')" />
+                    </WxButton>
                   </FlexCol>
-                </Button>
-              </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" 
-                showMore 
-                @moreClicked="handleGoCollect()"
-              >
-                <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" />
-              <ProvideVar :vars="{
-                GridItemIconSize: 90,
-                GridItemBackgroundColor: 'transparent',
-                GridItemPaddingHorizontal: 0,
-                GridItemPaddingVertical: 8,
-              }">
-                <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>
-              </ProvideVar>
-
-            </template>
-            <template v-else-if="node.type === 'Block:Games'">
-
-              <!-- 活力乡源 -->
-              <HomeTitle :title="node.props?.title" />
-              <Height height="gap.md" />
-              <ProvideVar :vars="{
-                GridItemIconSize: 90,
-                GridItemBackgroundColor: 'transparent',
-                GridItemPaddingHorizontal: 0,
-                GridItemPaddingVertical: 8,
-              }">
-                <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>
-              </ProvideVar>
-
-            </template>
-            <template v-else-if="node.type === 'Block:Official'">
+                  <Divider type="vertical" />
+                  <Touchable direction="column" center gap="gap.sm" flexBasis="25%" @click="navTo('/pages/home/village/follow/list', {
+                    villageId: villageStore.currentVillage?.id ?? undefined,
+                  })">
+                    <Text text="关注人数" fontConfig="contentText" fontSize="fontSize.sm" />
+                    <Text :text="villageInfoLoader.content.value?.followerCount|| '0'" fontConfig="importantTitle" />
+                    <Height :size="36" />
+                  </Touchable>
+                  <Divider type="vertical" />
+                  <Button 
+                    :padding="0" 
+                    type="text" 
+                    size="small" 
+                    textColor="text.title"
+                    rightIcon="arrow-right" 
+                    @click="handleGoNew()"
+                  >
+                    <FlexCol>
+                      <Text text="新手" fontConfig="contentText" />
+                      <Text text="上路" fontConfig="contentText" />
+                    </FlexCol>
+                  </Button>
+                </FlexRow>
 
-              <!-- 文脉乡源 -->
-              <HomeTitle :title="node.props?.title">
-                <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)"
-                    >
+              </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" 
+                  showMore 
+                  @moreClicked="handleGoCollect()"
+                >
+                  <template #right>
+                    <FlexRow align="center" gap="gap.md">
                       <FrameButton
                         text="管理" 
                         size="small" 
                         primary
-                        @click="handleGoOfficalManage()"
+                        @click="handleGoDigManage()"
                       />
-                    </BubbleTip>
-                    <FrameButton 
-                      text="乡源AI帮你写" 
-                      size="small" 
-                      icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
-                      @click="handleGoPublish()" 
+                      <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" />
+                <ProvideVar :vars="{
+                  GridItemIconSize: 90,
+                  GridItemBackgroundColor: 'transparent',
+                  GridItemPaddingHorizontal: 0,
+                  GridItemPaddingVertical: 8,
+                }">
+                  <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>
+                </ProvideVar>
+
+              </template>
+              <template v-else-if="node.type === 'Block:Games'">
+
+                <!-- 活力乡源 -->
+                <HomeTitle :title="node.props?.title" />
+                <Height height="gap.md" />
+                <ProvideVar :vars="{
+                  GridItemIconSize: 90,
+                  GridItemBackgroundColor: 'transparent',
+                  GridItemPaddingHorizontal: 0,
+                  GridItemPaddingVertical: 8,
+                }">
+                  <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?.())" 
                     />
-                  </FlexRow>
-                </template>
-              </HomeTitle>
-              <Height height="gap.md" />
-              <RoundTags 
-                v-model:active="listActiveTag" 
-                :tags="tagsLoader.content.value || []" 
-              />
-              <Height height="gap.md" />
-              <OfficialAccountPublishWrap
-                :topic="recommendTagName" 
-                :path="recommendPath"
-                @publishsuccess="onPublishSuccess"
-                @empty="isOfficialEmpty=true"
-              />
-
+                  </Grid>
+                </ProvideVar>
+
+              </template>
+              <template v-else-if="node.type === 'Block:Official'">
+
+                <!-- 文脉乡源 -->
+                <HomeTitle :title="node.props?.title">
+                  <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 || []" 
+                />
+                <Height height="gap.md" />
+                <OfficialAccountPublishWrap
+                  :topic="recommendTagName" 
+                  :path="recommendPath"
+                  @publishsuccess="onPublishSuccess"
+                  @empty="isOfficialEmpty=true"
+                />
+
+              </template>
             </template>
-          </template>
-        </DynamicXNodeChild>
-      </template>
-    </DynamicX>
+          </DynamicXNodeChild>
+        </template>
+      </DynamicX>
+    </ProvideVar>
   </SimplePageContentLoader>
 
   <UpgradeDialog 
@@ -409,7 +426,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, reactive, ref, watch } from 'vue';
+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';
@@ -462,6 +479,7 @@ import DynamicX from '@/components/dynamic/DynamicX.vue';
 import type { DynamicXPage } from '@/components/dynamic/DynamicX';
 import DynamicXNodeChild from '@/components/dynamic/x/DynamicXNodeChild.vue';
 import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import HomeLargeTitle from '@/common/components/parts/HomeLargeTitle.vue';
 
 const emit = defineEmits<{
   (e: 'goTree'): void;
@@ -705,9 +723,14 @@ const pageScriptGlobal = reactive({
   emit,
 
 });
+
+const topBanner = inject('topBanner') as Ref<string>;
+
 const pageThemeLoader = useSimpleDataLoader(async () => {
   //TODO: 加载村社主题
-  return DefaultCard as DynamicXPage;
+  const page = DefaultCard as DynamicXPage;
+  topBanner.value = page.props?.topBanner || 'https://xy.wenlvti.net/app_static/images/home/BannerHome.png';
+  return page;
 });
 const tagsLoader = useSimpleDataLoader(async () => {
   //TODO: 加载村社话题

+ 1 - 0
src/pages/home/village/introd/data/DefaultCard.json

@@ -2,6 +2,7 @@
   "name": "VILLAGE",
   "type": "page",
   "props": {},
+  "style": {},
   "nodes": [
     {
       "name": "VILLAGE:CONTENT",

+ 158 - 0
src/pages/home/village/introd/data/TestCard.json

@@ -0,0 +1,158 @@
+{
+  "name": "VILLAGE",
+  "type": "page",
+  "props": {
+    "topBanner": "https://server.imengyu.top/uploads/media/4KSef6ZDiNAh.jpg",
+    "varOvrride": {
+      
+    },
+    "colorOvrride": {
+      "default.primary": "#333333",
+      "pressed.primary": "#999999",
+      "default.button": "#efefef",
+      "background.page": "#f5f5f5",
+      "background.primary": "#f5f5f5",
+      "background.secondary": "#e8e8e8",
+      "background.tertiary": "#fafafa",
+      "background.quaternary": "#d0d0d0",
+      "text.titleLight": "#222222",
+      "text.title": "#333333",
+      "text.content": "#444444"
+    }
+  },
+  "style": {
+    "backgroundImage": "url('https://server.imengyu.top/uploads/media/XrZxJjJrBwxr.jpg')",
+    "backgroundSize": "cover",
+    "backgroundPosition": "top center",
+    "backgroundRepeat": "repeat"
+  },
+  "nodes": [
+    {
+      "name": "VILLAGE:CONTENT",
+      "type": "flex",
+      "props": {
+        "direction": "column",
+        "padding": [30, 30, 0, 30],
+        "gap": "gap.lg"
+      },
+      "nodes": [
+        {
+          "name": "VILLAGE:CONTENT:Basic",
+          "type": "BackgroundBox",
+          "props": {
+            "color1": "rgba(88, 88, 88, 0.3)",
+            "color2": "rgba(255, 255, 255, 0.4)",
+            "color2Position": "85%",
+            "color3": "rgba(0, 0, 0, 0.2)",
+            "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",
+                "align": "center",
+                "gap": "gap.md"
+              },
+              "nodes": [
+                {
+                  "type": "button",
+                  "props": {
+                    "size": "mini",
+                    "icon": "picture",
+                    "text": "村社相册"
+                  },
+                  "events": {
+                    "click": "globalContext.handleGoGallery()"
+                  }
+                },
+                {
+                  "type": "button",
+                  "props": {
+                    "size": "mini",
+                    "icon": "edit-filling",
+                    "text": "编辑简介"
+                  },
+                  "events": {
+                    "click": "globalContext.handleGoEdit()"
+                  }
+                },
+                {
+                  "type": "button",
+                  "props": {
+                    "size": "mini",
+                    "icon": "fabulous",
+                    "text": "主页换肤"
+                  },
+                  "events": {
+                    "click": "globalContext.handleGoSkin()"
+                  }
+                }
+              ]
+            },
+            { "type": "Card:Static" }
+          ]
+        },
+        {
+          "name": "VILLAGE:CONTENT:Rank",
+          "type": "Block:Rank",
+          "props": {
+            "title": "排行榜"
+          }
+        },
+        {
+          "name": "VILLAGE:CONTENT:Collect",
+          "type": "Block:Collect",
+          "props": {
+            "title": "魅力乡源"
+          }
+        },
+        {
+          "name": "VILLAGE:CONTENT:Games",
+          "type": "Block:Games",
+          "props": {
+            "title": "活力乡源",
+            "items": [
+              {
+                "title": "乡源荣光",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeHornor.png",
+                "handler": "globalContext.handleGoCollect(23, '乡源荣光')"
+              },
+              {
+                "title": "乡源好物",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeGoods.png",
+                "handler": "globalContext.navTo(\"/pages/home/village/goods/index\", { villageId: globalContext.getVillageId() })"
+              },
+              {
+                "title": "乡源树",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeTree.png",
+                "handler": "globalContext.emit(\"goTree\")"
+              },
+              {
+                "title": "互动游戏",
+                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeGame.png",
+                "handler": "globalContext.navTo(\"/pages/home/village/games/index\")"
+              }
+            ]
+          }
+        },
+        {
+          "name": "VILLAGE:CONTENT:Official",
+          "type": "Block:Official",
+          "props": {
+            "title": "文脉乡源",
+            "tags": "dynamic:globalContext.getDefaultTags()"
+          }
+        }
+      ]
+    }
+  ]
+}

+ 16 - 0
src/pages/home/village/introd/tree.vue

@@ -1,5 +1,17 @@
 <template>
   <FlexCol v-if="currentVillage">
+
+    <FlexRow center :padding="[0,30]">
+      <HomeLargeTitle title="村社名片" :active="true" @click="emit('goCard')" />
+      <Width :width="30" />
+      <HomeLargeTitle title="乡源树" titleColorAnim>
+        <template #icon>
+          <Image src="https://xy.wenlvti.net/app_static/images/village/TreeIconAmin2.gif" :width="34" :height="50" mode="heightFix" />
+        </template>
+      </HomeLargeTitle>
+      <Width :width="30" />
+    </FlexRow>
+
     <FlexCol :margin="[10,0,0,0]">
       <Text textAlign="center" text="一人添果,全村增光;乡源树茂,故土名扬" fontConfig="primaryTitle" fontSize="35rpx"  />
     </FlexCol>
@@ -191,10 +203,14 @@ import BlessSuccessDialog from '../dialogs/BlessSuccessDialog.vue';
 import TreeLevelUpDialog from '../dialogs/TreeLevelUpDialog.vue';
 import PopTextAnim from './components/PopTextAnim.vue';
 import BoxMid from '@/common/components/box/BoxMid.vue';
+import HomeLargeTitle from '@/common/components/parts/HomeLargeTitle.vue';
+import Width from '@/components/layout/space/Width.vue';
 
 const GROWTH_FEED_COUNT = 6;
 const DEFAULT_AVATAR = 'https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteerNew.png';
 
+const emit = defineEmits(['goCard']);
+
 const villageStore = useVillageStore();
 const authStore = useAuthStore();
 const { requireLoginAsync } = useRequireLogin();

+ 6 - 2
src/pages/index.vue

@@ -2,7 +2,7 @@
 	<view class="index" :style="{
     backgroundImage: tabIndex === 4 ? 
       `url('https://xy.wenlvti.net/app_static/images/mine/TopBanner.png')` 
-      : `url('https://xy.wenlvti.net/app_static/images/home/BannerHome.png')`,
+      : `url('${topBanner}')`,
     backgroundRepeat: 'no-repeat',
     backgroundSize: '100% auto',
     backgroundPosition: 'top center',
@@ -72,7 +72,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch } from 'vue';
+import { onMounted, provide, ref, watch } from 'vue';
 import { onPageScroll, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
 import { useTheme } from '@/components/theme/ThemeDefine';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
@@ -109,6 +109,10 @@ const backToTop = ref<InstanceType<typeof BackToTop>>();
 const villageIndex = ref<InstanceType<typeof VillageIndex>>();
 const homeIndex = ref<InstanceType<typeof HomeIndex>>();
 
+const topBanner = ref('');
+
+provide('topBanner', topBanner);
+
 const { querys } = useLoadQuerys({ 
   openLogin: false,
   villageId: 0,

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

@@ -0,0 +1,550 @@
+# 村社名片换肤等功能需增加接口
+
+> 本文档仅供后端参阅,仅列出接口名称和输入输出参数,不涉及数据库设计。本文档的接口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** 获取话题列表 |