Explorar o código

📦 海天特殊村社首页单元整理1

快乐的梦鱼 hai 12 horas
pai
achega
a180983e4c
Modificáronse 31 ficheiros con 1705 adicións e 248 borrados
  1. BIN=BIN
      server/images/village/speical/IconMap.png
  2. BIN=BIN
      server/images/village/speical/IconSpeacker.png
  3. BIN=BIN
      server/images/village/speical/IconSpeackerStore.png
  4. BIN=BIN
      server/images/village/speical/IconStoryStation.png
  5. BIN=BIN
      server/images/village/speical/IconWrite.png
  6. 3 3
      src/api/CommonContent.ts
  7. 4 1
      src/api/inhert/VillageInfoApi.ts
  8. 4 2
      src/common/components/parts/Box2LineImageRightShadow.vue
  9. 5 1
      src/common/components/parts/Box2LineLargeImageUserShadow.vue
  10. 24 2
      src/common/components/parts/HomeTitle.vue
  11. 54 0
      src/common/components/parts/IndexRoundBoxSimpleItem.vue
  12. 9 2
      src/common/components/parts/IndexRoundSimpleItem.vue
  13. 78 16
      src/common/components/parts/RoundTags.vue
  14. 1 1
      src/common/config/Theme.ts
  15. 9 4
      src/components/display/title/SubTitle.vue
  16. 88 6
      src/components/dynamic/minisc/MiniScript.ts
  17. 1 1
      src/components/theme/ThemeDefine.ts
  18. 29 8
      src/pages/home/village/introd/card.vue
  19. 6 3
      src/pages/home/village/introd/card/blocks/GridButtonBlock4.vue
  20. 58 0
      src/pages/home/village/introd/card/blocks/GridButtonBlockHorizontai.vue
  21. 29 32
      src/pages/home/village/introd/card/blocks/OfficialAccountPublish.vue
  22. 83 0
      src/pages/home/village/introd/card/blocks/RichBlock.vue
  23. 59 0
      src/pages/home/village/introd/card/content/ContentBlocks.ts
  24. 327 0
      src/pages/home/village/introd/card/content/ContentBlocks.vue
  25. 0 0
      src/pages/home/village/introd/card/content/ContentMap.vue
  26. 0 0
      src/pages/home/village/introd/card/content/ContentSwiper.vue
  27. 42 0
      src/pages/home/village/introd/card/content/ContentTitle.vue
  28. 269 0
      src/pages/home/village/introd/card/data/DynamicData.ts
  29. 214 0
      src/pages/home/village/introd/data/DarkCard.json
  30. 11 2
      src/pages/home/village/introd/data/DefaultCard.json
  31. 298 164
      src/pages/home/village/introd/data/TestCard.json

BIN=BIN
server/images/village/speical/IconMap.png


BIN=BIN
server/images/village/speical/IconSpeacker.png


BIN=BIN
server/images/village/speical/IconSpeackerStore.png


BIN=BIN
server/images/village/speical/IconStoryStation.png


BIN=BIN
server/images/village/speical/IconWrite.png


+ 3 - 3
src/api/CommonContent.ts

@@ -56,7 +56,7 @@ export class GetContentListParams extends DataModel<GetContentListParams> {
   }
 
 
-  setMainBodyColumnId(val: number|number[]) {
+  setMainBodyColumnId(val: number|number[]|string|undefined) {
     this.mainBodyColumnId = val;
     return this;
   }
@@ -94,7 +94,7 @@ export class GetContentListParams extends DataModel<GetContentListParams> {
   /**
    * 主体栏目id
    */
-  mainBodyColumnId: number|number[] = 0;
+  mainBodyColumnId?: number|number[]|string|undefined = 0;
   /**
    * 标志:hot=热门,recommend=推荐,top=置顶
    */
@@ -309,7 +309,7 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
   public modelId: number;
   protected debugName: string;
 
-  private toStringArray(arr: number|number[]|undefined) {
+  private toStringArray(arr: number|number[]|string|undefined) {
     if (typeof arr === 'undefined') 
       return '';
     return typeof arr === 'object' ? arr.join(',') : arr.toString();

+ 4 - 1
src/api/inhert/VillageInfoApi.ts

@@ -309,6 +309,7 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
     pageSize: number, 
     keywords?: string,
     collectModuleId?: number|string|undefined,
+    catalogId?: number|undefined,
   }) {
     return (this.post<{
       total: number,
@@ -317,6 +318,7 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
       page: options.page,
       pageSize: options.pageSize,
       keywords: options.keywords,
+      catalog_id: options.catalogId || undefined,
       collect_module_id: options.collectModuleId,
       status: 4,
     }))
@@ -352,10 +354,11 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
   }
 
 
-  async getInfoForDiscover(id: number) {
+  async getInfoForDiscover(id: number, params?: Record<string, any>) {
     return (await this.post(`/village/collect/info`, '通用获取信息详情', {
       id,
       status: 4,
+      ...params
     }, undefined, CommonInfoModel)).data as CommonInfoModel
   }
 

+ 4 - 2
src/common/components/parts/Box2LineImageRightShadow.vue

@@ -9,6 +9,7 @@
     direction="row"
     justify="space-between"
     :padding="[20,10]"
+    v-bind="props"
     @click="$emit('click')"
   >
     <FlexRow width="100%" :gap="20">
@@ -33,7 +34,7 @@
         <FlexRow>
           <Tag v-if="badge" :text="badge" scheme="light" type="success" size="small" />
         </FlexRow>
-        <RoundTags v-if="tags" :tags="tags" small />
+        <RoundTags v-if="tags" :tags="tags" small tagType="round" />
       </FlexCol>
     </FlexRow>
     <Text color="primary-second-text" fontConfig="caption">{{ right }}</Text>
@@ -71,6 +72,7 @@ defineProps({
   badge: {
     type: String,
     default: '',
-  }
+  },
+  props: Object,
 })
 </script>

+ 5 - 1
src/common/components/parts/Box2LineLargeImageUserShadow.vue

@@ -17,6 +17,7 @@
       width: fixSize ? undefined : 'calc(100% - 40rpx)',
     }"
     direction="column"
+    v-bind="props"
     @click="$emit('click')"
   >
     <Image 
@@ -69,7 +70,7 @@
       <Image width="40" :src="IconChat" mode="widthFix" />
       <Text fontConfig="subText">{{ comment }}</Text>
     </FlexRow>
-    <RoundTags v-if="tags" :tags="tags" small />
+    <RoundTags v-if="tags" :tags="tags" small tagType="round" />
     <FlexRow v-if="bottomTime" :gap="20">
       <Image width="40" :src="IconTime" mode="widthFix" />
       <Text fontConfig="subText">{{ bottomTime }}</Text>
@@ -161,6 +162,9 @@ defineProps({
   bottomTime: {
     type: String,
   },
+  props: {
+    type: Object,
+  },
 })
 </script>
 

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

@@ -3,20 +3,26 @@
     :title="title" 
     :moreText="moreText"
     :showMore="showMore" 
+    :center="center"
     :padding="[showTopMargin ? 25 : 5, 0, 5, 0]"
     @moreClicked="emit('moreClicked')"
   >
-    <template #icon>
+    <template v-if="showIcon" #icon>
       <FlexRow align="center">
         <Image :src="icon" :width="66" :height="42" mode="widthFix" />
         <Width :width="15" />
       </FlexRow>
     </template>
     <template #title> 
-      <FlexRow align="center">
+      <FlexRow v-if="size === 'large'" align="center">
         <Text :text="titleBegin2" fontConfig="primaryTitle" color="text.titleLight" />
         <Text v-if="titleEnd" :text="titleEnd" fontConfig="primaryTitle" color="text.title" />
       </FlexRow>
+      <Text v-else-if="size === 'medium'" :text="title" fontConfig="primaryTitle" color="text.title" fontSize="fontSize.lg" />
+      <Text v-else-if="size === 'small'" :text="title" fontConfig="primaryTitle" color="text.title" fontSize="fontSize.md" />
+    </template>
+    <template v-if="subTitle" #subTitle>
+      <Text :text="subTitle" fontConfig="subText" fontSize="fontSize.sm" />
     </template>
     <template #right>
       <slot name="right" />
