Procházet zdrojové kódy

优化启动参数 image 解析方式

cc12458 před 2 měsíci
rodič
revize
22e1aac4e1

+ 1 - 1
src/loader/bridge.loader.ts

@@ -35,7 +35,7 @@ export default function bridgeLoader(): DEV.Loader {
   };
 
   return async function (app, config) {
-    const scanned = config.image.el.split('|').includes('scan');
+    const scanned = config.image.el?.includes('scan');
     if (platformIsAIO_1()) window.bridge = new EventTarget() as InstanceType<typeof Bridge>;
     else if (platformIsAIO()) await waitFor(() => window.bridge != null);
     window.bridge.addEventListener('scan', async ({ detail }) => {

+ 1 - 1
src/loader/debug.loader.ts

@@ -4,7 +4,7 @@ type Lib = 'eruda' | 'vconsole';
 
 export default function debugLoader(tag = 'debug', ignoreDevelop = true): DEV.Loader {
   return async function (app, config) {
-    const debug = config.image.el.split('|').includes('debug');
+    const debug = config.image.debug;
     if (import.meta.env.DEV && ignoreDevelop && !debug) return;
 
     const query = getURLSearchParams();

+ 8 - 2
src/loader/launch.loader.ts

@@ -8,17 +8,23 @@ export default function launchLoader(container = '#app'): DEV.Loader {
     app.use(router);
     app.use(pinia);
 
+    let currentRoute = router.currentRoute.value.fullPath;
+    if (currentRoute === '/') currentRoute = '/screen';
+
     if (config.image.com) {
       const component = components[`/src/pages/${config.image.com}.vue`];
       if (!component) throw { message: `配置 ${config.image.com} 组件未找到` };
+      const path = '/screen';
       router.addRoute({
         name: 'screen',
-        path: config.image.page,
+        path,
         component,
         meta: { scan: true },
       });
+      if (currentRoute === path) await router.replace(path);
     }
-    if (config.image.page && config.image.page !== router.currentRoute.value.fullPath) await router.replace(config.image.page);
+
+    if (config.image.page && config.image.page !== currentRoute) await router.replace(config.image.page);
     useFlowStore().$init(config.flowConfig)
 
     app.mount(container);

+ 1 - 1
src/pages/scan.page.vue

@@ -7,7 +7,7 @@ import Start from '@/components/Start.vue';
 const { data } = useRequest(getApplicationMethod, { initialData: { image: { el: '', copyright: '' } } });
 const title = computed(() => data.value.image.title || import.meta.env.SIX_APP_TITLE);
 const copyright = computed(() => data.value.copyright);
-const button = computed(() => data.value.image.el.split('|').includes('btn'));
+const button = computed(() => data.value.image.el?.includes('btn'));
 </script>
 
 <template>

+ 1 - 1
src/pages/screen.page.vue

@@ -11,7 +11,7 @@ import Start from '@/components/Start.vue';
 const { data } = useRequest(getApplicationMethod, { initialData: { image: { el: '', copyright: '' } } });
 const title = computed(() => data.value.image.title || import.meta.env.SIX_APP_TITLE);
 const copyright = computed(() => data.value.copyright);
-const button = computed(() => data.value.image.el.split('|').includes('btn'));
+const button = computed(() => data.value.image.el?.includes('btn'));
 
 const container = useTemplateRef<HTMLDivElement>('container');
 const { width, height } = useElementSize(container);

+ 16 - 11
src/request/model/index.ts

@@ -1,4 +1,4 @@
-import { analysis } from '@/tools/regex';
+import { analysis, getURLSearchParams } from '@/tools';
 
 export * from './register.model';
 export * from './analysis.model';
@@ -9,20 +9,25 @@ export * from './scheme.model';
 export function application(data: Record<string, any>) {
   const image = analysis(
     [
-      ['preset', '\\d+', '1'],
-      ['el', '[\\w|]+', ''],
-      ['page', '/\\w+'],
-      ['com', '[^;]+'],
-      ['title', '[^;]+'],
+      {key: 'preset', pattern: Number, default: 1},
+      {key: 'el', pattern: String, multiple: true },
+      {key: 'page', pattern: String},
+      {key: 'com', pattern: String},
+      {key: 'title', pattern: String},
+      {key: 'debug', pattern: Boolean},
     ] as const,
-    data?.tabletDrainageImage
+    [data?.tabletDrainageImage, getURLSearchParams().get('image')].filter(Boolean).map(decodeURIComponent).join('&')
   );
-  if (!image.el) image.el = void 0 as any;
-  if (image.preset === '2') {
-    image.el ??= `scan`;
+
+  if (image.preset === 1) {
+    if (!image.el) image.el = ['btn'];
+  } else if (image.preset === 2) {
+    if (!image.el) image.el = ['scan'];
     image.com ??= `scan.page`
     image.title ??= `萧山区中医智能辅诊系统`
-  } else image.el ??= `btn`;
+  }
+  image.el ??= [];
+  image.debug ??= image.el.includes('debug');
 
   return {
     copyright: [data?.partner, data?.technicalSupporter].filter(Boolean).join('<br>'),

+ 1 - 0
src/tools/index.ts

@@ -1,4 +1,5 @@
 export * from './url.tool';
 export * from './string.tool';
+export * from './params';
 export * from './polyfills';
 export * from './uuid.tool';

+ 187 - 0
src/tools/params.ts

@@ -0,0 +1,187 @@
+type Primitive = string | number | boolean;
+type PatternToType<P> =
+  P extends NumberConstructor ? number :
+    P extends StringConstructor ? string :
+      P extends BooleanConstructor ? boolean :
+        P extends RegExp ? string :
+          unknown;
+interface FieldSchema<Key extends string = string, T = Primitive> {
+  key: Key;
+  /**
+   * 校验规则
+   */
+  pattern?: NumberConstructor | StringConstructor | BooleanConstructor | RegExp;
+  /**
+   * 自定义解析函数
+   */
+  parse?: (raw: string) => T;
+  /**
+   * 默认值
+   */
+  default?: T;
+  /**
+   * 是否允许重复字段
+   */
+  multiple?: boolean;
+  /** 是否必填(无 default 时使用) */
+  required?: boolean;
+}
+
+type InferField<S extends FieldSchema> = S['multiple'] extends true ? PatternToType<S['pattern']>[] : PatternToType<S['pattern']>;
+
+
+type IsRequired<S extends FieldSchema> = S['default'] extends undefined
+  ? (S['required'] extends true ? true : false)
+  : true;
+
+export type InferResult<T extends readonly FieldSchema[]> =
+  & { [S in T[number] as IsRequired<S> extends true ? S['key'] : never]: InferField<S>; }
+  & { [S in T[number] as IsRequired<S> extends true ? never : S['key']]?: InferField<S>; };
+
+
+export interface AnalysisOptions {
+  /**
+   * 严格模式
+   */
+  strict?: boolean;
+}
+
+/**
+ * Streaming normalize
+ * @description 支持
+ *  - 标准 URL (a=1&b=2)
+ *  - DSL 语法 (a:1;b:2)
+ *  - pipe 语法 (el:a|b|c → el=a&el=b&el=c)
+ *  - JSON 自动识别 (meta={"x":1,"y":[1,2]})
+ * @param input
+ * @return 标准 URL
+ */
+function normalizeInput(input?: string): string {
+  if (!input) return '';
+  // 去掉前导 ?
+  const source = input.startsWith('?') ? input.slice(1) : input;
+
+  const result: string[] = [];
+  let key = '';
+  let value = '';
+  let readingKey = true;
+  let inJSON = false;
+  let jsonDepth = 0;
+
+  const pushPair = () => {
+    if (!key) return;
+    if (value.includes('|') && !inJSON) value.split('|').forEach((value) => result.push(`${key}=${value}`));
+    else result.push(`${key}=${value}`);
+    key = '';
+    value = '';
+    readingKey = true;
+  };
+
+  const jsonStart = ['{', '['];
+  const jsonEnd = ['}', ']'];
+
+  for (const char of source) {
+    // JSON 开始
+    if (!inJSON && jsonStart.includes(char)) {
+      inJSON = true;
+      jsonDepth = 1;
+      value += char;
+      continue;
+    }
+    // JSON 内部
+    if (inJSON) {
+      value += char;
+      if (jsonStart.includes(char)) jsonDepth++;
+      if (jsonEnd.includes(char)) jsonDepth--;
+      if (jsonDepth === 0) inJSON = false;
+      continue;
+    }
+    if (['&',';'].includes(char) && !inJSON) {
+      pushPair();
+      continue;
+    }
+    if ([':', '='].includes(char) && readingKey) {
+      readingKey = false;
+      continue;
+    }
+    if (readingKey) key += char;
+    else value += char;
+  }
+
+  pushPair();
+  return result.join('&');
+}
+
+/**
+ * 自动解析 JSON
+ */
+function autoParse(raw: string): any {
+  const trimmed = raw.trim();
+  if (
+    (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
+    (trimmed.startsWith('[') && trimmed.endsWith(']'))
+  ) try { return JSON.parse(trimmed); } catch { return raw; }
+  return raw;
+}
+
+/**
+ * 自动解析并转换 pattern 类型
+ */
+function parseValue(raw: string, pattern?: FieldSchema['pattern'], parseFn?: FieldSchema['parse'], strict?: boolean): any {
+  let value = parseFn ? parseFn(raw) : autoParse(raw);
+
+  // 原始类型自动转换
+  if (pattern === String) value = String(value);
+  else if (pattern === Number) {
+    value = Number(value);
+    if (isNaN(value) && strict) throw new Error(`Invalid number: ${raw}`);
+  } else if (pattern === Boolean) {
+    if (value === 'true' || value === '1' || value === true) value = true;
+    else if (value === 'false' || value === '0' || value === false) value = false;
+    else if (strict) throw new Error(`Invalid boolean: ${raw}`);
+    else value = Boolean(value);
+  } else if (pattern instanceof RegExp && !pattern.test(String(value)) && strict) throw new Error(`Invalid value: ${raw}`);
+
+  return value;
+}
+
+/**
+ * 参数解析器
+ * @description
+ *  Raw Input
+ *    ↓ normalizeInput
+ *    ↓ URLSearchParams
+ *    ↓ parseValue (JSON + JS 类型 + pattern)
+ *    ↓ 类型安全输出
+ * @example
+ * ```
+ * // 标准 URL
+ * preset=1&el=debug&el=btn
+ * // DSL 语法
+ * preset:1;el:debug
+ * // Pipe 语法
+ * el:a|b|c
+ * ```
+ *
+ * @param schema
+ * @param value
+ * @param options
+ */
+export function analysis<T extends readonly FieldSchema[]>(schema: T, value?: string, options?: AnalysisOptions): InferResult<T> {
+  const input = normalizeInput(value);
+  const params = new URLSearchParams(input);
+  const result: Record<string, unknown> = {};
+
+  for (const { key, default: defaultValue, multiple, required, pattern, parse } of schema) {
+    const matches = params.getAll(key);
+    if (matches.length === 0) {
+      if (defaultValue != null) result[key] = defaultValue;
+      else if (required && options?.strict) throw new Error(`Missing required field: ${key}`);
+      continue;
+    }
+    const values = matches.map(raw => parseValue(raw, pattern, parse, options?.strict));
+    result[key] = multiple ? values : values[0];
+  }
+
+  return result as InferResult<T>;
+}

+ 0 - 31
src/tools/regex.ts

@@ -1,31 +0,0 @@
-/**
- * 模式配置类型定义
- */
-type PatternConfig = readonly [key: string, pattern: string, defaultValue?: string];
-
-/**
- * 根据模式配置推断返回类型
- */
-type InferResultType<T extends readonly PatternConfig[]> = {
-  [K in T[number] as K extends readonly [infer Key, ...any[]] ? Key : never]: K extends readonly [any, any, infer Default]
-    ? Default extends string
-      ? string
-      : string | undefined
-    : string | undefined;
-};
-
-/**
- * 分析函数 - 根据 patterns 参数类型确定返回类型
- * @param patterns 模式配置数组
- * @param value 要解析的字符串
- * @returns 解析后的对象,类型根据patterns自动推断
- */
-export function analysis<T extends readonly PatternConfig[]>(patterns: T, value?: string): InferResultType<T> {
-  const regex = patterns.map(([key, pattern]) => `(?:${key}:(?<${key}>${pattern})[;$]?)?`).join('');
-  const groups = (value || '').match(new RegExp(`^${regex}`))?.groups ?? {};
-
-  return patterns.reduce((values, [key, _, defaultValue]) => {
-    values[key as keyof InferResultType<T>] = (groups[key] ?? defaultValue) as any;
-    return values;
-  }, {} as InferResultType<T>);
-}