Browse Source

🎨 修改细节问题

快乐的梦鱼 2 weeks ago
parent
commit
c34972bb35

File diff suppressed because it is too large
+ 2 - 0
src/App.vue


+ 1 - 1
src/components/README.md

@@ -6,7 +6,7 @@ NaEasy UI 是一款简单的 UniApp 移动端UI组件库。
 
 ## 版本
 
-当前版本:1.0.8-26012501
+当前版本:1.0.9-26031103
 
 ## 版权说明
 

+ 1 - 1
src/components/basic/CellGroup.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexCol :flex="1">
+  <FlexCol>
     <text v-if="title" :style="(titleSpeicalStyle as any)">
       {{ title }}
     </text>

+ 12 - 0
src/components/basic/Icon.vue

@@ -43,6 +43,7 @@
     :innerStyle="style"
     :innerClass="innerClass"
     :src="iconData.value"
+    :showFailed="!noError"
     mode="aspectFill"
   />
 </template>
@@ -82,14 +83,25 @@ export interface IconProps {
    * 自定义类名
    */
   innerClass?: string,
+  /**
+   * 是否不显示错误图标
+   */
+  noError?: boolean,
 }
 
 const theme = useTheme();
 const props = withDefaults(defineProps<IconProps>(), {
   size: 45,
+  noError: false,
 });
 const icon = computed(() => props.icon || props.name);
 const iconData = computed(() => {
+  if (IconUtils.iconCount.value === 0) {
+    return {
+      type: 'none',
+      value: '',
+    } as IconItem
+  }
   const data = icon.value ? IconUtils.getIconDataFromMap(icon.value) : undefined;
   if (!data && icon.value && icon.value.startsWith('icon-')) {
     return {

+ 27 - 3
src/components/basic/IconUtils.ts

@@ -1,4 +1,4 @@
-import DefaultIcons from "../data/DefaultIcon.json";
+import { ref } from "vue";
 
 export type IconItem = {
   type: 'iconfont'|'image'|'svg'|'none',
@@ -13,6 +13,7 @@ type IconMap = Record<string, IconItem>;
 const iconMap = {} as IconMap;
 
 export const IconUtils = {
+  iconCount: ref(0),
   /**
    * 设置 Icon 组件的图标名称映射。
    * 如果已存在同名数据,则会覆盖之前的。
@@ -53,6 +54,7 @@ export const IconUtils = {
       } 
       iconMap[key] = result;
     }
+    this.iconCount.value = Object.keys(iconMap).length;
   },
   getColoredSvg(svg: string, color: string) {
     return toDataSvg(
@@ -74,6 +76,30 @@ export const IconUtils = {
   getIconDataFromMap(key: string) {
     return iconMap[key];
   },
+  /**
+   * 加载默认图标。可选从本地或者网络加载。
+   * @param urlOrJson 图标URL或者JSON字符串
+   */
+  loadDefaultIcons(urlOrJson: string | Record<string, string>) {
+    if (typeof urlOrJson === 'object') {
+      this.configIconMap(urlOrJson);
+      return;
+    }
+    if (urlOrJson.startsWith('http') || urlOrJson.startsWith('https')) {
+      uni.request({
+        url: urlOrJson,
+        method: 'GET',
+        success: (res) => {
+          this.configIconMap(typeof res.data === 'string' ? JSON.parse(res.data) : res.data);
+        },
+        fail: (err) => {
+          console.error('加载默认图标失败', err);
+        },
+      });
+    } else {
+      this.configIconMap(JSON.parse(urlOrJson));
+    }
+  },
 }
 
 function toDataSvg(str: string) {
@@ -81,5 +107,3 @@ function toDataSvg(str: string) {
     str = str.replace("<svg ", "<svg xmlns='http://www.w3.org/2000/svg' ");
   return `data:image/svg+xml,${encodeURIComponent(str.replace(/\'/g, '"'))}`.replace(/\#/g, '%23');
 }
-
-IconUtils.configIconMap(DefaultIcons);

+ 1 - 1
src/components/basic/Image.vue

@@ -27,7 +27,7 @@
       @error="isErrorState = true; isLoadState = false"
     />
     <view v-if="showFailed && isErrorState && !failedImage" class="inner-view error">
-      <Icon icon="warning" color="text.second" :size="32" />
+      <Icon icon="warning" color="text.second" :size="32" noError />
       <Text v-if="realWidth > 50" color="text.second" :text="src ? '加载失败' : '暂无图片'" :fontSize="22" />
     </view>
     <view v-if="showLoading && isLoadState" class="inner-view loading">

+ 10 - 0
src/components/display/parse/Parse.ts

@@ -3,6 +3,16 @@ export interface ParseNode {
    * 标签名
    */
   tag: string;
+  
+  /**
+   * 父标签名
+   */
+  parentTag?: string;
+  /**
+   * 在父节点中的索引
+   */
+  index?: number;
+
   /**
    * 标签属性
    */

+ 60 - 18
src/components/display/parse/Parse.vue

@@ -1,6 +1,12 @@
 <template>
   <view class="nana-Parse-container" :style="contentStyle">
-    <ParseNodeRender v-for="(node, index) in nodes" :key="index" :node="node" />
+    <ParseNodeRender 
+      v-for="(node, index) in nodes" 
+      :key="index" 
+      :node="node" 
+      @linkTap="emit('linkTap', $event)"
+      @viewTap="emit('viewTap', $event)"
+    />
   </view>
 </template>
 
@@ -20,18 +26,27 @@ export interface ParseProps {
    */
   tagStyle?: Record<string, string>;
   /**
+   * 类样式。键为类名,值为样式
+   */
+  classStyle?: Record<string, string>;
+  /**
    * 容器样式
    */
   contentStyle?: any;
 }
 
+const emit = defineEmits(['linkTap', 'viewTap']);
+
 const props = withDefaults(defineProps<ParseProps>(), {
-  tagStyle: () => ({})
+  tagStyle: () => ({}),
+  classStyle: () => ({}),
+  contentStyle: () => ({})
 });
 
 const praseImages = ref<string[]>([]);
 
 provide('tagStyle', toRef(props, 'tagStyle'));
+provide('classStyle', toRef(props, 'classStyle'));
 provide('praseImages', praseImages);
 
 const toObj = (attrs: DefaultTreeAdapterTypes.Element['attrs']) => {
@@ -47,25 +62,49 @@ const parseHtml = (html: string): ParseNode[] => {
   const doc = parse(html);
   praseImages.value = [];
   
-  const traverse = (element: DefaultTreeAdapterTypes.Element): ParseNode => {
+  const solveTextNode = (child: DefaultTreeAdapterTypes.TextNode, nodes: ParseNode[], parentTag?: string) => {
+    const value = child.value;
+    if (value.trim() === '') {
+      return null;
+    }
+    if (value.trim() === '\n') {
+      const node: ParseNode = {
+        tag: 'br',
+      };
+      nodes.push(node);
+      return node;
+    }
+    const node: ParseNode = {
+      tag: 'text',
+      attrs: {
+        content: value
+      },
+      parentTag,
+      index: 0
+    };
+    nodes.push(node);
+    return node;
+  }
+  const traverse = (element: DefaultTreeAdapterTypes.Element, parentTag?: string): ParseNode => {
     const node: ParseNode = {
       tag: element.tagName,
       attrs: toObj(element.attrs),
-      children: []
+      children: [],
+      parentTag,
+      index: 0
     };
     
     // 解析子节点
     if (element.childNodes) {
+      let index = 0;
       for (const child of element.childNodes) {
         if (child.nodeName === '#text') {
-          node.children?.push({
-            tag: 'text',
-            attrs: {
-              content: (child as DefaultTreeAdapterTypes.TextNode).value
-            }
-          });
+          const textNode = solveTextNode(child as DefaultTreeAdapterTypes.TextNode, node.children || [], element.tagName);
+          if (textNode)
+            textNode.index = index++;
         } else if (child.nodeName !== '#comment' && child.nodeName !== '#documentType') {
-          const childNode = traverse(child as DefaultTreeAdapterTypes.Element);
+          const childNode = traverse(child as DefaultTreeAdapterTypes.Element, element.tagName);
+          childNode.index = index++;
           node.children?.push(childNode);
           if (childNode.tag === 'img') {
             praseImages.value.push(childNode.attrs?.src as string);
@@ -76,18 +115,21 @@ const parseHtml = (html: string): ParseNode[] => {
     
     return node;
   };
+
+  let index = 0;
   for (const child of doc.childNodes) {
     if (child.nodeName === '#text') {
-      nodes.push({
-        tag: 'text',
-        attrs: {
-          content: (child as DefaultTreeAdapterTypes.TextNode).value
-        }
-      });
+      const textNode = solveTextNode(child as DefaultTreeAdapterTypes.TextNode, nodes, 'body');
+      if (textNode)
+        textNode.index = index++;
     } else if (child.nodeName !== '#documentType') {
-      nodes.push(traverse(child as DefaultTreeAdapterTypes.Element));
+      const childNode = traverse(child as DefaultTreeAdapterTypes.Element, 'body');
+      childNode.index = index++;
+      nodes.push(childNode);
     }
   }
+  console.log(doc);
+  
   return nodes;
 };
 

+ 161 - 26
src/components/display/parse/ParseNodeRender.vue

@@ -1,4 +1,6 @@
 <template>
+  <!-- 节点渲染 -->
+
   <!-- 图片 -->
   <image 
     v-if="node.tag === 'img'" 
@@ -11,7 +13,12 @@
   />
   
   <!-- 换行 -->
+  <!-- #ifdef H5 -->
+  <br v-else-if="node.tag === 'br'" />
+  <!-- #endif -->
+  <!-- #ifndef H5 -->
   <text v-else-if="node.tag === 'br'">\n</text>
+  <!-- #endif -->
   
   <!-- 链接 -->
   <view 
@@ -65,6 +72,80 @@
     <InjectMPRender :type="node.attrs?.type as string || ''" v-bind="node.attrs" />
   </view>
 
+  <!-- 表格 -->
+  <view
+    v-else-if="node.tag === 'table'"
+    :id="node.attrs?.id"
+    :class="'_parse_table ' + (node.attrs?.class || '')"
+    :style="style"
+  >
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+  <view
+    v-else-if="node.tag === 'thead' || node.tag === 'tbody'"
+    :class="node.tag === 'thead' ? '_parse_thead' : '_parse_tbody'"
+    :style="style"
+  >
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+  <view
+    v-else-if="node.tag === 'tr'"
+    :class="'_parse_tr ' + (node.attrs?.class || '')"
+    :style="style"
+  >
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+  <view
+    v-else-if="node.tag === 'th'"
+    :class="'_parse_th ' + (node.attrs?.class || '')"
+    :style="style"
+  >
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+  <view
+    v-else-if="node.tag === 'td'"
+    :class="'_parse_td ' + (node.attrs?.class || '')"
+    :style="style"
+  >
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+
+  <!-- 特殊标签,例如 ol ul 需要在前缀加序号 -->
+  <view v-else-if="node.tag === 'li'" 
+    class="_ol_ul_container"
+    :style="style"
+    :class="node.attrs?.class || ''"
+  >
+    <text v-if="node.parentTag === 'ol'" class="_ol_prefix">·</text>
+    <text v-else class="_ul_prefix">{{ (node.index || 0) + 1 }}.</text>
+    
+    <ParseNodeRender
+      v-for="(child, index) in node.children"
+      :key="index"
+      :node="child"
+    />
+  </view>
+
   <!-- 其他标签 -->
   <view
     v-else-if="node.tag !== 'text'"
@@ -72,6 +153,7 @@
     :data-tag="node.tag"
     :class="node.attrs?.class || ''"
     :style="style"
+    @click="viewTap(node.attrs?.id)"
   >
     <ParseNodeRender
       v-for="(child, index) in node.children"
@@ -101,26 +183,29 @@ const props = withDefaults(defineProps<{
 }>(), {
 });
 
+const emit = defineEmits(['linkTap', 'viewTap']);
 const tagStyle = inject<Ref<Record<string, string>>>('tagStyle', ref({}));
+const classStyle = inject<Ref<Record<string, string>>>('classStyle', ref({}));
 const praseImages = inject<Ref<string[]>>('praseImages', ref([]));
+// 与 HTML 标准默认样式(UA 样式)一致,参考 CSS 2.1 / HTML5 规范
 const builtInStyles = {
-  // 标题标签
+  // 标题标签 (CSS 2.1 suggested defaults)
   'h1': 'font-size: 2em; font-weight: bold; margin: 0.67em 0;',
-  'h2': 'font-size: 1.5em; font-weight: bold; margin: 0.83em 0;',
-  'h3': 'font-size: 1.17em; font-weight: bold; margin: 1em 0;',
-  'h4': 'font-size: 1em; font-weight: bold; margin: 1.33em 0;',
-  'h5': 'font-size: 0.83em; font-weight: bold; margin: 1.67em 0;',
+  'h2': 'font-size: 1.5em; font-weight: bold; margin: 0.75em 0;',
+  'h3': 'font-size: 1.17em; font-weight: bold; margin: 0.83em 0;',
+  'h4': 'font-size: 1em; font-weight: bold; margin: 1.12em 0;',
+  'h5': 'font-size: 0.83em; font-weight: bold; margin: 1.5em 0;',
   'h6': 'font-size: 0.67em; font-weight: bold; margin: 2.33em 0;',
-  
+
   // 段落和引用
   'p': 'margin: 1em 0;',
-  'blockquote': 'margin: 1em 0; padding: 0 1em; border-left: 4px solid #ddd; color: #666;',
-  
+  'blockquote': 'margin: 1em 0; border-left: 4px solid #ddd; padding-left: 1em; color: #666;',
+
   // 列表
-  'ul': 'margin: 1em 0; padding-left: 2em;',
-  'ol': 'margin: 1em 0; padding-left: 2em;',
+  'ul': 'margin: 1em 0; padding-left: 40px;',
+  'ol': 'margin: 1em 0; padding-left: 40px;',
   'li': 'margin: 0.5em 0;',
-  
+
   // 强调标签
   'b': 'font-weight: bold;',
   'strong': 'font-weight: bold;',
@@ -128,27 +213,36 @@ const builtInStyles = {
   'em': 'font-style: italic;',
   'u': 'text-decoration: underline;',
   'del': 'text-decoration: line-through;',
-  
-  // 代码相关
-  'code': 'font-family: monospace; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px;',
-  'pre': 'font-family: monospace; background: #f5f5f5; padding: 1em; border-radius: 3px; overflow: auto;',
-  
+
+  // 代码相关 (UA 仅 monospace,无背景)
+  'code': 'font-family: monospace;',
+  'pre': 'font-family: monospace; white-space: pre; margin: 1em 0;',
+
   // 其他内联标签
-  'mark': 'background-color: #ffeb3b; padding: 0.2em;',
-  'sup': 'font-size: 0.83em; vertical-align: super;',
-  'sub': 'font-size: 0.83em; vertical-align: sub;',
-  'small': 'font-size: 0.83em;',
-  'large': 'font-size: 1.17em;',
-  
-  // 分隔线
-  'hr': 'border: 0; border-top: 1px solid #ddd; margin: 1em 0;'
+  'mark': 'background-color: yellow; color: black;',
+  'sup': 'font-size: smaller; vertical-align: super;',
+  'sub': 'font-size: smaller; vertical-align: sub;',
+  'small': 'font-size: smaller;',
+  'large': 'font-size: larger;',
+
+  // 分隔线 (CSS 2.1: margin 0.5em 0,inset 在小程序端用 solid 兼容)
+  'hr': 'border: none; border-top: 1px solid #ccc; margin: 0.5em 0;',
+
+  // 表格(简单布局,无 table-layout)
+  'table': 'display: block; width: 100%; margin: 1em 0; overflow-x: auto;',
+  'thead': 'display: block;',
+  'tbody': 'display: block;',
+  'tr': 'display: flex; flex-direction: row;',
+  'th': 'font-weight: bold; padding: 8px 10px; border-right: 1px solid #ddd; border-bottom: 1px solid #ddd; flex: 1; min-width: 0; box-sizing: border-box;',
+  'td': 'padding: 8px 10px; border-right: 1px solid #ddd; border-bottom: 1px solid #ddd; flex: 1; min-width: 0; box-sizing: border-box;'
 } as Record<string, string>;
 const style = computed(() => 
   [
-    (props.node.attrs?.style || ''), 
     (builtInStyles[props.node.tag] || ''),
-    isInline.value ? 'display:inline' : '',
     (tagStyle.value[props.node.tag] || ''),
+    (classStyle.value[props.node.attrs?.class as string] || ''),
+    isInline.value ? 'display:inline' : '',
+    (props.node.attrs?.style || ''), 
   ].join(';'),
 );
 const isInline = computed(() => [
@@ -160,6 +254,7 @@ const isInline = computed(() => [
 // 链接点击事件
 const linkTap = (e: any) => {
   const href = props.node.attrs?.href as string;
+  emit('linkTap', href);
   if (href) {
     if (href[0] === '#') {
       // 跳转锚点
@@ -206,6 +301,10 @@ const linkTap = (e: any) => {
   }
 };
 
+const viewTap = (e: any) => {
+  emit('viewTap', e);
+};
+
 function preview(url: string) {
   if (url) {
     if (praseImages.value.includes(url)) {
@@ -249,6 +348,42 @@ defineOptions({
   -webkit-touch-callout: none;
 }
 
+/* ol ul 容器 */
+._ol_ul_container {
+  display: block;
+}
+/* ol/ul 前缀 */
+._ol_prefix,
+._ul_prefix {
+  display: inline-block;
+  padding-right: 10px;
+}
+
+/* 表格:用 view + flex 模拟,无 table 标签;仅画右、下边框,首列补左边框,首行补上边框 */
+._parse_table {
+  border: 1px solid #ddd;
+  border-radius: 4px;
+}
+._parse_thead ._parse_tr {
+  background-color: #f5f5f5;
+}
+._parse_thead ._parse_tr ._parse_th,
+._parse_thead ._parse_tr ._parse_td {
+  border-top: 1px solid #ddd;
+}
+._parse_tbody ._parse_tr:first-child ._parse_th,
+._parse_tbody ._parse_tr:first-child ._parse_td {
+  border-top: 1px solid #ddd;
+}
+._parse_tr ._parse_th:first-child,
+._parse_tr ._parse_td:first-child {
+  border-left: 1px solid #ddd;
+}
+._parse_th,
+._parse_td {
+  word-break: break-all;
+}
+
 /* 视频默认效果 */
 ._video {
   width: 300px;

+ 2 - 0
src/components/form/Rate.vue

@@ -202,6 +202,8 @@ function handleDrag(x: number) {
   if (v === 0 && !props.canbeZero)
     v = props.half ? 0.5 : 1;
 
+  v = Math.max(0, Math.min(v, props.count));
+
   if (v !== value.value) {
     updateValue(v);
   }

+ 7 - 0
src/components/keyboard/NumberKeyBoard.vue

@@ -4,6 +4,7 @@
     closeable
     :closeIcon="false"
     :mask="mask === true"
+    :size="size"
     position="bottom"
     @close="onClose"
   >
@@ -39,6 +40,11 @@ export interface NumberKeyBoardProps extends NumberKeyBoardInnerProps {
    * @default false
    */
   mask?: boolean;
+  /**
+   * 键盘大小
+   * @default 'auto'
+   */
+  size?: string|number;
 }
 
 const emit = defineEmits([ 'delete', 'input', 'finish', 'update:show' ]);
@@ -47,6 +53,7 @@ const props = withDefaults(defineProps<NumberKeyBoardProps>(), {
   keyPressedImpactFeedback: true,
   keyRandomOrder: false,
   showCloseButton: true,
+  size: 'auto',
 });
 
 const onClose = () => {

+ 7 - 0
src/components/keyboard/PlateKeyBoard.vue

@@ -4,6 +4,7 @@
     closeable
     :closeIcon="false"
     :mask="mask === true"
+    :size="size"
     position="bottom"
     @close="onClose"
   >
@@ -41,6 +42,11 @@ export interface PlateKeyBoardProps extends PlateKeyBoardInnerProps {
    * @default false
    */
   mask?: boolean;
+  /**
+   * 键盘大小
+   * @default 'auto'
+   */
+  size?: string|number;
 }
 
 const emit = defineEmits([ 'delete', 'input', 'finish', 'cancel', 'update:show' ]);
@@ -48,6 +54,7 @@ const props = withDefaults(defineProps<PlateKeyBoardProps>(), {
   showFinishButton: () => propGetThemeVar('PlateKeyBoardShowFinishButton', true),
   showDeleteButton: () => propGetThemeVar('PlateKeyBoardShowDeleteButton', true),
   keyPressedImpactFeedback: () => propGetThemeVar('PlateKeyBoardKeyPressedImpactFeedback', true),
+  size: 'auto',
 });
 
 const onClose = () => {

+ 18 - 14
src/pages/article/data/CommonCategoryBlocks.vue

@@ -2,7 +2,10 @@
   <!--通用内容首页小分块组件-->
   <FlexCol width="100%">
     <!-- 分类 -->
-    <template v-for="(category,i) in categoryDatas" :key="i">
+    <FlexCol 
+      v-for="(category,i) in categoryDatas" :key="i"
+      :innerStyle="category.style"
+    >
       <HomeTitle 
         v-if="category.showTitle"
         :title="category.title"
@@ -188,7 +191,7 @@
           </template>
         </FlexCol>
       </SimplePageContentLoader>  
-    </template>
+    </FlexCol>
   </FlexCol>
 </template>
 
@@ -263,12 +266,6 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
           mainBodyColumnId = params.mainBodyColumnId;
           break;
         }
-        case 'detailContent':
-        case 'parentKey':
-        case 'request':
-          toast(`此配置选项不支持默认的详情页面跳转!`);
-          console.warn(`此配置选项不支持默认的详情页面跳转!`);
-          return;
       }  
 
       const params = { id, mainBodyColumnId, modelId };
@@ -303,13 +300,16 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
             mainBodyColumnId = params.mainBodyColumnId;
             break;
           }
-          case 'detailContent':
-          case 'parentKey':
-          case 'request':
-            toast(`此配置选项不支持默认的更多页面跳转!`);
-            console.warn(`此配置选项不支持默认的更多页面跳转!`);
-            return;
         }  
+        if (item.morePage?.startsWith('dynamic:')) {
+          doCallDynamicFunction(item.morePage.substring(8), {
+            sourceData: {
+              main: {},
+              customData: {},
+            },
+          });
+          return;
+        }
         navCommonList({
           title: item.title,
           mainBodyColumnId: mainBodyColumnId,
@@ -356,6 +356,10 @@ const categoryDatas = computed(() => props.categoryDefine.map(item => {
           res = props.parentData[item.data.key];
           break;
         }
+        case 'staticData': {
+          res = item.data.data;
+          break;
+        }
         case 'request': {
           res = (await CommonContent.request(
             item.data.url, 

+ 45 - 3
src/pages/article/data/CommonCategoryDynamicData.ts

@@ -29,6 +29,8 @@ import ResultContent from "@/api/research/ResultContent";
 import { CommonCategorDynamicDropDownValuesToParams } from "./data-defines/Dropdown";
 import NewsIndexContent from "@/api/news/NewsIndexContent";
 import VillageContent from "@/api/fusion/VillageContent";
+import NotConfigue from "@/api/NotConfigue";
+import { doEvaluateDynamicDataExpression } from "./CommonCategoryDynamicEvax";
 
 export * from './data-defines/Category';
 export * from './data-defines/Dropdown';
@@ -106,8 +108,23 @@ export interface IHomeCommonCategoryDynamicDataRequest {
   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>,
@@ -119,12 +136,20 @@ export interface IHomeCommonCategoryDynamicDataParentKey {
   type: 'parentKey',
   key: string,
 }
+/**
+ * 默认列表动态数据接口定义 - 静态数据
+ */
+export interface IHomeCommonCategoryDynamicDataStaticData {
+  type: 'staticData',
+  data: Record<string, any>[],
+}
 
 export type IHomeCommonCategoryDynamicData = IHomeCommonCategoryDynamicDataCommonContent 
   | IHomeCommonCategoryDynamicDataSerializedApi 
   | IHomeCommonCategoryDynamicDataRequest
   | IHomeCommonCategoryDynamicDataParentKey
-  | IHomeCommonCategoryDynamicDataDetailContent;
+  | IHomeCommonCategoryDynamicDataDetailContent
+  | IHomeCommonCategoryDynamicDataStaticData;
 
 /**
  * 序列化接口映射表
@@ -219,12 +244,16 @@ export async function doLoadDynamicListData(
         }) 
       , page, pageSize));
     }
-    case 'request':
-      return (await CommonContent.request(
+    case 'staticData': {
+      return item.data;
+    }
+    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,
             ...CommonCategorDynamicDropDownValuesToParams(dropDownValues, dropdownDefines || [])
@@ -233,6 +262,16 @@ export async function doLoadDynamicListData(
         '',
         undefined,
       )).data;
+      if (item.dataSolve) {
+        data = doEvaluateDynamicDataExpression(item.dataSolve, {
+          sourceData: {
+            main: data,
+            customData: {},
+          },
+        });
+      }
+      return data;
+    }
   }
 }
 export async function doLoadDynamicDetailData(
@@ -249,6 +288,9 @@ export async function doLoadDynamicDetailData(
     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(`此处不允许加载父级数据`);

+ 20 - 9
src/pages/article/data/CommonCategoryDynamicEvax.ts

@@ -40,14 +40,14 @@ function parseMapExpression(mapExpression: string) {
  * CT,[key]        [N]: 暂存变量:删除
  * OP,[R],[OP],[R] [Y]: 执行操作,A为操作数,OP为操作符,B为操作数
  * 
- * 多个表达式用 / 分隔,会按顺序依次评估,并返回最后一个表达式的结果
+ * 多个表达式用换行符或者‼分隔,会按顺序依次评估,并返回最后一个表达式的结果
  * @param expression 
  * @returns 
  */
 export function doEvaluateDynamicDataExpression(expressions: string, context: IDynamicCompareExpressionContext) {
   if (!expressions)
     return undefined;
-  const expressionArr = expressions.replace(/\n/g, '/').split('/');
+  const expressionArr = expressions.replace(/\n/g, '‼').split('‼');
   const tempData = new Map<string, any>();
   
   function evaluateExpression(expression: string, prevResult: any) : any {
@@ -240,7 +240,7 @@ export function doEvaluateDynamicDataExpression(expressions: string, context: ID
   for (const expression of expressionArr)
     result = evaluateExpression(expression, result);
 
-  //console.log('doEvaluateDynamicDataExpression: ', expressions, 'result: ', result);
+  console.log('doEvaluateDynamicDataExpression: ', expressions, 'result: ', result);
   return result;
 }
 /**
@@ -320,7 +320,7 @@ export function doEvaluateDynamicCompareExpression(expression: string, context:
  * #jumpMp#openOfficialAccountArticle:A,K#main.externalLink/
  * #default#navigateToAutoContent:K#main
  * 
- * 多个函数用 / 分隔,会按顺序依次调用,并返回最后一个函数的返回值
+ * 多个函数用换行符分隔,会按顺序依次调用,并返回最后一个函数的返回值
  * 
  * @param functions 
  * @param context 
@@ -328,7 +328,7 @@ export function doEvaluateDynamicCompareExpression(expression: string, context:
 export async function doCallDynamicFunction(functions: string, context: IDynamicCompareExpressionContext) {
   if (functions.startsWith('dynamic:'))
     functions = functions.substring(8);
-  const functionArr = functions.replace(/\n/g, '/').split('/');
+  const functionArr = functions.replace(/\n/g, '‼').split('‼');
   let result = null;
   let currentPointer = 0;
   let breakFlag = false;
@@ -359,6 +359,8 @@ export async function doCallDynamicFunction(functions: string, context: IDynamic
     const arr = expression.split(':');
     const functionName = arr[0];
     const params = arr.slice(1);
+    console.log('22222', expression, functionName, params);
+    
     function assertParamCount(count: number) {
       if (params.length !== count)
         throw new Error('doCallDynamicFunction: invalid param count: ' + functionName);
@@ -429,12 +431,21 @@ export async function doCallDynamicFunction(functions: string, context: IDynamic
           doEvaluateDynamicDataExpression(params[1], context);
         break;
       case 'navTo': 
-        assertParamCount(2);
-        navTo(doEvaluateDynamicDataExpression(params[0], context), doEvaluateDynamicDataExpression(params[1], context));
+        assertParamCount(1);
+        console.log('11111', params[0]);
+        
+        navTo(doEvaluateDynamicDataExpression(params[0], context), params[1] ? doEvaluateDynamicDataExpression(params[1], context) : undefined);
         break;
       case 'redirectTo': 
-        assertParamCount(2);
-        redirectTo(doEvaluateDynamicDataExpression(params[0], context), doEvaluateDynamicDataExpression(params[1], context));
+        assertParamCount(1);
+        redirectTo(doEvaluateDynamicDataExpression(params[0], context), params[1] ? doEvaluateDynamicDataExpression(params[1], context) : undefined);
+        break;
+      case 'reLaunch': 
+        assertParamCount(1);
+        uni.reLaunch({
+          url: doEvaluateDynamicDataExpression(params[0], context), 
+          ...(params[1] ? doEvaluateDynamicDataExpression(params[1], context) : {})
+        })
         break;
       case 'back':
         back();

+ 1 - 1
src/pages/article/data/CommonCategoryGlobalLoader.ts

@@ -26,7 +26,7 @@ export function useCommonCategoryGlobalLoader() {
     uni.showLoading({ title: '加载中' });
     try {
       //本地开发时,使用默认配置
-      if (getIsDevtoolsPlatform()) {
+      if (false && getIsDevtoolsPlatform()) {
         commonCategoryData.value = DefaultCofig as IHomeCommonCategoryDefine;
         return;
       }

+ 4 - 0
src/pages/article/data/defines/List.ts

@@ -117,6 +117,10 @@ export interface IHomeCommonCategoryListTabNestCategoryDefine {
  */
 export interface IHomeCommonCategoryListTabNestCategoryItemDefine {
   /**
+   * 子分类外层样式
+   */
+  style?: Record<string, any>,
+  /**
    * 是否可见
    * @default true
    */

+ 38 - 0
src/pages/article/data/editor/components/DynamicDataEditor.vue

@@ -102,6 +102,12 @@
       </template>
 
       <template v-else-if="currentType === 'request'">
+        <a-form-item label="是否添加token (addToken)">
+          <a-switch
+            :checked="(modelValue as IHomeCommonCategoryDynamicDataRequest)?.addToken"
+            @change="(v: boolean) => setRequestAddToken(v)"
+          />
+        </a-form-item>
         <a-form-item label="请求方法 (method)">
           <a-select
             :value="(modelValue as IHomeCommonCategoryDynamicDataRequest)?.method"
@@ -129,8 +135,22 @@
             @update:modelValue="(v: Record<string, any>) => setRequest('otherParams', v)"
           />
         </a-form-item>
+        <a-form-item label="自定义数据处理脚本 (dataSolve)">
+          <a-input
+            :value="(modelValue as IHomeCommonCategoryDynamicDataRequest)?.dataSolve"
+            @change="(e: Event) => setRequestDataSolve((e.target as HTMLInputElement)?.value)"
+          />
+        </a-form-item>
       </template>
 
+      <template v-else-if="currentType === 'staticData'">
+        <a-form-item label="静态数据 (data)">
+          <KeyValueEditor
+            :modelValue="{ array: (modelValue as IHomeCommonCategoryDynamicDataStaticData)?.data }"
+            @update:modelValue="(v: Record<string, any>) => setStaticData('data', v.array)"
+          />
+        </a-form-item>
+      </template>
 
     </a-form>
   </div>
@@ -145,6 +165,7 @@ import type {
   IHomeCommonCategoryDynamicDataRequest,
   IHomeCommonCategoryDynamicDataDetailContent,
   IHomeCommonCategoryDynamicDataParentKey,
+  IHomeCommonCategoryDynamicDataStaticData,
 } from '@/pages/article/data/CommonCategoryDynamicData';
 import { SerializedApiMap } from '@/pages/article/data/CommonCategoryDynamicData';
 import KeyValueEditor from './KeyValueEditor.vue';
@@ -299,6 +320,23 @@ function setDetailContent(key: 'id' | 'modelId', value: number | undefined) {
   emit('update:modelValue', next);
 }
 
+function setRequestDataSolve(value: string) {
+  const cur = props.modelValue as IHomeCommonCategoryDynamicDataRequest | undefined;
+  if (!cur || cur.type !== 'request') return;
+  emit('update:modelValue', { ...cur, dataSolve: value });
+}
+function setRequestAddToken(value: boolean) {
+  const cur = props.modelValue as IHomeCommonCategoryDynamicDataRequest | undefined;
+  if (!cur || cur.type !== 'request') return;
+  emit('update:modelValue', { ...cur, addToken: value });
+}
+
+function setStaticData(key: 'data', value: Record<string, any>[]) {
+  const cur = props.modelValue as IHomeCommonCategoryDynamicDataStaticData | undefined;
+  if (!cur || cur.type !== 'staticData') return;
+  emit('update:modelValue', { ...cur, [key]: value });
+}
+
 </script>
 
 <style lang="scss" scoped>

+ 3 - 0
src/pages/article/data/editor/subpart/NestCategoryEditorItem.vue

@@ -22,6 +22,9 @@
         :indeterminate="cat.showMore === undefined"
       />
     </a-form-item>
+    <a-form-item label="外层样式">
+      <KeyValueEditor v-model="cat.style" />
+    </a-form-item>
     <a-form-item v-if="cat.showTitle !== false && cat.showMore !== false" label="更多页">
       <LinkPathEditor v-model="cat.morePage" />
     </a-form-item>

+ 50 - 1
src/pages/blocks/RichBlock.vue

@@ -1,6 +1,13 @@
 <template>
   <view>
-    <Parse :content="content" />
+    <Parse 
+      :content="content" 
+      :tagStyle="tagStyle"
+      :classStyle="classStyle"
+      :contentStyle="contentStyle"
+      @linkTap="linkTap"
+      @viewTap="viewTap"
+    />
   </view>
 </template>
 
@@ -8,12 +15,33 @@
 import Parse from '@/components/display/parse/Parse.vue';
 import { onMounted, ref } from 'vue';
 import { doLoadDynamicDetailData, type IHomeCommonCategoryDynamicData } from '../article/data/CommonCategoryDynamicData';
+import { doEvaluateDynamicDataExpression } from '../article/data/CommonCategoryDynamicEvax';
 
 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: () => ({}),
+  },
 });
 
 const finalContent = ref('');
@@ -26,6 +54,27 @@ async function loadContent() {
   }
 }
 
+function linkTap(href: string) {
+  if (props.linkTapScript) {
+    doEvaluateDynamicDataExpression(props.linkTapScript, {
+      sourceData: {
+        main: { href },
+        customData: {},
+      },
+    });
+  }
+}
+function viewTap(id: string) {
+  if (props.viewTapScript) {
+    doEvaluateDynamicDataExpression(props.viewTapScript, {
+      sourceData: {
+        main: { id },
+        customData: {},
+      },
+    });
+  }
+}
+
 onMounted(() => {
   loadContent();
 });

+ 1 - 1
src/pages/parts/Box2LineLargeImageUserShadow.vue

@@ -17,7 +17,7 @@
     <NaImage 
       v-if="image" 
       width="100%"
-      :height="300"
+      :height="320"
       :radius="15"
       :src="image" 
       mode="aspectFit"