@@ -41,10 +47,26 @@ const props = defineProps({
     type: String,
     default: 'https://xy.wenlvti.net/app_static/images/home/HomeTitleIcon.png',
   },
+  subTitle: {
+    type: String,
+    default: '',
+  },
+  center: {
+    type: Boolean,
+    default: false,
+  },
   showMore: {
     type: Boolean,
     default: false,
   },
+  showIcon: {
+    type: Boolean,
+    default: true,
+  },
+  size: {
+    type: String,
+    default: 'large',
+  },
   showTopMargin: {
     type: Boolean,
     default: true,

+ 54 - 0
src/common/components/parts/IndexRoundBoxSimpleItem.vue

@@ -0,0 +1,54 @@
+<template>
+  <Touchable
+    :key="item.id"
+    position="relative"
+    radius="radius.md"
+    gap="gap.lg"
+    direction="column"
+    center
+    overflow="hidden"
+    :flexShrink="0"
+    v-bind="item.props"
+    @click="emit('click', item)"
+  >
+    <Image 
+      v-if="item.image"
+      :src="item.image" 
+      :width="iconSize" 
+      :height="iconSize" 
+      mode="aspectFill" 
+    />
+    <Text v-if="item.title" fontConfig="contentSpeicalText" fontFamily="SongtiSCBlack" :lines="1" :wrap="false">{{ item.title }}</Text>
+    <Text v-if="item.desc" fontSize="fontSize.sm" :text="item.desc" :lines="1" :maxWidth="400" />
+    <RoundTags v-if="item.tags" :gap="item.tags?.length > 0" :tags="item.tags" small tagType="round" />
+  </Touchable>
+</template>
+
+<script setup lang="ts">
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import RoundTags from './RoundTags.vue';
+
+const props = withDefaults(defineProps<{
+  item: {
+    id?: string|number;
+    title?: string;
+    desc?: string;
+    image?: string;
+    tags?: string[];
+    props?: object;
+  },
+  iconSize?: number;
+}>(), {
+  iconSize: 60,
+})
+
+const emit = defineEmits(['click'])
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
+</script>

+ 9 - 2
src/common/components/parts/IndexRoundSimpleItem.vue

@@ -10,10 +10,15 @@
     overflow="hidden"
     border="1px solid #fff"
     :flexShrink="0"
-    width="48%"
+    :width="half ? '48%' : '100%'"
     @click="emit('click', item)"
   >
-    <Image :src="item.image" :width="400" :height="imageHeight" mode="aspectFill" />
+    <Image 
+      :src="item.image" 
+      :width="half ? 320 : 640" 
+      :height="imageHeight" 
+      mode="aspectFill" 
+    />
     <FlexCol position="absolute" :inset="{ b: -1, l: -1, r: -1 }" backgroundColor="background.primary" padding="padding.md" gap="gap.md">
       <FlexRow justify="space-between" align="center">
         <Text fontConfig="contentSpeicalText" fontFamily="SongtiSCBlack" :lines="1" :wrap="false">{{ item.title }}</Text>
@@ -40,7 +45,9 @@ const props = withDefaults(defineProps<{
     image?: string;
   },
   imageHeight?: number;
+  half?: boolean;
 }>(), {
+  half: true,
   imageHeight: 270,
 })
 

+ 78 - 16
src/common/components/parts/RoundTags.vue

@@ -1,31 +1,77 @@
 <template>
-  <scroll-view scroll-x>
+  <scroll-view scroll-x :style="{ width: width }">
     <FlexRow gap="gap.md">
-      <BackgroundBox 
-        v-for="(tag, k) in tags"
-        :key="k" 
-        :backgroundImage="tag === active ? 
-          'https://xy.wenlvti.net/app_static/images/village/TagActive.png' : 
-          'https://xy.wenlvti.net/app_static/images/village/TagNormal.png'"
-        :backgroundCutBorder="[10, 10, 10, 10]"
-        :backgroundCutBorderSize="[10, 10, 10, 10]"
-        :padding="[20, 30]"
-        :text="tag"
-      >
-        <Touchable @click="emit('update:active', tag)">
-          <Text :text="tag" fontConfig="contentText" :color="tag === active ? 'white' : 'text.content'" /> 
+      <template v-if="tagType === 'round'">
+        <Touchable
+          v-for="(tag, k) in tags"
+          :key="k" 
+          radius="radius.lg"
+          :padding="small ? [15, 20] : [20, 30]"
+          :backgroundColor="tag === active ? 'primary' : 'button'"
+          @click="emit('update:active', tag)"
+        >
+          <Text 
+            :text="tag" 
+            fontConfig="contentText" 
+            :fontSize="small ? 'fontSize.sm' : 'fontSize.md'"
+            :color="tag === active ? 'white' : 'text.content'" 
+            :wrap="false"
+          /> 
         </Touchable>
-      </BackgroundBox>
+      </template>
+      <template v-else-if="tagType === 'line'">
+        <Touchable
+          v-for="(tag, k) in tags"
+          :key="k" 
+          :padding="small ? [15, 20] : [20, 30]"
+          :innerStyle="{
+            borderBottom: `1px solid ${tag === active ? activeBorderColor : 'transparent'}`,
+          }"
+          @click="emit('update:active', tag)"
+        >
+          <Text 
+            :text="tag" 
+            fontConfig="contentText" 
+            :fontSize="small ? 'fontSize.sm' : 'fontSize.md'"
+            :wrap="false"
+            :color="tag === active ? 'primary' : 'text.content'" 
+          /> 
+        </Touchable>
+      </template>
+      <template v-else>
+        <BackgroundBox 
+          v-for="(tag, k) in tags"
+          :key="k" 
+          :backgroundImage="tag === active ? 
+            'https://xy.wenlvti.net/app_static/images/village/TagActive.png' : 
+            'https://xy.wenlvti.net/app_static/images/village/TagNormal.png'"
+          :backgroundCutBorder="[10, 10, 10, 10]"
+          :backgroundCutBorderSize="[10, 10, 10, 10]"
+          :padding="small ? [10, 15] : [15, 20]"
+          :text="tag"
+        >
+          <Touchable @click="emit('update:active', tag)">
+            <Text 
+              :text="tag" 
+              fontConfig="contentText" 
+              :fontSize="small ? 'fontSize.sm' : 'fontSize.md'"
+              :color="tag === active ? 'white' : 'text.content'" 
+              :wrap="false"
+            /> 
+          </Touchable>
+        </BackgroundBox>
+      </template>
     </FlexRow>
   </scroll-view>
 </template>
 
 <script setup lang="ts">
-import { type PropType } from 'vue';
+import { computed, type PropType } from 'vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import Text from '@/components/basic/Text.vue';
 import Touchable from '@/components/feedback/Touchable.vue';
