Pārlūkot izejas kodu

📦 管理贴图话题列表基础架构

快乐的梦鱼 3 nedēļas atpakaļ
vecāks
revīzija
133642b141

+ 361 - 0
src/common/components/DragSortList.vue

@@ -0,0 +1,361 @@
+<template>
+  <view class="ds-list" :style="innerStyle">
+    <view
+      v-for="(item, index) in localList"
+      :key="item._key"
+      class="ds-item"
+      :class="{
+        'ds-item--dragging': dragIndex === index,
+        'ds-item--disabled': item.disabled,
+        'ds-item--transition': dragIndex >= 0,
+      }"
+      :style="computeItemStyle(index)"
+    >
+      <view class="ds-item__inner" :style="itemStyle">
+        <!-- 图标 -->
+        <Icon
+          v-if="item.icon"
+          :icon="item.icon"
+          :size="50"
+        />
+        <!-- 标题 -->
+        <text class="ds-item__title">{{ item.title }}</text>
+        <!-- 拖拽手柄 -->
+        <view
+          class="ds-item__handle"
+          :class="{ 'ds-item__handle--inactive': item.disabled }"
+          @touchstart.stop="onTouchStart($event, index)"
+          @touchmove.stop.prevent="onTouchMove($event)"
+          @touchend.stop="onTouchEnd"
+          @touchcancel.stop="onTouchEnd"
+        >
+          <view class="ds-item__grip-dot" v-for="i in 6" :key="i" />
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import Icon from '@/components/basic/Icon.vue';
+import { getCurrentInstance, reactive, ref, watch, type PropType } from 'vue';
+
+// ==================== 类型定义 ====================
+
+export interface DragSortItem {
+  title: string;
+  icon: string;
+  disabled: boolean;
+}
+
+// ==================== Props ====================
+
+const props = defineProps({
+  list: {
+    type: Array as PropType<DragSortItem[]>,
+    default: () => [],
+  },
+  /** 列表内部样式(作用于容器) */
+  innerStyle: {
+    type: Object,
+    default: () => ({}),
+  },
+  /** 列表项样式(作用于每条 item 的内容区) */
+  itemStyle: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+// ==================== Emits ====================
+
+const emit = defineEmits<{
+  (e: 'update:list', value: DragSortItem[]): void;
+  (e: 'change', value: DragSortItem[]): void;
+  (e: 'sort', fromIndex: number, toIndex: number): void;
+}>();
+
+// ==================== 内部状态 ====================
+
+/** 内部维护的列表副本,供拖拽排序使用 */
+const localList = reactive<Array<DragSortItem & { _key: string }>>([]);
+let keyCounter = 0;
+
+/** 生成唯一 key */
+function genKey(): string {
+  return `_ds_${++keyCounter}`;
+}
+
+/** 同步外部 list 到内部 */
+function syncList(list: DragSortItem[]) {
+  localList.length = 0;
+  for (const item of list) {
+    localList.push({ ...item, _key: genKey() });
+  }
+}
+
+watch(
+  () => props.list,
+  (val) => {
+    // 仅在非拖拽状态下同步外部变化
+    if (dragIndex.value < 0) {
+      syncList(val);
+    }
+  },
+  { immediate: true, deep: true },
+);
+
+// ==================== 拖拽状态 ====================
+
+/** 当前拖拽项的索引,-1 表示未拖拽 */
+const dragIndex = ref(-1);
+/** 拖拽起始 Y 坐标(pageY) */
+const dragStartY = ref(0);
+/** 当前拖拽偏移量(px) */
+const dragOffsetY = ref(0);
+/** 拖拽开始前各 item 的位置信息 */
+interface ItemRect {
+  top: number;
+  bottom: number;
+  height: number;
+}
+const itemRects = ref<ItemRect[]>([]);
+/** 平均 item 高度(含 gap) */
+const itemPitch = ref(60);
+/** 当前目标索引 */
+const targetIndex = ref(-1);
+
+// ==================== 实例引用 ====================
+
+const instance = getCurrentInstance();
+
+// ==================== 测量 item 位置 ====================
+
+function measureRects(): Promise<ItemRect[]> {
+  return new Promise((resolve) => {
+    if (!instance) {
+      resolve([]);
+      return;
+    }
+    const query = uni.createSelectorQuery()
+      // #ifdef MP
+      .in(instance)
+      // #endif
+    ;
+    query.selectAll('.ds-item').boundingClientRect((rects) => {
+      // 兼容不同平台:selectAll 可能返回数组或单个对象
+      const list: ItemRect[] = (rects instanceof Array ? rects : (rects ? [rects] : [])) as unknown as ItemRect[];
+      if (list.length > 0) {
+        // 计算 pitch(相邻 item 顶部的间距)
+        if (list.length >= 2) {
+          itemPitch.value = list[1].top - list[0].top;
+        } else if (list.length === 1) {
+          itemPitch.value = list[0].height + 10; // fallback
+        }
+      }
+      resolve(list);
+    }).exec();
+  });
+}
+
+// ==================== 样式计算 ====================
+
+function computeItemStyle(index: number): Record<string, string> {
+  if (dragIndex.value < 0) {
+    // 非拖拽状态:无偏移,附带过渡动画
+    return {
+      transform: 'translateY(0)',
+      transition: 'transform 0.3s ease',
+      zIndex: '1',
+    };
+  }
+
+  const from = dragIndex.value;
+  const to = targetIndex.value;
+  const pitch = itemPitch.value;
+
+  if (index === from) {
+    // 被拖拽项:跟随手指,无过渡
+    return {
+      transform: `translateY(${dragOffsetY.value}px)`,
+      transition: 'none',
+      zIndex: '100',
+      opacity: '0.92',
+    };
+  }
+
+  // 非拖拽项:根据目标位置偏移
+  let shift = 0;
+  if (to > from) {
+    // 向下拖拽:from+1 到 to 之间的项向上偏移一个 pitch
+    if (index > from && index <= to) {
+      shift = -pitch;
+    }
+  } else if (to < from) {
+    // 向上拖拽:to 到 from-1 之间的项向下偏移一个 pitch
+    if (index >= to && index < from) {
+      shift = pitch;
+    }
+  }
+
+  return {
+    transform: `translateY(${shift}px)`,
+    transition: 'transform 0.25s ease',
+    zIndex: '1',
+  };
+}
+
+// ==================== 拖拽事件处理 ====================
+
+async function onTouchStart(e: any, index: number) {
+  const item = localList[index];
+  if (!item || item.disabled) return;
+
+  // 测量所有 item 位置
+  const rects = await measureRects();
+  if (rects.length === 0) return;
+
+  const touch = e.touches[0];
+  dragIndex.value = index;
+  dragStartY.value = touch.pageY;
+  dragOffsetY.value = 0;
+  itemRects.value = rects;
+  targetIndex.value = index;
+}
+
+function onTouchMove(e: any) {
+  if (dragIndex.value < 0) return;
+
+  const touch = e.touches[0];
+  const offset = touch.pageY - dragStartY.value;
+  dragOffsetY.value = offset;
+
+  // 计算目标索引
+  const from = dragIndex.value;
+  const pitch = itemPitch.value;
+  if (pitch <= 0) return;
+
+  const steps = Math.round(offset / pitch);
+  let to = from + steps;
+  // 限制范围
+  to = Math.max(0, Math.min(localList.length - 1, to));
+
+  // 跳过 disabled 项(disabled 项不可作为拖拽源,但可作为落点,这里不做额外限制)
+  if (to !== targetIndex.value) {
+    targetIndex.value = to;
+  }
+}
+
+function onTouchEnd() {
+  if (dragIndex.value < 0) return;
+
+  const from = dragIndex.value;
+  const to = targetIndex.value;
+
+  // 执行排序
+  if (from !== to && from >= 0 && to >= 0) {
+    const moved = localList.splice(from, 1)[0];
+    localList.splice(to, 0, moved);
+
+    // 触发事件
+    const plainList: DragSortItem[] = localList.map(({ _key, ...item }) => item as DragSortItem);
+    emit('update:list', plainList);
+    emit('change', plainList);
+    emit('sort', from, to);
+  }
+
+  // 重置拖拽状态
+  dragIndex.value = -1;
+  dragStartY.value = 0;
+  dragOffsetY.value = 0;
+  targetIndex.value = -1;
+  itemRects.value = [];
+}
+
+// ==================== 虚拟宿主配置 ====================
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.ds-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10rpx;
+  position: relative;
+}
+
+.ds-item {
+  position: relative;
+  z-index: 1;
+
+  &--dragging {
+    // 拖拽中的项有阴影提升
+    .ds-item__inner {
+      box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
+    }
+  }
+
+  &--disabled {
+    opacity: 0.55;
+    pointer-events: auto; // 保持事件可穿透(只是视觉弱化)
+  }
+
+  // 过渡动画(非拖拽项)
+  &--transition {
+    // transition 在行内 style 中按需设置
+  }
+}
+
+.ds-item__inner {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: #ffffff;
+  border-radius: 12rpx;
+  padding: 8rpx 10rpx;
+  gap: 10rpx;
+  min-height: 80rpx;
+  box-sizing: border-box;
+}
+
+.ds-item__title {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 1.4;
+}
+
+.ds-item__handle {
+  display: flex;
+  flex-wrap: wrap;
+  width: 36rpx;
+  height: 44rpx;
+  flex-shrink: 0;
+  align-content: center;
+  justify-content: center;
+  gap: 6rpx 6rpx;
+  padding: 4rpx;
+  box-sizing: border-box;
+  cursor: grab;
+
+  &--inactive {
+    opacity: 0.3;
+    pointer-events: none;
+  }
+}
+
+.ds-item__grip-dot {
+  width: 8rpx;
+  height: 8rpx;
+  background: #c0c0c0;
+  border-radius: 2rpx;
+}
+</style>

+ 21 - 0
src/pages.json

@@ -343,6 +343,27 @@
       }
     },
     {
+      "path": "pages/home/village/introd/skin/orders",
+      "style": {
+        "navigationBarTitleText": "我的皮肤订单",
+        "navigationStyle": "custom"
+      }
+    },  
+    {
+      "path": "pages/home/village/introd/skin/buied",
+      "style": {
+        "navigationBarTitleText": "购买的皮肤",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/home/village/introd/config/config-tags",
+      "style": {
+        "navigationBarTitleText": "管理贴图话题列表",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/home/village/introd/skin/details",
       "style": {
         "navigationBarTitleText": "皮肤详情",

+ 345 - 477
src/pages/home/village/introd/card.vue

@@ -1,353 +1,356 @@
 <template>
-  <DynamicX 
-    :page="page"
-    :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"
-                  >
+  <SimplePageContentLoader :loader="pageThemeLoader">
+    <DynamicX 
+      v-if="pageThemeLoader.content.value"
+      :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()"
+                    />
+                    <FrameButton
+                      text="共编村史" 
+                      icon="https://xy.wenlvti.net/app_static/images/village/IconHistory.png"
+                      size="small"
+                      @click="handleGoCollect()"
                     />
-                  </BubbleTip>
-                  <FrameButton 
-                    text="乡源AI帮你写" 
-                    size="small" 
-                    icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
-                    @click="handleGoPublish()" 
+                    <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="node.props?.tags" 
-            />
-            <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>
+  </SimplePageContentLoader>
 
   <UpgradeDialog 
     ref="upgradeRef"
@@ -418,6 +421,9 @@ import { useGetNotice } from '../composeabe/GetNotice';
 import { assertNotNull, FormatUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 import { navTo } from '@/components/utils/PageAction';
 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';
@@ -455,8 +461,7 @@ import ShareQRDialog from '../dialogs/ShareQRDialog.vue';
 import DynamicX from '@/components/dynamic/DynamicX.vue';
 import type { DynamicXPage } from '@/components/dynamic/DynamicX';
 import DynamicXNodeChild from '@/components/dynamic/x/DynamicXNodeChild.vue';
-import Badge from '@/components/display/Badge.vue';
-import { isDevEnv } from '@/common/config/AppCofig';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
 
 const emit = defineEmits<{
   (e: 'goTree'): void;
@@ -695,156 +700,19 @@ const pageScriptGlobal = reactive({
   handleGoDigManage,
   handleGoCollect,
   getVillageId: () => villageStore.currentVillage?.id ?? 0,
+  getDefaultTags: () => defaultTags,
   navTo,
   emit,
+
+});
+const pageThemeLoader = useSimpleDataLoader(async () => {
+  //TODO: 加载村社主题
+  return DefaultCard as DynamicXPage;
+});
+const tagsLoader = useSimpleDataLoader(async () => {
+  //TODO: 加载村社话题
+  return defaultTags;
 });
-const page : DynamicXPage = {
-  name: 'VILLAGE',
-  type: 'page',
-  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: "#eecaa0",
-            color2: "white",
-            color2Position: "85%",
-            color3: "white",
-            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: [
-              '广场', '美食', '美景', 
-              '故事', 
-              '老手艺', '老物件'
-            ]
-          },
-        },
-      ],
-    },
-  ],
-};
 
 watch(() => villageStore.currentVillage, async () => {
   await waitTimeOut(100);

+ 2 - 0
src/pages/home/village/introd/components/PopTextAnim.vue

@@ -3,6 +3,7 @@
     v-if="visible"
     class="pop-text-anim"
     :class="{ 'pop-text-anim--fading': fading }"
+    :style="innerStyle ? innerStyle : {}"
     @transitionend="handleAnimEnd"
   >
     <slot />
@@ -15,6 +16,7 @@ import { ref, watch, nextTick } from 'vue';
 const props = withDefaults(defineProps<{
   show: boolean;
   delay?: number;
+  innerStyle?: any;
 }>(), {
   delay: 2200,
 });

+ 7 - 4
src/pages/home/village/introd/config.vue

@@ -22,7 +22,7 @@
         padding="padding.lg"
         radius="radius.md"
         backgroundColor="background.tertiary"
-        @click="handleGoConfigDetails"
+        @click="handleGoConfigMain"
       >
         <Icon icon="https://xy.wenlvti.net/app_static/images/tabs/TabIconDigActive.png" :size="120" />
         <Text>管理主页栏目显示与顺序</Text>
@@ -34,7 +34,7 @@
         padding="padding.lg"
         radius="radius.md"
         backgroundColor="background.tertiary"
-        @click="handleGoConfigDetails"
+        @click="handleGoConfigTags"
       >
         <Icon icon="https://xy.wenlvti.net/app_static/images/tabs/TabIconMineActive.png" :size="120" />
         <Text>管理贴图话题列表</Text>
@@ -81,8 +81,11 @@ const handleGoRecommendDetails = (item: { id: number }) => {
 const handleGoRecommendMore = () => {
   navTo('/pages/home/village/introd/skin/skin');
 }
-const handleGoConfigDetails = () => {
-  navTo('/pages/home/village/introd/config/details');
+const handleGoConfigMain = () => {
+  navTo('/pages/home/village/introd/config/config-main');
+}
+const handleGoConfigTags = () => {
+  navTo('/pages/home/village/introd/config/config-tags');
 }
 
 </script>

+ 94 - 0
src/pages/home/village/introd/config/config-tags.vue

@@ -0,0 +1,94 @@
+<template>
+  <CommonTopBanner title="管理贴图话题列表">
+    <FlexCol padding="padding.md" gap="gap.lg">
+      
+      <FlexRow justify="flex-end" gap="gap.md">
+        <Button icon="add" @click="handleAddTag()">添加话题</Button>
+      </FlexRow>
+
+      <DragSortList
+        v-model:list="tags"
+        :innerStyle="{ padding: '20rpx' }"
+      />
+
+      <FlexCol padding="padding.md" gap="gap.lg">
+        <PrimaryButton radius="radius.xl" text="保存修改" @click="handleSave" />
+        <Button radius="radius.xl" text="取消" @click="handleCancel" />
+      </FlexCol>
+    </FlexCol>
+
+    <Dialog
+      v-model:show="addTagVisible"
+      title="添加话题"
+      showCancel
+      :onConfirm="() => addTag()"
+      :onCancel="() => { addTagVisible = false }"
+    >
+      <template #content>
+        <Field
+          v-model="addTagTitle" 
+          placeholder="话题标题"
+          allowClear 
+          showWordLimit
+          :maxlength="100"
+        />
+      </template>
+    </Dialog>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
+import Button from '@/components/basic/Button.vue';
+import Dialog from '@/components/dialog/Dialog.vue';
+import Field from '@/components/form/Field.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import { back } from '@/components/utils/PageAction';
+import { onMounted, ref } from 'vue';
+import { defaultTags } from '../data/DefaultTag';
+import DragSortList from '@/common/components/DragSortList.vue';
+
+const tags = ref<{
+  title: string;
+  icon: string;
+  disabled: boolean;
+}[]>([]);
+
+const addTagVisible = ref(false);
+const addTagTitle = ref('');
+
+const addTag = () => {
+  if (addTagTitle.value.trim() === '') {
+    return;
+  }
+  tags.value.unshift({
+    title: addTagTitle.value,
+    icon: '',
+    disabled: false,
+  });
+  addTagTitle.value = '';
+  addTagVisible.value = false;
+}
+const handleAddTag = () => {
+  addTagVisible.value = true;
+}
+
+const handleSave = () => {
+  //TODO: saveTags();
+}
+
+const handleCancel = () => {
+  back();
+}
+
+onMounted(() => {
+  //TODO: getTags(); 如果为空则使用默认话题列表 defaultTags
+  tags.value = defaultTags.map(item => ({
+    title: item,
+    icon: '',
+    disabled: false,
+  }));
+})
+</script>

+ 128 - 16
src/pages/home/village/introd/data/DefaultCard.json

@@ -1,22 +1,134 @@
 {
-  "blocks": [
+  "name": "VILLAGE",
+  "type": "page",
+  "props": {},
+  "nodes": [
     {
-      "type": "mainBox",
-      "text": "",
-      "boxProps": {
-
+      "name": "VILLAGE:CONTENT",
+      "type": "flex",
+      "props": {
+        "direction": "column",
+        "padding": [30, 30, 0, 30],
+        "gap": "gap.lg"
       },
-      "children": [
-        { "type": "mainBox:gallery" }
+      "nodes": [
+        {
+          "name": "VILLAGE:CONTENT:Basic",
+          "type": "BackgroundBox",
+          "props": {
+            "color1": "#eecaa0",
+            "color2": "white",
+            "color2Position": "85%",
+            "color3": "white",
+            "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()"
+          }
+        }
       ]
-    },
-    {
-      "type": "volunteerRank",
-      "text": "排行榜"
-    },
-    {
-      "type": "text",
-      "text": "默认卡片内容"
     }
   ]
-}
+}

+ 1 - 0
src/pages/home/village/introd/data/DefaultTag.ts

@@ -0,0 +1 @@
+export const defaultTags = [ '广场', '美食', '美景', '故事', '老技艺', '老物件' ];

+ 45 - 0
src/pages/home/village/introd/skin/buied.vue

@@ -0,0 +1,45 @@
+<template>
+  <CommonTopBanner title="已购皮肤">
+    <FlexCol padding="padding.md">
+      <SimplePageContentLoader :loader="skinLoader">
+        <FlexRow gap="gap.lg" align="stretch" justify="space-between" wrap>
+          <IndexRoundSimpleItem
+            v-for="item in skinLoader.content.value"
+            :key="item.id"
+            :item="item"
+            :imageHeight="400"
+            @click="handleGoSkinDetails(item)"
+          />
+        </FlexRow>
+      </SimplePageContentLoader>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { navTo } from '@/components/utils/PageAction';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import IndexRoundSimpleItem from '@/common/components/parts/IndexRoundSimpleItem.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+
+const skinLoader = useSimpleDataLoader(async () => {
+  //TODO: 缺少接口
+  return [
+    {
+      id: 1,
+      title: '默认皮肤',
+      desc: '默认的村社名片',
+      image: 'https://xy.wenlvti.net/app_static/images/village/skin/DefaultA.jpg',
+      price: 0,
+    },
+  ];
+});
+
+const handleGoSkinDetails = (item: { id: number }) => {
+  navTo('/pages/home/village/introd/skin/details', { id: item.id })
+}
+
+</script>

+ 1 - 0
src/pages/home/village/introd/skin/details.vue

@@ -54,6 +54,7 @@ import PrimaryButton from '@/common/components/PrimaryButton.vue';
 const IMAGE_WIDTH = 330;
 
 const skinLoader = useSimpleDataLoader(async () => {
+  //TODO: 缺少接口
   return {
     id: 1,
     title: '默认皮肤',

+ 97 - 0
src/pages/home/village/introd/skin/orders.vue

@@ -0,0 +1,97 @@
+<template>
+  <CommonTopBanner title="皮肤订单">
+    <SimplePageListLoader :loader="listLoader" :emptyView="{ text: '暂无皮肤订单' }">
+      <FlexCol gap="gap.md" padding="padding.md">
+        <BoxMid
+          v-for="item in listLoader.list.value"
+          :key="item.id"
+          direction="row"
+          :padding="[16, 30]"
+        >
+          <Touchable
+            :gap="20"
+            :padding="[15, 20]"
+            touchable
+            flex="1"
+            direction="row"
+            justify="space-between"
+            align="center"
+          >
+            <FlexCol>
+              <Text fontConfig="lightImportantTitle" fontFamily="SongtiSCBlack">{{ item.levelName }}</Text>
+              <Text fontSize="fontSize.sm" :text="`${item.villageName} ${item.vipLevel}级`" />
+            </FlexCol>
+            <FlexRow gap="gap.xl">
+              <FlexCol align="flex-end">
+                <FlexRow align="center" gap="gap.sm">
+                  <Text fontSize="fontSize.sm" fontConfig="lightGoldTitle">¥</Text>
+                  <Text fontConfig="lightGoldTitle">{{ item.price }}</Text>
+                  <Width :width="10" />
+                  <Tag size="small" :text="item.statusText" :type="getStatusTypeByStatusText(item.statusText)" />
+                </FlexRow>
+                <Text fontConfig="secondText">{{ item.createtime }}</Text>
+              </FlexCol>
+            </FlexRow>
+          </Touchable>
+        </BoxMid>
+      </FlexCol>
+    </SimplePageListLoader>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useAuthStore } from '@/store/auth';
+import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import TreeApi, { UpgradeOrderItem } from '@/api/light/TreeApi';
+import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import Tag from '@/components/display/Tag.vue';
+import Width from '@/components/layout/space/Width.vue';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+}, () => {
+  listLoader.load();
+});
+const authStore = useAuthStore();
+
+const listLoader = useSimplePageListLoader(20, async (page, pageSize) => {
+  if (!authStore.userId) {
+    return {
+      list: [],
+      total: 0,
+    };
+  }
+  //TODO: 更换为获取皮肤订单接口
+  const res = await TreeApi.getUpgradeOrderList({
+    page,
+    pageSize,
+    villageId: querys.value.villageId || undefined,
+    userId: authStore.userId,
+  });
+  return {
+    list: res.list,
+    total: res.total,
+  };
+});
+
+function getStatusTypeByStatusText(statusText: string): 'warning' | 'success' | 'default' {
+  switch (statusText) {
+    case '待支付':
+    case '待审核':
+      return 'warning';
+    case '已支付':
+      return 'success';
+    default:
+      return 'default';
+  }
+}
+</script>

+ 13 - 2
src/pages/home/village/introd/skin/skin.vue

@@ -1,6 +1,10 @@
 <template>
   <CommonTopBanner title="皮肤中心">
-    <FlexCol padding="padding.md">
+    <FlexCol padding="padding.md" gap="gap.lg">
+      <FlexRow justify="flex-end" gap="gap.md">
+        <Button @click="handleGoSkinOrders()">查看订单</Button>
+        <Button @click="handleGoSkinBuied()">我的皮肤</Button>
+      </FlexRow>
       <SimplePageContentLoader :loader="skinLoader">
         <FlexRow gap="gap.lg" align="stretch" justify="space-between" wrap>
           <IndexRoundSimpleItem
@@ -32,8 +36,10 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
 import Text from '@/components/basic/Text.vue';
+import Button from '@/components/basic/Button.vue';
 
 const skinLoader = useSimpleDataLoader(async () => {
+  //TODO: 缺少接口
   return [
     {
       id: 1,
@@ -111,6 +117,11 @@ const skinLoader = useSimpleDataLoader(async () => {
 const handleGoSkinDetails = (item: { id: number }) => {
   navTo('/pages/home/village/introd/skin/details', { id: item.id })
 }
-
+const handleGoSkinOrders = () => {
+  navTo('/pages/home/village/introd/skin/orders')
+}
+const handleGoSkinBuied = () => {
+  navTo('/pages/home/village/introd/skin/buied')
+}
 
 </script>

+ 1 - 1
src/pages/home/village/introd/tree.vue

@@ -43,7 +43,7 @@
         </FlexRow>
         <Height height="space.lg" />
         <FlexRow position="relative" center gap="gap.md" overflow="visible">
-          <PopTextAnim :show="Boolean(popAnimText)" @end="popAnimText = ''">
+          <PopTextAnim :show="Boolean(popAnimText)" :innerStyle="{ zIndex: 100 }" @end="popAnimText = ''">
             <BoxMid direction="column" center>
               <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconLight.png" :size="50" />
               <Text :text="popAnimText" fontConfig="contentText" fontSize="30rpx" color="#E79412" />

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