+import { useTheme } from '@/components/theme/ThemeDefine';
 
 const props = defineProps({
   tags: {
@@ -36,7 +82,23 @@ const props = defineProps({
     type: String,
     default: '',
   },
+  tagType: {
+    type: String as PropType<'round' | 'image' | 'line'>,
+    default: 'image',
+  },
+  small: {
+    type: Boolean,
+    default: false,
+  },
+  width: {
+    type: String,
+    default: '',
+  }
 })
 
 const emit = defineEmits(['update:active']);
+
+const theme = useTheme();
+const active = computed(() => props.active || (props.tags.length === 1 ? props.tags?.[0] : '') || '');
+const activeBorderColor = computed(() => theme.resolveThemeColor('primary'));
 </script>

+ 1 - 1
src/common/config/Theme.ts

@@ -4,7 +4,7 @@ import { DefaultTheme } from "@/components/theme/Theme";
 export function configAppTheme() {
   //修改默认主题颜色
   configTheme(false, (theme, defaultDarkTheme) => {
-    theme.colorConfigs.default.primary = '#e19579';
+    theme.colorConfigs.default.primary = '#c07b3c';
     theme.colorConfigs.pressed.primary = '#fbce7a';
     theme.colorConfigs.default.button = '#f6e9d9';
     theme.colorConfigs.background.page = '#fef2e8';

+ 9 - 4
src/components/display/title/SubTitle.vue

@@ -6,6 +6,7 @@ import Touchable from '@/components/feedback/Touchable.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Width from '@/components/layout/space/Width.vue';
 import type { TextProps } from '@/components/basic/Text';
+import FlexView from '@/components/layout/FlexView.vue';
 
 defineEmits([ 'moreClicked' ])
 const props = withDefaults(defineProps<{
@@ -17,10 +18,12 @@ const props = withDefaults(defineProps<{
   padding?: ThemePaddingOrMarginType,
   titleProps?: TextProps,
   badgeStyle?: object,
+  center?: boolean,
 }>(), {
   title: '',
   moreText: '更多',
   showMore: false,
+  center: false,
   badgeColor: 'primary',
   padding: () => [ 0, 0, 30, 0 ],
 })
@@ -37,8 +40,9 @@ const finalBadgeStyle = theme.useThemeStyle({
 </script>
 
 <template>
-  <FlexRow 
-    justify="space-between"
+  <FlexView
+    :direction="center ? 'column' : 'row'"
+    :justify="center ? 'center' : 'space-between'"
     align="center" 
     :padding="padding" 
     :backgroundColor="backgroundColor"
@@ -56,13 +60,14 @@ const finalBadgeStyle = theme.useThemeStyle({
       </slot>
       <slot name="titleSuffix" />
     </FlexRow>
+    <slot name="subTitle" />
     <FlexRow align="center">
       <slot name="right" />
-      <Touchable v-if="showMore" align="center" padding="padding.md" touchable @click="$emit('moreClicked')">
+      <Touchable v-if="showMore" align="center" touchable @click="$emit('moreClicked')">
         <Text fontConfig="subText" :text="moreText" />
         <Width :size="10" />
         <Icon icon="arrow-right" :size="26" />
       </Touchable>
     </FlexRow>
-  </FlexRow>
+  </FlexView>
 </template>

+ 88 - 6
src/components/dynamic/minisc/MiniScript.ts

@@ -1,3 +1,5 @@
+import { ObjectUtils } from "@imengyu/imengyu-utils";
+
 export type MiniScriptErrorPhase = 'lex' | 'parse' | 'runtime' | 'register';
 
 export type MiniScriptErrorDetails = {
@@ -822,8 +824,48 @@ class Parser {
 
 type ExecDiag = { readonly source: string };
 
+/** params 在脚本中的标识符名称 */
+const PARAMS_NAME = 'params';
+
+/** 深只读代理缓存:保证同一对象多次包装返回同一代理,维持对象身份一致 */
+const readonlyProxyCache = new WeakMap<object, object>();
+
+/**
+ * 将 params 包装为深度只读对象。脚本读取 params.xxx 正常,对其(含嵌套对象)赋值、
+ * 新增或删除属性会抛出 MiniScriptError。原始对象不会被修改。
+ */
+function makeReadonlyObject<T>(value: T): T {
+  if (value === null || typeof value !== 'object') return value;
+  const cached = readonlyProxyCache.get(value);
+  if (cached) return cached as T;
+  const proxy = new Proxy(value, {
+    get(target, prop, receiver) {
+      const result = Reflect.get(target, prop, receiver);
+      return makeReadonlyObject(result);
+    },
+    set(_target, prop) {
+      throw new MiniScriptError(`params 为只读参数,不能修改属性 "${String(prop)}"`, {
+        phase: 'runtime',
+      });
+    },
+    defineProperty(_target, prop) {
+      throw new MiniScriptError(`params 为只读参数,不能定义属性 "${String(prop)}"`, {
+        phase: 'runtime',
+      });
+    },
+    deleteProperty(_target, prop) {
+      throw new MiniScriptError(`params 为只读参数,不能删除属性 "${String(prop)}"`, {
+        phase: 'runtime',
+      });
+    },
+  }) as object;
+  readonlyProxyCache.set(value, proxy);
+  return proxy as T;
+}
+
 class Env {
   private readonly context: Record<string, unknown> | null;
+  private readonly params: Record<string, unknown> | null;
   /** 已封存的环境(如缓存复用的 globals)不接受赋值,写入会在下层作用域产生遮蔽变量 */
   private sealed = false;
 
@@ -831,13 +873,15 @@ class Env {
     readonly parent: Env | null,
     private readonly vars: Map<string, unknown> = new Map(),
     context?: Record<string, unknown> | null,
+    params?: Record<string, unknown> | null,
     private readonly execDiag: ExecDiag | null = null,
   ) {
     this.context = context === undefined ? null : context;
+    this.params = params === undefined ? null : params;
   }
 
   child(): Env {
-    return new Env(this, new Map(), undefined, this.execDiag);
+    return new Env(this, new Map(), undefined, undefined, this.execDiag);
   }
 
   /** 标记为只读环境,使其可跨多次 execute 安全复用 */
@@ -867,8 +911,13 @@ class Env {
     );
   }
 
+  /** params 是整体对象,以标识符 "params" 暴露给脚本,这里判断当前环境是否持有它 */
+  private isParamsName(name: string): boolean {
+    return name === PARAMS_NAME && this.params !== null;
+  }
+
   private definesHere(name: string): boolean {
-    return this.vars.has(name) || this.contextHas(name);
+    return this.vars.has(name) || this.contextHas(name) || this.isParamsName(name);
   }
 
   /**
@@ -888,6 +937,7 @@ class Env {
   get(name: string): unknown {
     if (this.vars.has(name)) return this.vars.get(name);
     if (this.contextHas(name)) return this.context![name];
+    if (this.isParamsName(name)) return this.params!;
     if (this.parent) return this.parent.get(name);
     throw new MiniScriptError(
       `未定义的标识符: ${name}`,
@@ -900,6 +950,13 @@ class Env {
   set(name: string, value: unknown): void {
     const target = this.findAssignTarget(name);
     if (target !== null) {
+      // params 为只读绑定,不能对其赋值
+      if (target.isParamsName(name)) {
+        throw new MiniScriptError(
+          `params 为只读参数,不能对 "${name}" 赋值`,
+          target.buildRuntimeErrorDetails(),
+        );
+      }
       // 命中封存环境时 target 为其下层,此处按 vars 遮蔽写入
       if (target.vars.has(name)) {
         target.vars.set(name, value);
@@ -1189,7 +1246,8 @@ function isExtensionNamespaceNode(value: unknown): value is Record<string, unkno
   ## 扩展与上下文
 
   - registerFunction(name, fn):注册全局函数;与内置同名时覆盖内置绑定。name 含点号时按路径挂到嵌套对象上(如 uni.getSystemInfo → 全局 uni.getSystemInfo)。
-  - execute(script, context):context 的自有属性作为顶层变量;赋值会回写到该对象(含脚本内新建的顶层变量)。
+  - execute(script, context, params):context 的自有属性作为顶层变量(可读可写);赋值会回写到该对象(含脚本内新建的顶层变量)。
+    params 由外部传入,作为只读参数,脚本通过 params.xxx 访问;对 params 或其属性赋值/新增/删除都会报错。
     对内置/扩展全局名的赋值只在本次执行内生效(产生遮蔽变量),不会影响后续 execute。
   - 性能:内置 + 扩展构成的全局环境在实例内缓存复用;相同脚本源文本的 AST 也会缓存(LRU,上限 64)。
     可用 clearCache() 主动释放。
@@ -1207,6 +1265,16 @@ export class MiniScript {
   private globalsCache: Env | null = null;
   /** 已解析脚本的 AST 缓存(LRU,按脚本源文本) */
   private readonly astCache = new Map<string, Program>();
+  /** 由脚本设置的固定全局变量,脚本执行时自动注入 */
+  private globalContent: Record<string, unknown> | null = null;
+
+  /**
+   * 设置全局内容变量
+   * 注意,优先级较高,会覆盖执行传入的上下文变量。
+   */
+  setGlobalContent(content: Record<string, unknown>): void {
+    this.globalContent = content;
+  }
 
   /**
    * 注册可在脚本中直接调用的函数。
@@ -1274,12 +1342,26 @@ export class MiniScript {
   }
 
   /**
-   * 执行脚本。`context` 中的键可作为顶层变量;`registerFunction` 注册的函数优先于同名内置。
+   * 执行脚本。`context` 中的键可作为顶层变量(可读可写,赋值会回写该对象);
+   * `params` 中的键作为只读参数,脚本通过 `params.xxx` 访问,不能修改。
+   * `registerFunction` 注册的函数优先于同名内置。
    * 支持 `return`;若无 `return`,取最后一条表达式语句的值。
    */
-  execute(script: string, context: Record<string, unknown> = {}): unknown {
+  execute(
+    script: string,
+    context: Record<string, unknown> = {},
+    params?: Record<string, unknown>,
+  ): unknown {
     const prog = this.parseCached(script);
-    const root = new Env(this.getGlobals(), new Map(), context, { source: script });
+    if (this.globalContent)
+      ObjectUtils.cloneValuesToObject(this.globalContent, context);
+    const root = new Env(
+      this.getGlobals(),
+      new Map(),
+      context,
+      params === undefined ? undefined : makeReadonlyObject(params),
+      { source: script },
+    );
     return runProgram(prog, root);
   }
 

+ 1 - 1
src/components/theme/ThemeDefine.ts

@@ -19,7 +19,7 @@ export type ViewStyle = Record<string, any>
 export type TextStyle = Record<string, any>
 
 export type ThemeSizeType = number|string;
-export type ThemePaddingOrMarginType = number|Array<number>|ThemePaddingMargin|string|string[];
+export type ThemePaddingOrMarginType = number|Array<number|string>|ThemePaddingMargin|string|string[];
 
 export interface ThemeConfig {
   varOverrides: Record<string, any>,

+ 29 - 8
src/pages/home/village/introd/card.vue

@@ -228,15 +228,30 @@
                 </FlexRow>
 
               </template>
+
               <!-- 排行榜 -->
               <Rank 
                 v-else-if="node.type === 'Block:Rank'" 
                 :title="node.props?.title"
                 :villageId="villageStore.currentVillage?.id ?? 0"
               />
-              <!-- 4网格 -->
-              <GridButtonBlock4 
-                v-else-if="node.type === 'Block:GridButtonBlock4'"
+              <!-- 标题 -->
+              <ContentTitle
+                v-else-if="node.type === 'Block:ContentTitle'"
+                v-bind="(node.props as ContentTitleBlockDefine)"
+                :scriptEngine="context.getScriptEngine()"
+                :getNodeContext="() => node.getNodeScriptContext?.() || {}"
+              />
+              <!-- 内容单元组 -->
+              <ContentBlocks
+                v-else-if="node.type === 'Block:ContentBlocks'"
+                v-bind="node.props"
+                :scriptEngine="context.getScriptEngine()"
+                :getNodeContext="() => node.getNodeScriptContext?.() || {}"
+              />
+              <!-- 网格按钮组 -->
+              <GridButtonBlock 
+                v-else-if="node.type === 'Block:GridButtonBlock'"
                 :title="node.props?.title"
                 :titleRightButtons="node.props?.titleRightButtons"
                 :items="node.props?.items"
@@ -250,6 +265,7 @@
                 :villageTitle="villageStore.currentVillage?.name ?? ''"
                 :title="node.props?.title"
                 :tags="tagsLoader.content.value || []"
+                :tagType="node.props?.tagType"
                 @goOfficalManage="handleGoOfficalManage"
               />
             </template>
@@ -330,6 +346,7 @@ 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 TestCard from './data/TestCard.json';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
@@ -365,7 +382,10 @@ import HomeLargeTitle from '@/common/components/parts/HomeLargeTitle.vue';
 import SkinApi from '@/api/light/SkinApi';
 import OfficialAccountPublish from './card/blocks/OfficialAccountPublish.vue';
 import Rank from './card/blocks/Rank.vue';
-import GridButtonBlock4 from './card/blocks/GridButtonBlock4.vue';
+import GridButtonBlock from './card/blocks/GridButtonBlock.vue';
+import ContentBlocks from './card/content/ContentBlocks.vue';
+import ContentTitle from './card/content/ContentTitle.vue';
+import type { ContentTitleBlockDefine } from './card/content/ContentBlocks';
 
 const emit = defineEmits<{
   (e: 'goTree'): void;
@@ -374,7 +394,6 @@ const emit = defineEmits<{
 
 const authStore = useAuthStore();
 const villageStore = useVillageStore();
-const { getIsVolunteer } = useUserTools();
 const { requireLogin } = useRequireLogin();
 const { isFollowed, onFollow, onUnFollow } = useFollow();
 const { getIsJoinedVillage } = useUserTools();
@@ -562,17 +581,19 @@ const pageScriptGlobal = reactive({
   getDefaultTags: () => defaultTags,
   navTo,
   emit,
-
+  todo: () => alert({ content: '暂未实现,敬请期待' }),
 });
 
 const topBanner = inject('topBanner') as Ref<string>;
 
 const pageThemeLoader = useSimpleDataLoader(async () => {
   assertNotNull(villageStore.currentVillage)
-  //TODO: 加载村社主题
-  const theme = (villageStore.currentVillage.currentSkinId === 1 || !villageStore.currentVillage.currentSkinId) ?
+  //加载村社主题
+  /*const theme = (villageStore.currentVillage.currentSkinId === 1 || !villageStore.currentVillage.currentSkinId) ?
     DefaultCard :
     JSON.parse((await SkinApi.getSkinInfo(villageStore.currentVillage.currentSkinId)).skinProps);
+  */
+  const theme = TestCard;
   const page = theme as DynamicXPage;
   topBanner.value = page.props?.topBanner || 'https://xy.wenlvti.net/app_static/images/home/BannerHome.png';
   return page;

+ 6 - 3
src/pages/home/village/introd/card/blocks/GridButtonBlock4.vue

@@ -16,7 +16,7 @@
     </template>
   </HomeTitle>
   <Height height="gap.md" />
-  <Grid :borderGrid="false" :mainAxisCount="4">
+  <Grid :borderGrid="false" :mainAxisCount="mainAxisCount">
     <GridItem 
       v-for="item in items" 
       :key="item.title"
@@ -37,7 +37,8 @@ import GridItem from '@/components/layout/grid/GridItem.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Width from '@/components/layout/space/Width.vue';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
+  mainAxisCount?: number,
   title: string,
   titleRightButtons?: {
     handler: string,
@@ -51,7 +52,9 @@ const props = defineProps<{
     icon: string,
     handler: string,
   }[],
-}>()
+}>(), {
+  mainAxisCount: 4,
+})
 
 const emit = defineEmits<{
   (e: 'click', script: string): void

+ 58 - 0
src/pages/home/village/introd/card/blocks/GridButtonBlockHorizontai.vue

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

+ 29 - 32
src/pages/home/village/introd/card/blocks/OfficialAccountPublish.vue

@@ -1,41 +1,38 @@
 <template>
   <!-- 文脉乡源 -->
   <FlexCol>
-    <FlexCol gap="gap.md" padding="padding.lg">
-      <HomeTitle :title="title" :showTopMargin="false">
-        <template #right>
-          <FlexRow align="center" gap="gap.md">
-            <BubbleTip
-              v-model:show="showManageTip"
-              position="bottom"
-              crossPosition="right"
-              arrowOffsetX="180rpx"
-              content="村社贴图太乱?点这里整理"
-              @contentClick="handleCloseManageTip(true)"
-              @close="handleCloseManageTip(false)"
-            >
-              <FrameButton
-                text="管理" 
-                size="small" 
-                primary
-                @click="emit('goOfficalManage', recommendTagName)"
-              />
-            </BubbleTip>
-            <FrameButton 
-              text="乡源AI帮你写" 
-              size="small" 
-              icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
-              @click="handleGoPublish()" 
-            />
-          </FlexRow>
-        </template>
-      </HomeTitle>
-      <Height height="gap.md" />
+    <FlexRow  justify="space-between" gap="gap.md" :padding="[0,'padding.lg']">
       <RoundTags 
         v-model:active="listActiveTag" 
         :tags="tags" 
+        :tagType="(tagType as 'image')"
+        small
+        width="440rpx"
       />
-    </FlexCol>
+      <FlexRow align="center" gap="gap.md" :flexShrink="0">
+        <BubbleTip
+          v-model:show="showManageTip"
+          position="bottom"
+          crossPosition="right"
+          arrowOffsetX="180rpx"
+          content="村社贴图太乱?点这里整理"
+          @contentClick="handleCloseManageTip(true)"
+          @close="handleCloseManageTip(false)"
+        >
+          <FrameButton
+            text="管理" 
+            size="small" 
+            primary
+            @click="emit('goOfficalManage', recommendTagName)"
+          />
+        </BubbleTip>
+        <FrameButton 
+          text="AI帮写" 
+          size="small"
+          @click="handleGoPublish()" 
+        />
+      </FlexRow>
+    </FlexRow>
     <OfficialAccountPublishWrap
       :topic="recommendTagName" 
       :path="recommendPath"
@@ -53,7 +50,6 @@ import { useAuthStore } from '@/store/auth';
 import { useVillageStore } from '@/store/village';
 import { navTo } from '@/components/utils/PageAction';
 import OfficialAccountPublishWrap from '@/common/components/OfficialAccountPublishWrap.vue';
-import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import RoundTags from '@/common/components/parts/RoundTags.vue';
 import BubbleTip from '@/components/feedback/BubbleTip.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
@@ -67,6 +63,7 @@ const props = defineProps<{
   villageTitle: string;
   title: string;
   tags: string[];
+  tagType: string;
 }>();
 
 const emit = defineEmits<{

+ 83 - 0
src/pages/home/village/introd/card/blocks/RichBlock.vue

@@ -0,0 +1,83 @@
+<template>
+  <view>
+    <Parse 
+      :content="content" 
+      :tagStyle="tagStyle"
+      :classStyle="classStyle"
+      :contentStyle="contentStyle"
+      @linkTap="linkTap"
+      @viewTap="viewTap"
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import Parse from '@/components/display/parse/Parse.vue';
+import { onMounted, ref, type PropType } from 'vue';
+import { doLoadDynamicDetailData, type IDynamicData } from '../data/DynamicData';
+import type { MiniScript } from '@/components/dynamic/minisc/MiniScript';
+
+const props = defineProps({
+  content: {
+    type: null,
+    default: ''
+  },
+  linkTapScript: {
+    type: String,
+    default: '',
+  },
+  viewTapScript: {
+    type: String,
+    default: '',
+  },
+  tagStyle: {
+    type: Object,
+    default: () => ({}),
+  },
+  classStyle: {
+    type: Object,
+    default: () => ({}),
+  },
+  contentStyle: {
+    type: Object,
+    default: () => ({}),
+  },
+  scriptEngine: {
+    type: Object as PropType<MiniScript>,
+    default: () => ({}),
+  },
+  getNodeContext: {
+    type: Function,
+    default: () => ({}),
+  },
+});
+
+const finalContent = ref('');
+
+async function loadContent() {
+  if (typeof props.content === 'string') {
+    finalContent.value = props.content;
+  } else if (typeof props.content === 'object' && props.content.content) {
+    finalContent.value = await doLoadDynamicDetailData(props.content.content as IDynamicData);
+  }
+}
+
+function linkTap(href: string) {
+  if (props.linkTapScript) {
+    props.scriptEngine.execute(props.linkTapScript, {
+      href
+    }) as any;
+  }
+}
+function viewTap(id: string) {
+  if (props.viewTapScript) {
+    props.scriptEngine.execute(props.viewTapScript, {
+      id
+    }) as any;
+  }
+}
+
+onMounted(() => {
+  loadContent();
+});
+</script>

+ 59 - 0
src/pages/home/village/introd/card/content/ContentBlocks.ts

@@ -0,0 +1,59 @@
+import type { IDynamicData } from "../data/DynamicData";
+
+export interface ContentTitleBlockDefine {
+  showMore?: boolean;
+  moreText?: string;
+  showIcon?: boolean;
+  showTopMargin?: boolean;
+  icon?: string;
+  lightCount?: number;
+  size?: "large" | "medium" | "small";
+  subTitle?: string;
+  center?: boolean;
+  title: string;
+  titleRightButtons?: {
+    handler: string,
+    text?: string,
+    icon?: string,
+    primary?: boolean,
+    size?: "large" | "medium" | "small",
+  }[],
+  onMoreClicked?: string;
+}
+export interface ContentBlockDefine extends ContentTitleBlockDefine {
+
+  showTitle: boolean;
+
+  style?: Record<string, string>;
+  content: IDynamicData|null;
+  count?: number;
+
+  blockProps?: Record<string, any>;
+
+  type?: 'article'
+    |'large-image2'|'horizontal-large'|'large-image'|'large-grid2'|'small-grid'
+    |'small-grid2'|'simple-article'|'simple-text'|'box-grid'
+    |'default'
+    |'RichBlock'
+    |undefined;
+
+  onContentSolve?: string;
+  onItemDetailClicked?: string;
+}
+
+export const ContentBlockType : {
+  type: ContentBlockDefine['type'],
+  label: string,
+}[] = [
+  { type: 'default', label: '默认' },
+  { type: 'article', label: '文章' },
+  { type: 'simple-article', label: '简单文章' },
+  { type: 'simple-text', label: '简单文本' },
+  { type: 'large-image', label: '大图' },
+  { type: 'large-image2', label: '大图2列' },
+  { type: 'large-grid2', label: '大图2列' },
+  { type: 'small-grid2', label: '小图2列' },
+  { type: 'small-grid', label: '小图' },
+  { type: 'horizontal-large', label: '横排大图' },
+  { type: 'RichBlock', label: '预制块:富文本块' },
+];

+ 327 - 0
src/pages/home/village/introd/card/content/ContentBlocks.vue

@@ -0,0 +1,327 @@
+<template>
+  <!--通用内容首页小分块组件-->
+  <FlexCol width="100%">
+    <!-- 分类 -->
+    <FlexCol 
+      v-for="(category,i) in categoryDatas" :key="i"
+      :innerStyle="category.style"
+    >
+      <HomeTitle
+        v-if="category.showTitle"
+        :title="category.title"
+        :showMore="category.showMore !== false"
+        :showIcon="category.showIcon !== false"
+        :showTopMargin="category.showTopMargin !== false"
+        :icon="category.icon" 
+        :size="category.size || 'medium'"
+        :subTitle="category.subTitle"
+        :center="category.center"
+        :moreText="category.moreText || '更多'"
+        @moreClicked="category.morePage" 
+      >
+        <template v-if="category.titleRightButtons" #right>
+          <FlexRow align="center" gap="gap.md">
+            <FrameButton
+              v-for="item in category.titleRightButtons"
+              :key="item.text"
+              :text="item.text" 
+              :size="item.size" 
+              :primary="item.primary || false"
+              @click="() => scriptEngine.execute(item.handler, getNodeContext(), { params: { item } })"
+            />
+            <Width :size="15" />
+          </FlexRow>
+        </template>
+      </HomeTitle>
+      <!--预制块-->
+      <template v-if="category.type === 'RichBlock'">
+        <RichBlock 
+          v-bind="category.blockProps" 
+          :scriptEngine="scriptEngine" 
+          :getNodeContext="getNodeContext"
+        />
+      </template>
+      <!--通用列表-->
+      <SimplePageContentLoader v-else-if="category.data" :loader="category.data" >
+        <FlexCol>
+          <template v-if="category.type === 'simple-article'">
+            <FlexCol :margin="[10, 0]">
+              <Box2LineImageRightShadow
+                v-for="(item, i) in category.data.content.value" 
+                :key="i" 
+                :title="item.title"
+                :titleBox="item.titleBox"
+                :desc="item.desc"
+                :tags="(item.bottomTags as string[])"
+                :props="item.props"
+                @click="category.detailsPage(item)"
+              />
+            </FlexCol>
+          </template>
+          <template v-else-if="category.type === 'large-image'">
+            <FlexCol>
+              <Box2LineLargeImageUserShadow
+                v-for="(item, i) in category.data.content.value"
+                titleColor="title-text"
+                fixSize
+                title1
+                :key="i"
+                :title="item.title"
+                :titleBox="Boolean(item.titleBox)"
+                :desc="item.desc"
+                :image="item.image"
+                :tags="(item.bottomTags as string[])"
+                :props="item.props"
+                @click="category.detailsPage(item)"
+              />
+            </FlexCol>
+          </template>
+          <template v-else-if="category.type === 'large-image2'">
+            <scroll-view scroll-x>
+              <FlexRow>
+                <Box2LineLargeImageUserShadow
+                  v-for="(item, i) in category.data.content.value"
+                  :width="400"
+                  titleColor="title-text"
+                  fixSize
+                  title1
+                  :key="i"
+                  :title="item.title"
+                  :titleBox="Boolean(item.titleBox)"
+                  :desc="item.desc"
+                  :image="item.image"
+                  :tags="(item.bottomTags as string[])"
+                  :props="item.props"
+                  @click="category.detailsPage(item)"
+                />
+              </FlexRow>
+            </scroll-view>
+          </template>
+          <template v-else-if="category.type === 'horizontal-large'">
+            <scroll-view scroll-x>
+              <FlexRow :padding="[20,0]" overflow="visible" align="stretch">
+                <Box2LineLargeImageUserShadow
+                  v-for="(item, i) in category.data.content.value"
+                  :width="400"
+                  titleColor="title-text"
+                  title1
+                  fixSize
+                  :key="i"
+                  :title="item.title"
+                  :titleBox="Boolean(item.titleBox)"
+                  :desc="item.desc"
+                  :image="item.thumbnail || item.image"
+                  :tags="(item.bottomTags as string[])"
+                  :props="item.props"
+                  @click="category.detailsPage(item)"
+                />
+              </FlexRow>
+            </scroll-view>
+          </template>
+          <template v-else-if="category.type === 'large-grid2'">
+            <FlexRow wrap align="stretch" justify="space-between" overflow="visible">
+              <Box2LineLargeImageUserShadow
+                v-for="(item, i) in category.data.content.value"
+                titleColor="title-text"
+                width="calc(50% - 10rpx)"
+                fixSize
+                :key="i"
+                :title="item.title"
+                :titleBox="Boolean(item.titleBox)"
+                :desc="item.desc"
+                :image="item.image"
+                :tags="(item.bottomTags as string[])"
+                :props="item.props"
+                @click="category.detailsPage(item)"
+              />
+            </FlexRow>
+          </template>
+          <template v-else-if="category.type === 'small-grid2'">
+            <FlexRow wrap justify="space-between" align="stretch">
+              <IndexRoundSimpleItem 
+                v-for="(tab, k) in category.data.content.value"
+                :key="k"
+                :item="{
+                  title: tab.title,
+                  image: tab.thumbnail || tab.image,
+                }"
+                :half="true"
+                :props="tab.props"
+                @click="category.detailsPage(tab)"
+              />
+            </FlexRow>
+          </template>
+          <template v-else-if="category.type === 'small-grid'">
+            <FlexCol :gap="10" align="center">
+              <IndexRoundSimpleItem 
+                v-for="(tab, k) in category.data.content.value"
+                :key="k"
+                :item="{
+                  title: tab.title,
+                  image: tab.thumbnail || tab.image,
+                }"
+                :half="false"
+                :props="tab.props"
+                :imageHeight="400"
+                @click="category.detailsPage(tab)"
+              />
+            </FlexCol>
+          </template>
+          <template v-else-if="category.type === 'simple-text'">
+            <FlexCol>
+              <Box2LineImageRightShadow
+                v-for="(item, i) in category.data.content.value"
+                titleColor="title-text"
+                :border="false"
+                fixSize
+                :key="i"
+                :title="item.title"
+                :titleBox="Boolean(item.titleBox)"
+                :desc="item.desc"
+                :showImage="false"
+                :props="item.props"
+                :tags="(item.bottomTags as string[])"
+              />
+            </FlexCol>
+          </template>
+          <template v-else-if="category.type === 'article'">
+            <Box2LineImageRightShadow
+              v-for="(item, i) in category.data.content.value"
+              titleColor="title-text"
+              fixSize
+              wideImage
+              :key="i"
+              :title="item.title"
+              :titleBox="Boolean(item.titleBox)"
+              :desc="item.desc"
+              :image="item.image"
+              :tags="(item.bottomTags as string[])"
+              :props="item.props"
+              @click="category.detailsPage(item)"
+            />
+          </template>
+          <template v-else-if="category.type === 'box-grid'">
+            <FlexRow wrap justify="space-between" align="stretch">
+              <IndexRoundBoxSimpleItem 
+                v-for="(tab, k) in category.data.content.value"
+                :key="k"
+                :item="tab"
+                :iconSize="tab.iconSize"
+                :props="tab.props"
+                @click="category.detailsPage(tab)"
+              />
+            </FlexRow>
+          </template>
+          <template v-else>
+            <Box2LineImageRightShadow
+              v-for="(item, i) in category.data.content.value"
+              titleColor="title-text"
+              fixSize
+              :key="i"
+              :title="item.title"
+              :titleBox="Boolean(item.titleBox)"
+              :desc="item.desc"
+              :image="item.image"
+              :tags="(item.bottomTags as string[])"
+              :props="item.props"
+              @click="category.detailsPage(item)"
+            />
+          </template>
+        </FlexCol>
+      </SimplePageContentLoader>
+      <text v-else>No Data</text>  
+    </FlexCol>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">;
+import { computed, onMounted, watch, type PropType } from 'vue';
+import { GetContentListItem } from '@/api/CommonContent';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { doLoadDynamicListData } from '../data/DynamicData';
+import type { ContentBlockDefine } from './ContentBlocks';
+import type { MiniScript } from '@/components/dynamic/minisc/MiniScript';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import HomeTitle from '@/common/components/parts/HomeTitle.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import RichBlock from '../blocks/RichBlock.vue';
+import Box2LineLargeImageUserShadow from '@/common/components/parts/Box2LineLargeImageUserShadow.vue';
+import Box2LineImageRightShadow from '@/common/components/parts/Box2LineImageRightShadow.vue';
+import IndexRoundSimpleItem from '@/common/components/parts/IndexRoundSimpleItem.vue';
+import Width from '@/components/layout/space/Width.vue';
+import IndexRoundBoxSimpleItem from '@/common/components/parts/IndexRoundBoxSimpleItem.vue';
+
+const props = defineProps({
+  /**
+   * 分类定义
+   */
+  categoryDefine: {
+    type: Array as PropType<ContentBlockDefine[]>,
+    default: () => [],
+  },
+  /**
+   * 父级数据
+   */
+  parentData: {
+    type: Object as PropType<any>,
+    default: () => {},
+  },
+  scriptEngine: {
+    type: Object as PropType<MiniScript>,
+    default: () => {},
+  },
+  getNodeContext: {
+    type: Function as PropType<() => Record<string, any>>,
+    default: () => {},
+  },
+});
+
+const categoryDatas = computed(() => props.categoryDefine.map(item => { 
+  if (!item.content)
+    return {
+      ...item,
+      detailsPage: () => {},
+      morePage: () => {
+        if (item.onMoreClicked)
+          props.scriptEngine.execute(item.onMoreClicked, props.getNodeContext(), {});
+      },
+      data: null,
+    };
+  return {
+    ...item,
+    detailsPage: (dataItem: GetContentListItem) => {
+      if (item.onItemDetailClicked)
+        props.scriptEngine.execute(item.onItemDetailClicked, props.getNodeContext(), { dataItem });
+    },
+    morePage: () => {
+      if (item.onMoreClicked)
+        props.scriptEngine.execute(item.onMoreClicked, props.getNodeContext(), {});
+    },
+    data: useSimpleDataLoader(async () => {
+      let res : any[] = [];
+      if (!item.content)
+        return res;
+      res = (await doLoadDynamicListData(
+        item.content, 1, item.count || 4, '', 
+        props.scriptEngine,
+        props.getNodeContext,
+      )).list;
+      if (item.onContentSolve)
+        res = props.scriptEngine.execute(item.onContentSolve || '', props.getNodeContext(), { res }) as any;
+      return res;
+    }, false)
+  }
+}));
+
+function loadCategoryDatas() {
+  categoryDatas.value.forEach(item => {
+    if (item.data)
+      item.data.load(true);
+  })
+}
+
+watch(categoryDatas, loadCategoryDatas);
+onMounted( loadCategoryDatas);
+</script>

src/pages/home/village/introd/card/blocks/ContentMap.vue → src/pages/home/village/introd/card/content/ContentMap.vue


src/pages/home/village/introd/card/blocks/Rank copy 4.vue → src/pages/home/village/introd/card/content/ContentSwiper.vue


+ 42 - 0
src/pages/home/village/introd/card/content/ContentTitle.vue

@@ -0,0 +1,42 @@
+<template>
+  <HomeTitle
+    :title="props.title"
+    :showMore="props.showMore !== false"
+    :showIcon="props.showIcon !== false"
+    :showTopMargin="props.showTopMargin !== false"
+    :icon="props.icon"
+    :size="props.size"
+    :subTitle="props.subTitle"
+    :center="props.center"
+    :moreText="props.moreText || '更多'"
+    @moreClicked="props.onMoreClicked ? scriptEngine.execute(props.onMoreClicked, getNodeContext(), {}) : undefined" 
+  >
+    <template v-if="titleRightButtons" #right>
+      <FlexRow align="center" gap="gap.md">
+        <FrameButton
+          v-for="item in titleRightButtons"
+          :key="item.text"
+          :text="item.text" 
+          :size="item.size || 'small'" 
+          :primary="item.primary || false"
+          @click="() => scriptEngine.execute(item.handler, getNodeContext(), { params: { item } })"
+        />
+        <Width :size="15" />
+      </FlexRow>
+    </template>
+  </HomeTitle>
+</template>
+
+<script setup lang="ts">;
+import FlexRow from '@/components/layout/FlexRow.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import HomeTitle from '@/common/components/parts/HomeTitle.vue';
+import type { ContentTitleBlockDefine } from './ContentBlocks';
+import type { MiniScript } from '@/components/dynamic/minisc/MiniScript';
+import Width from '@/components/layout/space/Width.vue';
+
+const props = defineProps<ContentTitleBlockDefine & {
+  scriptEngine: MiniScript,
+  getNodeContext: () => Record<string, any>,
+}>();
+</script>

+ 269 - 0
src/pages/home/village/introd/card/data/DynamicData.ts

@@ -0,0 +1,269 @@
+import CommonContent, { GetContentListParams } from "@/api/CommonContent";
+import VillageInfoApi from "@/api/inhert/VillageInfoApi";
+import NotConfigue from "@/api/NotConfigue";
+import type { MiniScript } from "@/components/dynamic/minisc/MiniScript";
+
+/**
+ * 默认列表动态数据接口定义 - 静态数据
+ */
+export interface IDynamicDataStaticData {
+  type: 'staticData',
+  data: Record<string, any>[],
+}
+/**
+ * 默认列表动态数据接口定义 - 从父级指定键内容
+ */
+export interface IDynamicDataParentKey {
+  type: 'parentKey',
+  key: string,
+}
+/**
+ * 默认列表动态数据接口定义 - 请求内容
+ */
+export interface IDynamicDataRequest {
+  type: 'request',
+  method: "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE",
+  url: string,
+  /**
+   * 请求参数
+   */
+  querys?: Record<string, any>,
+  /**
+   * 请求头
+   */
+  headers?: Record<string, string>,
+  /**
+   * 是否添加token
+   */
+  addToken?: boolean,
+  /**
+   * 自定义数据处理脚本
+   */
+  dataSolve?: string,
+  /**
+   * 其他参数
+   */
+  otherParams?: Record<string, any>,
+}
+
+/**
+ * 默认列表动态数据接口定义 - 通用内容
+ */
+export interface IDynamicDataCommonContent {
+  type: 'commonContent',
+  params: {
+    /**
+     * 模型ID
+     */
+    modelId: number,
+    /**
+     * 栏目ID
+     */
+    mainBodyColumnId?: string|number|number[],
+    /**
+     * 分类类型id
+     */
+    typeId?: number,
+  },
+  /**
+   * 其他参数
+   */
+  otherParams?: Record<string, any>,
+}
+/**
+ * 默认列表动态数据接口定义 - 通用详情内容
+ */
+export interface IDynamicDataDetailContent {
+  type: 'detailContent',
+  params: {
+    /**
+     * 内容ID
+     */
+    id: number,
+    /**
+     * 模型ID
+     */
+    modelId: number,
+  },
+  /**
+   * 其他参数
+   */
+  otherParams?: Record<string, any>,
+}
+/**
+ * 默认列表动态数据接口定义 - 采集列表内容
+ */
+export interface IDynamicDataVillageInfoListContent {
+  type: 'VillageInfoList',
+  params: {
+    /**
+     * 采集模块ID
+     */
+    collectModuleId: number,
+    /**
+     * 菜单ID
+     */
+    catalogId?: number,
+  },
+  /**
+   * 其他参数
+   */
+  otherParams?: Record<string, any>,
+}
+/**
+ * 默认列表动态数据接口定义 - 采集详情内容
+ */
+export interface IDynamicDataVillageInfoDetailContent {
+  type: 'VillageInfoDetail',
+  params: {
+    /**
+     * ID
+     */
+    id: number,
+  },
+  /**
+   * 其他参数
+   */
+  otherParams?: Record<string, any>,
+}
+
+export type IDynamicData = IDynamicDataCommonContent 
+  | IDynamicDataRequest
+  | IDynamicDataDetailContent
+  | IDynamicDataParentKey
+  | IDynamicDataStaticData
+  | IDynamicDataVillageInfoListContent
+  | IDynamicDataVillageInfoDetailContent
+;
+
+
+export async function doLoadDynamicListData(
+  item: IDynamicData,
+  page: number,
+  pageSize: number,
+  keywords: string,
+  scriptEngine: MiniScript,
+  getNodeContext: () => any,
+  parentData?: any,
+) : Promise<{
+  list: any[],
+  total: number,
+}> {
+  switch (item.type) {
+    default:
+      throw new Error(`未实现的动态数据接口`);
+    case 'commonContent':
+      return await CommonContent.getContentList(new GetContentListParams()
+        .setModelId(item.params.modelId)
+        .setMainBodyColumnId(item.params.mainBodyColumnId)
+        .setKeywords(keywords)
+        .setSelfValues(item.otherParams || {}) 
+      , page, pageSize);
+    case 'detailContent': {
+      const res = await CommonContent.getContentDetail(item.params.id, undefined, item.params.modelId || undefined, item.otherParams);
+      return {
+        list: page === 1 ? [res] : [],
+        total: 1,
+      };
+    }
+    case 'staticData': {
+      return {
+        list: item.data,
+        total: item.data.length,
+      };
+    }
+    case 'request': {
+      let data = (await (item.addToken ? CommonContent : NotConfigue).request<any>(
+        item.url, 
+        { ...item.querys, page, pageSize }, 
+        {
+          method: item.method, 
+          headers: item.headers,
+          data: {
+            ...item.otherParams,
+          },
+        },
+        '',
+        undefined,
+      )).data;
+      if (item.dataSolve) {
+        data = scriptEngine.execute(item.dataSolve, 
+          getNodeContext(),
+          {
+            data,
+          }
+        ) as any;
+      }
+      return data;
+    }
+    case 'VillageInfoList': {
+      return await VillageInfoApi.getListForDiscover({
+        collectModuleId: item.params.collectModuleId,
+        catalogId: item.params.catalogId,
+        page, pageSize
+    });
+    }
+    case 'VillageInfoDetail': {
+      const res = (await VillageInfoApi.getInfoForDiscover(item.params.id, item.otherParams));
+      return {
+        list: page === 1 ? [res] : [],
+        total: 1,
+      };
+    }
+    case 'parentKey': {
+      if (!parentData)
+        throw new Error(`此处不允许加载父级数据`);
+      return parentData?.[item.key];
+    }
+  }
+}
+
+export async function doLoadDynamicDetailData(
+  item: IDynamicData,
+  id?: number,
+  modelId?: number,
+  parentData?: any,
+) {
+  switch (item.type) {
+    default:
+      throw new Error(`未实现的动态数据接口`);
+    case 'commonContent':
+      return await CommonContent.getContentDetail(id || 0, undefined, modelId || undefined, item.otherParams);
+    case 'detailContent': {
+      return await CommonContent.getContentDetail(item.params.id, undefined, item.params.modelId || undefined, item.otherParams);
+    }
+    case 'staticData': {
+      return item.data;
+    }
+    case 'parentKey': {
+      if (!parentData)
+        throw new Error(`此处不允许加载父级数据`);
+      return parentData?.[item.key];
+    }
+    case 'VillageInfoList': {
+      throw new Error(`不能加载采集列表详情`);
+    }
+    case 'VillageInfoDetail': {
+      return await VillageInfoApi.getInfoForDiscover(item.params.id, item.otherParams);
+    }
+       case 'request':
+      return (await CommonContent.request(
+        item.url, 
+        { 
+          id, 
+          modelId,
+          ...item.querys, 
+        }, 
+        {
+          method: item.method, 
+          data: {
+            id,
+            modelId,
+            ...item.otherParams,
+          },
+        },
+        '',
+        undefined,
+      )).data;
+  }
+}

+ 214 - 0
src/pages/home/village/introd/data/DarkCard.json

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

+ 11 - 2
src/pages/home/village/introd/data/DefaultCard.json

@@ -93,7 +93,7 @@
             },
             {
               "name": "VILLAGE:CONTENT:Collect",
-              "type": "Block:GridButtonBlock4",
+              "type": "Block:GridButtonBlock",
               "props": {
                 "title": "魅力乡源",
                 "titleRightButtons": [
@@ -156,7 +156,7 @@
             },
             {
               "name": "VILLAGE:CONTENT:Games",
-              "type": "Block:GridButtonBlock4",
+              "type": "Block:GridButtonBlock",
               "props": {
                 "title": "活力乡源",
                 "items": [
@@ -182,6 +182,15 @@
                   }
                 ]
               }
+            },
+            {
+              "type": "Block:ContentTitle",
+              "props": {
+                "title": "文脉乡源",
+                "showMore": false,
+                "showIcon": false,
+                "showTopMargin": false
+              }
             }
           ]
         },

+ 298 - 164
src/pages/home/village/introd/data/TestCard.json

@@ -1,211 +1,345 @@
 {
   "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"
-  },
+  "props": {},
+  "style": {},
   "nodes": [
     {
       "name": "VILLAGE:CONTENT",
       "type": "flex",
       "props": {
         "direction": "column",
-        "padding": [30, 30, 0, 30],
         "gap": "gap.lg"
       },
       "nodes": [
         {
-          "name": "VILLAGE:CONTENT:Basic",
-          "type": "BackgroundBox",
+          "type": "flex",
           "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],
+            "padding": "padding.lg",
             "gap": "gap.lg"
           },
           "nodes": [
-            { "type": "Card:Basic" },
-            { "type": "Card:Level" },
-            { "type": "Card:Gallery" },
-            { "type": "Card:AddressAndMap" },
             {
-              "type": "flex",
+              "type": "BackgroundBox",
               "props": {
-                "direction": "row",
-                "justify": "center",
-                "align": "center",
-                "gap": "gap.md"
+                "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": "button",
+                  "type": "flex",
                   "props": {
-                    "size": "mini",
-                    "icon": "picture",
-                    "text": "村社相册"
+                    "direction": "row",
+                    "justify": "center",
+                    "align": "center",
+                    "gap": "gap.md"
                   },
-                  "events": {
-                    "click": "globalContext.handleGoGallery()"
-                  }
+                  "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" }
+              ]
+            },
+
+            
+            {
+              "type": "flex",
+              "props": {
+                "direction": "column",
+                "gap": "gap.md",
+                "backgroundColor": "#fffdf5",
+                "radius": "radius.lg",
+                "border": "1px solid #f1debc",
+                "padding": "padding.lg"
+              },
+              "nodes": [
                 {
-                  "type": "button",
+                  "type": "Block:ContentTitle",
                   "props": {
-                    "size": "mini",
-                    "icon": "edit-filling",
-                    "text": "编辑简介"
-                  },
-                  "events": {
-                    "click": "globalContext.handleGoEdit()"
+                    "title": "[海天特区记忆文化空间]",
+                    "subTitle": "寻访特区来路,传承海天糟神",
+                    "size": "medium",
+                    "center": true,
+                    "showMore": false,
+                    "showIcon": false,
+                    "showTopMargin": false,
+                    "onMoreClicked": "globalContext.todo()"
                   }
                 },
                 {
-                  "type": "button",
+                  "type": "Block:ContentBlocks",
                   "props": {
-                    "size": "mini",
-                    "icon": "fabulous",
-                    "text": "主页换肤"
-                  },
-                  "events": {
-                    "click": "globalContext.handleGoSkin()"
+                    "categoryDefine": [
+                      {
+                        "content": {
+                          "type": "staticData",
+                          "data": [
+                            {
+                              "title": "口述纪实馆",
+                              "image": "https://xy.wenlvti.net/app_static/images/village/speical/IconSpeackerStore.png",
+                              "tags": [ "登录" ],
+                              "props": {
+                                "width": "170rpx",
+                                "height": "220rpx",
+                                "padding": "padding.md",
+                                "backgroundColor": "#f5e8c8",
+                                "innerStyle": {
+                                }
+                              }
+                            },
+                            {
+                              "props": {
+                                "width": "170rpx",
+                                "height": "220rpx",
+                                "padding": "padding.md",
+                                "innerStyle": {
+                                  "backgroundPosition": "center",
+                                  "backgroundSize": "cover",
+                                  "backgroundImage": "url('https://xy.wenlvti.net/app_static/images/village/speical/IconWrite.png')"
+                                }
+                              }
+                            },
+                            {
+                              "title": "故事共创站",
+                              "image": "https://xy.wenlvti.net/app_static/images/village/speical/IconStoryStation.png",
+                              "tags": [ "进入" ],
+                              "props": {
+                                "width": "170rpx",
+                                "height": "220rpx",
+                                "padding": "padding.md",
+                                "backgroundColor": "#f5e8c8",
+                                "innerStyle": {
+                                }
+                              }
+                            }
+                          ]
+                        },
+                        "type": "box-grid",
+                        "onItemDetailClicked": "globalContext.handleGoCollect(17, '老建设者足迹', 85)"
+                      }
+                    ]
                   }
                 }
               ]
             },
-            { "type": "Card:Static" }
-          ]
-        },
-        {
-          "name": "VILLAGE:CONTENT:Rank",
-          "type": "Block:Rank",
-          "props": {
-            "title": "排行榜"
-          }
-        },
-        {
-          "name": "VILLAGE:CONTENT:Collect",
-          "type": "Block:GridButtonBlock4",
-          "props": {
-            "title": "魅力乡源",
-            "titleRightButtons": [
-              {
-                "text": "管理",
-                "size": "small",
-                "primary": true,
-                "handler": "globalContext.handleGoDigManage()"
-              },
-              {
-                "text": "共编村史",
-                "size": "small",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconHistory.png",
-                "handler": "globalContext.handleGoCollect()"
+
+            {
+              "name": "VILLAGE:CONTENT:Rank",
+              "type": "Block:Rank",
+              "props": {
+                "title": "排行榜"
               }
-            ],
-            "items": [
-              {
-                "title": "村社概况",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIntrod.png",
-                "handler": "globalContext.handleGoCollect(11, '村社概况')"
-              },
-              {
-                "title": "自然风光",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeEnvirounment.png",
-                "handler": "globalContext.handleGoCollect(13, '自然风光')"
-              },
-              {
-                "title": "历史沿革",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png",
-                "handler": "globalContext.handleGoCollect(2, '历史沿革')"
-              },
-              {
-                "title": "特色产业",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIndustry.png",
-                "handler": "globalContext.handleGoCollect(9, '特色产业')"
-              },
-              {
-                "title": "文艺活动",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeActivity.png",
-                "handler": "globalContext.handleGoCollect(12, '文艺活动')"
-              },
-              {
-                "title": "非遗展示",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeShow.png",
-                "handler": "globalContext.handleGoCollect(10, '非遗展示')"
-              },
-              {
-                "title": "民俗风采",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeFolkloreVibe.png",
-                "handler": "globalContext.handleGoCollect(8, '民俗风采')"
-              },
-              {
-                "title": "历史人物",
-                "icon": "https://xy.wenlvti.net/app_static/images/village/IconGoods.png",
-                "handler": "globalContext.handleGoCollect(7, '历史人物')"
+            },
+            {
+              "name": "VILLAGE:CONTENT:Collect",
+              "type": "Block:GridButtonBlock",
+              "props": {
+                "title": "魅力乡源",
+                "titleRightButtons": [
+                  {
+                    "text": "管理",
+                    "size": "small",
+                    "primary": true,
+                    "handler": "globalContext.handleGoDigManage()"
+                  },
+                  {
+                    "text": "共编村史",
+                    "size": "small",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconHistory.png",
+                    "handler": "globalContext.handleGoCollect()"
+                  }
+                ],
+                "items": [
+                  {
+                    "title": "村社概况",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIntrod.png",
+                    "handler": "globalContext.handleGoCollect(11, '村社概况')"
+                  },
+                  {
+                    "title": "自然风光",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeEnvirounment.png",
+                    "handler": "globalContext.handleGoCollect(13, '自然风光')"
+                  },
+                  {
+                    "title": "历史沿革",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png",
+                    "handler": "globalContext.handleGoCollect(2, '历史沿革')"
+                  },
+                  {
+                    "title": "特色产业",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeIndustry.png",
+                    "handler": "globalContext.handleGoCollect(9, '特色产业')"
+                  },
+                  {
+                    "title": "文艺活动",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeActivity.png",
+                    "handler": "globalContext.handleGoCollect(12, '文艺活动')"
+                  },
+                  {
+                    "title": "非遗展示",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeShow.png",
+                    "handler": "globalContext.handleGoCollect(10, '非遗展示')"
+                  },
+                  {
+                    "title": "民俗风采",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconLargeFolkloreVibe.png",
+                    "handler": "globalContext.handleGoCollect(8, '民俗风采')"
+                  },
+                  {
+                    "title": "历史人物",
+                    "icon": "https://xy.wenlvti.net/app_static/images/village/IconGoods.png",
+                    "handler": "globalContext.handleGoCollect(7, '历史人物')"
+                  }
+                ]
               }
-            ]
-          }
-        },
-        {
-          "name": "VILLAGE:CONTENT:Games",
-          "type": "Block:GridButtonBlock4",
-          "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:Games",
+              "type": "Block:GridButtonBlock",
+              "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\")"
+                  }
+                ]
               }
-            ]
-          }
+            },
+
+            {
+              "type": "Block:ContentTitle",
+              "props": {
+                "title": "文脉乡源",
+                "showMore": false,
+                "showIcon": true,
+                "showTopMargin": false
+              }
+            },
+            {
+              "type": "flex",
+              "props": {
+                "direction": "column",
+                "gap": "gap.md",
+                "backgroundColor": "#fffdf5",
+                "radius": "radius.lg",
+                "border": "1px solid #f1debc",
+                "padding": "padding.lg"
+              },
+              "nodes": [
+                {
+                  "type": "Block:ContentTitle",
+                  "props": {
+                    "title": "[特区灯火·口述纪实馆]",
+                    "size": "medium",
+                    "showMore": true,
+                    "showIcon": false,
+                    "showTopMargin": false,
+                    "onMoreClicked": "globalContext.todo()"
+                  }
+                },
+                {
+                  "type": "Block:ContentBlocks",
+                  "props": {
+                    "categoryDefine": [
+                      {
+                        "content": {
+                          "type": "staticData",
+                          "data": [
+                            {
+                              "title": "老建设者足迹",
+                              "desc": "收听建设者实录,三温奋斗岁月",
+                              "image": "https://xy.wenlvti.net/app_static/images/village/speical/IconSpeacker.png",
+                              "bottomTags": [ "收听口述" ],
+                              "props": {
+                                "radius": "radius.lg",
+                                "padding": "padding.md",
+                                "backgroundColor": "#f5e8c8"
+                              }
+                            }
+                          ]
+                        },
+                        "type": "",
+                        "onItemDetailClicked": "globalContext.handleGoCollect(17, '老建设者足迹', 85)"
+                      }
+                    ]
+                  }
+                }
+              ]
+            }
+
+          ]
         },
+
+
         {
           "name": "VILLAGE:CONTENT:Official",
           "type": "Block:Official",
           "props": {
-            "title": "文脉乡源",
-            "tags": "dynamic:globalContext.getDefaultTags()"
+            "tags": "dynamic:globalContext.getDefaultTags()",
+            "tagType": "line"
           }
         }
       ]