texts.
+ """
+ notes: list[str] = []
+ for heading in content.find_all(["h2", "h3", "h4"]):
+ ht = _clean_text(heading.get_text(" ", strip=True))
+ if not ht:
+ continue
+ if ("平台" not in ht) and ("兼容" not in ht):
+ continue
+
+ cur = heading
+ for _ in range(15):
+ cur = cur.find_next_sibling()
+ if cur is None:
+ break
+ if isinstance(cur, Tag) and cur.name in ("h2", "h3", "h4"):
+ break
+ if isinstance(cur, Tag) and cur.name in ("p", "li"):
+ txt = _clean_text(cur.get_text(" ", strip=True))
+ if txt and not _looks_like_single_token_label(txt):
+ notes.append(txt)
+ if len(notes) >= 8:
+ break
+
+ # de-dup while preserving order
+ seen = set()
+ uniq = []
+ for n in notes:
+ if n in seen:
+ continue
+ seen.add(n)
+ uniq.append(n)
+ return uniq[:8]
+
+
+def fetch_component_page(name: str, timeout_s: int = 25) -> ComponentPage:
+ """
+ Fetch and parse a uni-app built-in component doc page.
+ """
+ url = (
+ "https://doc.dcloud.net.cn/uniCloud/unicloud-db"
+ if name == "unicloud-db"
+ else f"https://uniapp.dcloud.net.cn/component/{name}.html"
+ )
+
+ resp = requests.get(
+ url,
+ headers={
+ "User-Agent": "uniapp-develop-skills-docgen/1.0 (+local skill generator)",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ },
+ timeout=timeout_s,
+ )
+ resp.raise_for_status()
+
+ soup = BeautifulSoup(resp.text, "html.parser")
+ # Uni-app docs are VuePress-like. Extract only from the main content container to
+ # avoid picking up nav/menu text like "内置组件 / 扩展组件(uni-ui)".
+ container = soup.select_one(".theme-default-content, .content__default")
+ content = container if container is not None else soup
+
+ # Title: prefer stable local naming for built-in components.
+ # Some uni-app pages render dynamic titles ("uni-app官网") in static HTML.
+ h1 = content.find("h1") if isinstance(content, Tag) else soup.find("h1")
+ h1_text = _clean_text(h1.get_text(" ", strip=True)) if h1 else ""
+ if h1_text and not h1_text.lower().startswith("uni-app"):
+ title = h1_text
+ else:
+ title = name
+
+ # Intro: take first 2-4 paragraphs after h1 if possible, otherwise first 3 .
+ intro: list[str] = []
+ if h1:
+ cur = h1
+ for _ in range(50):
+ cur = cur.find_next()
+ if cur is None:
+ break
+ if isinstance(cur, Tag) and cur.name in ("h2", "h3"):
+ break
+ if isinstance(cur, Tag) and cur.name == "p":
+ txt = _clean_text(cur.get_text(" ", strip=True))
+ if txt:
+ intro.append(txt)
+ if len(intro) >= 4:
+ break
+ if not intro:
+ for p in content.find_all("p")[:10]:
+ txt = _clean_text(p.get_text(" ", strip=True))
+ if txt and not _looks_like_single_token_label(txt):
+ intro.append(txt)
+ if len(intro) >= 3:
+ break
+
+ # Tables (prefer section-based, fallback to header-based).
+ props_table = _find_section_table(content, keywords=("属性", "properties", "Props", "prop"))
+ if props_table is None:
+ props_table = _find_table_by_header_keywords(content, header_keywords=("属性名", "属性", "默认值", "类型"))
+
+ events_table = _find_section_table(content, keywords=("事件", "events", "Event"))
+ if events_table is None:
+ events_table = _find_table_by_header_keywords(content, header_keywords=("事件名", "事件名称", "回调", "参数"))
+
+ slots_table = _find_section_table(content, keywords=("插槽", "slot", "slots"))
+
+ platform_table = _find_platform_table(content)
+
+ # Prefer explicit events table; if missing, try to split mixed props/events table.
+ props_md = None
+ events_md = None
+ if props_table:
+ grid = _table_to_grid(props_table)
+ if grid:
+ props_grid, embedded_events_grid = _split_props_and_events_grid(grid)
+ props_md = _grid_to_markdown(props_grid) if props_grid else None
+ if embedded_events_grid and events_table is None:
+ events_md = _grid_to_markdown(embedded_events_grid)
+ else:
+ props_md = _table_to_markdown(props_table)
+
+ if events_table:
+ events_md = _table_to_markdown(events_table) or events_md
+
+ slots_md = _table_to_markdown(slots_table) if slots_table else None
+ platform_md = _table_to_markdown(platform_table) if platform_table else None
+ platform_notes = _extract_platform_notes(content) if not platform_md else []
+
+ # Examples: collect up to 6 non-trivial code blocks.
+ examples: list[tuple[str, str]] = []
+ for code in content.select("pre code"):
+ # Important: avoid adding separators between syntax highlight token spans.
+ # Using separator "" keeps original newlines but doesn't inject newlines
+ # between adjacent text nodes.
+ txt = code.get_text("", strip=False).strip()
+ if len(txt) < 40:
+ continue
+ # Remove trailing "复制代码" if present in text nodes
+ txt = re.sub(r"\n?复制代码\s*$", "", txt).rstrip()
+ lang = _guess_code_lang(code)
+ examples.append((lang, txt))
+ if len(examples) >= 6:
+ break
+
+ return ComponentPage(
+ name=name,
+ url=url,
+ title=title,
+ intro_paragraphs=intro,
+ properties_table_md=props_md,
+ events_table_md=events_md,
+ slots_table_md=slots_md,
+ platform_table_md=platform_md,
+ platform_notes=platform_notes,
+ example_blocks=examples,
+ )
+
+
+def render_block_style_md(page: ComponentPage) -> str:
+ """
+ Render markdown roughly aligned with `mermaid/examples/block.md` structure:
+ - Instructions
+ - Syntax (with properties/events/platform tables)
+ - Examples (multiple)
+ - Reference
+ """
+ title = page.title or page.name
+
+ lines: list[str] = []
+ lines.append(f"# {title}")
+ lines.append("")
+ lines.append("## Instructions")
+ lines.append("")
+ if page.intro_paragraphs:
+ for p in page.intro_paragraphs[:4]:
+ lines.append(p)
+ lines.append("")
+ else:
+ lines.append(f"`{page.name}` 是 uni-app 内置组件。")
+ lines.append("")
+
+ lines.append("### Syntax")
+ lines.append("")
+ lines.append(f"- 使用 `<{page.name} />`(或 `<{page.name}>{page.name}>`,当需要包裹子节点时)。")
+ lines.append("- 遇到平台差异时,建议使用条件编译(`#ifdef / #endif`)显式处理。")
+ lines.append("")
+
+ if page.properties_table_md:
+ lines.append("#### Properties")
+ lines.append("")
+ lines.append(page.properties_table_md)
+ lines.append("")
+ else:
+ lines.append("#### Properties")
+ lines.append("")
+ lines.append(f"See official docs for full properties list: `{page.url}`")
+ lines.append("")
+
+ if page.events_table_md:
+ lines.append("#### Events")
+ lines.append("")
+ lines.append(page.events_table_md)
+ lines.append("")
+ else:
+ lines.append("#### Events")
+ lines.append("")
+ lines.append(f"See official docs for full events list: `{page.url}`")
+ lines.append("")
+
+ if page.slots_table_md:
+ lines.append("#### Slots")
+ lines.append("")
+ lines.append(page.slots_table_md)
+ lines.append("")
+
+ if page.platform_table_md:
+ lines.append("#### Platform Compatibility")
+ lines.append("")
+ lines.append(page.platform_table_md)
+ lines.append("")
+ else:
+ lines.append("#### Platform Compatibility")
+ lines.append("")
+ if page.platform_notes:
+ for n in page.platform_notes:
+ lines.append(f"- {n}")
+ else:
+ lines.append(f"See official docs for platform support table: `{page.url}`")
+ lines.append("")
+
+ lines.append("### Examples")
+ lines.append("")
+ if not page.example_blocks:
+ lines.append(f"Examples are available in the official docs: `{page.url}`")
+ lines.append("")
+ else:
+ for idx, (lang, code) in enumerate(page.example_blocks, start=1):
+ lines.append(f"### Example (Example {idx})")
+ lines.append("")
+ lines.append(f"```{lang}")
+ lines.append(code.rstrip())
+ lines.append("```")
+ lines.append("")
+
+ lines.append(f"Reference: [Official Documentation]({page.url})")
+ lines.append("")
+
+ return "\n".join(lines)
+
+
+def _parse_only_arg(s: str) -> list[str]:
+ """Parse --only 'a,b,c' argument."""
+ items = []
+ for part in (s or "").split(","):
+ part = part.strip()
+ if part:
+ items.append(part)
+ return items
+
+
+def _parse_skip_arg(s: str) -> set[str]:
+ """Parse --skip 'a,b,c' argument."""
+ return set(_parse_only_arg(s))
+
+
+def main() -> int:
+ """CLI entrypoint."""
+ parser = argparse.ArgumentParser(description="Generate built-in component docs in block.md style.")
+ parser.add_argument("--only", type=str, default="", help="Comma-separated component names to generate.")
+ parser.add_argument("--skip", type=str, default="", help="Comma-separated component names to skip.")
+ parser.add_argument("--dry-run", action="store_true", help="Do not write files; just print what would happen.")
+ parser.add_argument("--sleep", type=float, default=0.2, help="Sleep between requests (seconds).")
+ args = parser.parse_args()
+
+ repo_root = Path(__file__).resolve().parents[1]
+ out_dir = repo_root / "references" / "components" / "built-in"
+ if not out_dir.exists():
+ raise SystemExit(f"Output dir not found: {out_dir}")
+
+ only = set(_parse_only_arg(args.only))
+ skip = _parse_skip_arg(args.skip)
+ targets = sorted(p.stem for p in out_dir.glob("*.md"))
+ if only:
+ targets = [t for t in targets if t in only]
+ if skip:
+ targets = [t for t in targets if t not in skip]
+
+ if not targets:
+ print("No targets found.")
+ return 0
+
+ for name in targets:
+ print(f"[fetch] {name}")
+ page = fetch_component_page(name)
+ md = render_block_style_md(page)
+ out_file = out_dir / f"{name}.md"
+ if args.dry_run:
+ print(f"[dry-run] would write {out_file} ({len(md)} chars)")
+ else:
+ out_file.write_text(md, encoding="utf-8")
+ print(f"[write] {out_file}")
+ time.sleep(max(args.sleep, 0.0))
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
+
diff --git a/.agents/skills/uniapp-project/scripts/generate-uniui-block-docs.py b/.agents/skills/uniapp-project/scripts/generate-uniui-block-docs.py
new file mode 100644
index 0000000..de778fd
--- /dev/null
+++ b/.agents/skills/uniapp-project/scripts/generate-uniui-block-docs.py
@@ -0,0 +1,375 @@
+#!/usr/bin/env python3
+"""
+Generate block.md-style documentation for uni-ui components.
+
+Outputs:
+ references/components/uni-ui/{component}.md
+
+Why:
+- Keep key info local to reduce token usage.
+- Enforce a consistent structure similar to `mermaid/examples/block.md`:
+ Instructions → Syntax → Examples → Reference
+
+Usage:
+ python3 scripts/generate-uniui-block-docs.py
+ python3 scripts/generate-uniui-block-docs.py --only uni-badge,uni-icons
+ python3 scripts/generate-uniui-block-docs.py --skip uni-badge
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import time
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Iterable, Optional, Sequence
+
+import requests
+from bs4 import BeautifulSoup, Tag
+
+
+@dataclass(frozen=True)
+class ComponentPage:
+ """uni-ui component page parsed result."""
+
+ name: str
+ url: str
+ title: str
+ intro_paragraphs: list[str]
+ properties_table_md: Optional[str]
+ events_table_md: Optional[str]
+ platform_table_md: Optional[str]
+ platform_notes: list[str]
+ example_blocks: list[tuple[str, str]] # (lang, code)
+
+
+def _clean_text(s: str) -> str:
+ """Normalize whitespace for human-readable text blocks."""
+ return re.sub(r"\s+", " ", s or "").strip()
+
+
+def _looks_like_single_token_label(s: str) -> bool:
+ """Filter nav/platform labels accidentally captured as paragraphs."""
+ if not s or len(s) > 16:
+ return False
+ return re.fullmatch(r"[A-Za-z0-9.+-]+", s) is not None
+
+
+def _guess_code_lang(code_tag: Tag) -> str:
+ """Guess fenced code language from CSS classes."""
+ classes = " ".join(code_tag.get("class", [])).lower()
+ if "language-vue" in classes:
+ return "vue"
+ if "language-html" in classes:
+ return "html"
+ if "language-javascript" in classes or "language-js" in classes:
+ return "javascript"
+ if "language-typescript" in classes or "language-ts" in classes:
+ return "typescript"
+ return "vue"
+
+
+def _table_to_grid(table: Tag) -> Optional[list[list[str]]]:
+ """HTML table -> normalized 2D grid."""
+ rows = table.find_all("tr")
+ grid: list[list[str]] = []
+ for tr in rows:
+ cells = tr.find_all(["th", "td"])
+ if not cells:
+ continue
+ grid.append([_clean_text(c.get_text(" ", strip=True)) for c in cells])
+ if len(grid) < 2:
+ return None
+ max_cols = max(len(r) for r in grid)
+ if max_cols < 2:
+ return None
+ return [r + [""] * (max_cols - len(r)) for r in grid]
+
+
+def _grid_to_markdown(grid: Sequence[Sequence[str]]) -> Optional[str]:
+ """2D grid -> markdown table."""
+ if not grid or len(grid) < 2:
+ return None
+ max_cols = max(len(r) for r in grid)
+ if max_cols < 2:
+ return None
+ norm = [list(r) + [""] * (max_cols - len(r)) for r in grid]
+ header = norm[0]
+ aligns = ["---"] * max_cols
+ out = []
+ out.append("| " + " | ".join(header) + " |")
+ out.append("| " + " | ".join(aligns) + " |")
+ for r in norm[1:]:
+ out.append("| " + " | ".join(r) + " |")
+ return "\n".join(out)
+
+
+def _find_section_table(content: Tag, keywords: Iterable[str]) -> Optional[Tag]:
+ """Find a table under a heading containing keywords."""
+ for heading in content.find_all(["h2", "h3", "h4"]):
+ ht = _clean_text(heading.get_text(" ", strip=True))
+ if not ht:
+ continue
+ if not any(k in ht for k in keywords):
+ continue
+ cur = heading
+ for _ in range(20):
+ cur = cur.find_next_sibling()
+ if cur is None:
+ break
+ if isinstance(cur, Tag) and cur.name == "table":
+ return cur
+ if isinstance(cur, Tag):
+ t = cur.find("table")
+ if t is not None:
+ return t
+ return None
+
+
+def _table_header_cells(table: Tag) -> list[str]:
+ tr = table.find("tr")
+ if tr is None:
+ return []
+ return [_clean_text(c.get_text(" ", strip=True)) for c in tr.find_all(["th", "td"])]
+
+
+def _is_platform_support_table(headers: list[str]) -> bool:
+ joined = " ".join(headers)
+ if "属性名" in joined or "默认值" in joined or "类型" in joined:
+ return False
+ if "平台" in joined and ("支持" in joined or "版本" in joined or "说明" in joined):
+ return True
+ return False
+
+
+def _find_platform_table(content: Tag) -> Optional[Tag]:
+ t = _find_section_table(content, keywords=("平台", "兼容", "兼容性", "Platform"))
+ if t is not None and _is_platform_support_table(_table_header_cells(t)):
+ return t
+ for table in content.find_all("table"):
+ if _is_platform_support_table(_table_header_cells(table)):
+ return table
+ return None
+
+
+def _extract_platform_notes(content: Tag) -> list[str]:
+ notes: list[str] = []
+ for heading in content.find_all(["h2", "h3", "h4"]):
+ ht = _clean_text(heading.get_text(" ", strip=True))
+ if ("平台" not in ht) and ("兼容" not in ht):
+ continue
+ cur = heading
+ for _ in range(15):
+ cur = cur.find_next_sibling()
+ if cur is None:
+ break
+ if isinstance(cur, Tag) and cur.name in ("h2", "h3", "h4"):
+ break
+ if isinstance(cur, Tag) and cur.name in ("p", "li"):
+ txt = _clean_text(cur.get_text(" ", strip=True))
+ if txt and not _looks_like_single_token_label(txt):
+ notes.append(txt)
+ if len(notes) >= 8:
+ break
+ seen = set()
+ uniq = []
+ for n in notes:
+ if n in seen:
+ continue
+ seen.add(n)
+ uniq.append(n)
+ return uniq[:8]
+
+
+def fetch_uniui_page(name: str, timeout_s: int = 25) -> ComponentPage:
+ """Fetch and parse uni-ui component page."""
+ url = f"https://uniapp.dcloud.net.cn/component/uniui/{name}.html"
+ resp = requests.get(
+ url,
+ headers={
+ "User-Agent": "uniapp-develop-skills-docgen/1.0 (+local skill generator)",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ },
+ timeout=timeout_s,
+ )
+ resp.raise_for_status()
+
+ soup = BeautifulSoup(resp.text, "html.parser")
+ container = soup.select_one(".theme-default-content, .content__default")
+ content = container if container is not None else soup
+
+ # Title: prefer component name for stability.
+ title = name
+
+ # Intro: first 3 paragraphs from content
+ intro: list[str] = []
+ for p in content.find_all("p")[:12]:
+ txt = _clean_text(p.get_text(" ", strip=True))
+ if txt and not _looks_like_single_token_label(txt):
+ intro.append(txt)
+ if len(intro) >= 3:
+ break
+
+ props_table = _find_section_table(content, keywords=("属性", "Properties", "props", "Props"))
+ props_md = _grid_to_markdown(_table_to_grid(props_table)) if props_table else None
+
+ events_table = _find_section_table(content, keywords=("事件", "Events", "Event"))
+ events_md = _grid_to_markdown(_table_to_grid(events_table)) if events_table else None
+
+ platform_table = _find_platform_table(content)
+ platform_md = _grid_to_markdown(_table_to_grid(platform_table)) if platform_table else None
+ platform_notes = _extract_platform_notes(content) if not platform_md else []
+
+ # Examples: collect up to 6 code blocks
+ examples: list[tuple[str, str]] = []
+ for code in content.select("pre code"):
+ txt = code.get_text("", strip=False).strip()
+ if len(txt) < 40:
+ continue
+ txt = re.sub(r"\n?复制代码\s*$", "", txt).rstrip()
+ examples.append((_guess_code_lang(code), txt))
+ if len(examples) >= 6:
+ break
+
+ return ComponentPage(
+ name=name,
+ url=url,
+ title=title,
+ intro_paragraphs=intro,
+ properties_table_md=props_md,
+ events_table_md=events_md,
+ platform_table_md=platform_md,
+ platform_notes=platform_notes,
+ example_blocks=examples,
+ )
+
+
+def render_block_style_md(page: ComponentPage, repo_root: Path) -> str:
+ """Render markdown in block.md-like structure."""
+ name = page.name
+ url = page.url
+
+ # Links
+ plugin_name = name.replace("uni-", "")
+ plugin_url = f"https://ext.dcloud.net.cn/plugin?name={plugin_name}"
+ local_example = repo_root / "examples" / "uni-ui" / f"{name}.vue"
+
+ lines: list[str] = []
+ lines.append(f"# {name}")
+ lines.append("")
+ lines.append("## Instructions")
+ lines.append("")
+ if page.intro_paragraphs:
+ for p in page.intro_paragraphs:
+ lines.append(p)
+ lines.append("")
+ else:
+ lines.append(f"`{name}` 是 uni-ui 扩展组件。")
+ lines.append("")
+
+ lines.append("### Syntax")
+ lines.append("")
+ lines.append(f"- 使用 `<{name} />`(或 `<{name}>{name}>`,当需要包裹子节点时)。")
+ lines.append("- 遇到平台差异时,建议使用条件编译(`#ifdef / #endif`)显式处理。")
+ lines.append("")
+
+ lines.append("#### Properties")
+ lines.append("")
+ if page.properties_table_md:
+ lines.append(page.properties_table_md)
+ else:
+ lines.append(f"See official docs for full properties list: `{url}`")
+ lines.append("")
+
+ lines.append("#### Events")
+ lines.append("")
+ if page.events_table_md:
+ lines.append(page.events_table_md)
+ else:
+ lines.append(f"See official docs for full events list: `{url}`")
+ lines.append("")
+
+ lines.append("#### Platform Compatibility")
+ lines.append("")
+ if page.platform_table_md:
+ lines.append(page.platform_table_md)
+ elif page.platform_notes:
+ for n in page.platform_notes:
+ lines.append(f"- {n}")
+ else:
+ lines.append(f"See official docs for platform support table: `{url}`")
+ lines.append("")
+
+ lines.append("### Examples")
+ lines.append("")
+ if page.example_blocks:
+ for idx, (lang, code) in enumerate(page.example_blocks, start=1):
+ lines.append(f"### Example (Example {idx})")
+ lines.append("")
+ lines.append(f"```{lang}")
+ lines.append(code.rstrip())
+ lines.append("```")
+ lines.append("")
+ else:
+ lines.append(f"Examples are available in the official docs: `{url}`")
+ lines.append("")
+
+ lines.append("Reference:")
+ lines.append(f"- [Official Documentation]({url})")
+ lines.append(f"- [Plugin Marketplace]({plugin_url})")
+ if local_example.exists():
+ rel = local_example.relative_to(repo_root)
+ lines.append(f"- [Local Example]({rel.as_posix()})")
+ lines.append("")
+
+ return "\n".join(lines)
+
+
+def _parse_list_arg(s: str) -> list[str]:
+ return [p.strip() for p in (s or "").split(",") if p.strip()]
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Generate uni-ui component docs in block.md style.")
+ parser.add_argument("--only", type=str, default="", help="Comma-separated component names to generate.")
+ parser.add_argument("--skip", type=str, default="", help="Comma-separated component names to skip.")
+ parser.add_argument("--dry-run", action="store_true", help="Do not write files.")
+ parser.add_argument("--sleep", type=float, default=0.2, help="Sleep between requests (seconds).")
+ args = parser.parse_args()
+
+ repo_root = Path(__file__).resolve().parents[1]
+ out_dir = repo_root / "references" / "components" / "uni-ui"
+ if not out_dir.exists():
+ raise SystemExit(f"Output dir not found: {out_dir}")
+
+ targets = sorted(p.stem for p in out_dir.glob("*.md"))
+ only = set(_parse_list_arg(args.only))
+ skip = set(_parse_list_arg(args.skip))
+ if only:
+ targets = [t for t in targets if t in only]
+ if skip:
+ targets = [t for t in targets if t not in skip]
+
+ if not targets:
+ print("No targets found.")
+ return 0
+
+ for name in targets:
+ print(f"[fetch] {name}")
+ page = fetch_uniui_page(name)
+ md = render_block_style_md(page, repo_root=repo_root)
+ out_file = out_dir / f"{name}.md"
+ if args.dry_run:
+ print(f"[dry-run] would write {out_file} ({len(md)} chars)")
+ else:
+ out_file.write_text(md, encoding="utf-8")
+ print(f"[write] {out_file}")
+ time.sleep(max(args.sleep, 0.0))
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
+
diff --git a/.agents/skills/uview-pro-vue3/LICENSE.txt b/.agents/skills/uview-pro-vue3/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/.agents/skills/uview-pro-vue3/SKILL.md b/.agents/skills/uview-pro-vue3/SKILL.md
new file mode 100644
index 0000000..695dd49
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/SKILL.md
@@ -0,0 +1,273 @@
+---
+name: uview-pro-vue3
+description: Provides comprehensive guidance for uView Pro Vue 3 component library including components, tools, layouts, and templates. Use when the user asks about uView Pro, needs to build Vue 3 applications with uView Pro, or implement mobile-first UI components.
+license: Complete terms in LICENSE.txt
+---
+
+## When to use this skill
+
+Use this skill whenever the user wants to:
+- Install and set up uView Pro in a uni-app project
+- Use uView Pro components in Vue 3 / uni-app applications
+- Configure uView Pro (theme, i18n, etc.)
+- Use form components (Button, Input, Form, etc.)
+- Use data display components (List, Card, etc.)
+- Use feedback components (Toast, Modal, etc.)
+- Use navigation components (Tabs, NavBar, etc.)
+- Use uView Pro tools and utilities
+- Use uView Pro layout templates
+- Customize component styles and themes
+- Handle component events
+- Understand uView Pro API and methods
+- Troubleshoot uView Pro issues
+
+## How to use this skill
+
+This skill is organized to match the uView Pro official documentation structure (https://uviewpro.cn/, https://uviewpro.cn/zh/guide/intro.html, https://uviewpro.cn/zh/components/intro.html, https://uviewpro.cn/zh/tools/intro.html, https://uviewpro.cn/zh/layout/intro.html). When working with uView Pro:
+
+1. **Identify the topic** from the user's request:
+ - Installation/安装 → `examples/guide/installation.md`
+ - Quick Start/快速开始 → `examples/guide/quick-start.md`
+ - Components/组件 → `examples/components/`
+ - Tools/工具 → `examples/tools/`
+ - Layout/布局 → `examples/layout/`
+ - API/API 文档 → `api/`
+
+2. **Load the appropriate example file** from the `examples/` directory:
+
+ **Guide (使用指南)**:
+ - `examples/guide/intro.md` - Introduction
+ - `examples/guide/installation.md` - Installation guide
+ - `examples/guide/quick-start.md` - Quick start guide
+ - `examples/guide/theme.md` - Theme customization
+ - `examples/guide/i18n.md` - Internationalization
+ - `examples/guide/config.md` - Configuration
+
+ **Components (组件)**:
+ - `examples/components/intro.md` - Components introduction
+ - `examples/components/button.md` - Button component
+ - `examples/components/input.md` - Input component
+ - `examples/components/form.md` - Form component
+ - `examples/components/list.md` - List component
+ - `examples/components/card.md` - Card component
+ - `examples/components/toast.md` - Toast component
+ - `examples/components/modal.md` - Modal component
+ - `examples/components/tabs.md` - Tabs component
+ - `examples/components/navbar.md` - NavBar component
+ - `examples/components/date-picker.md` - DatePicker component
+ - `examples/components/select.md` - Select component
+ - `examples/components/switch.md` - Switch component
+ - `examples/components/checkbox.md` - Checkbox component
+ - `examples/components/radio.md` - Radio component
+ - `examples/components/upload.md` - Upload component
+ - `examples/components/pagination.md` - Pagination component
+ - `examples/components/avatar.md` - Avatar component
+ - `examples/components/badge.md` - Badge component
+ - `examples/components/tag.md` - Tag component
+ - `examples/components/empty.md` - Empty component
+ - `examples/components/loading.md` - Loading component
+ - `examples/components/popup.md` - Popup component
+ - `examples/components/dropdown.md` - Dropdown component
+ - `examples/components/drawer.md` - Drawer component
+
+ **Tools (工具)**:
+ - `examples/tools/intro.md` - Tools introduction
+ - `examples/tools/http.md` - HTTP request
+ - `examples/tools/storage.md` - Storage utilities
+ - `examples/tools/router.md` - Router utilities
+ - `examples/tools/validator.md` - Validator utilities
+ - `examples/tools/format.md` - Format utilities
+ - `examples/tools/color.md` - Color utilities
+
+ **Layout (布局)**:
+ - `examples/layout/intro.md` - Layout introduction
+ - `examples/layout/grid.md` - Grid layout
+ - `examples/layout/flex.md` - Flex layout
+ - `examples/layout/container.md` - Container layout
+
+3. **Follow the specific instructions** in that example file for syntax, structure, and best practices
+
+ **Important Notes**:
+ - uView Pro is for Vue 3 and uni-app
+ - Components use Vue 3 Composition API
+ - Examples include both Options API and Composition API
+ - Each example file includes key concepts, code examples, and key points
+
+4. **Reference API documentation** in the `api/` directory when needed:
+ - `api/component-api.md` - Component API reference
+ - `api/props-and-events.md` - Props and events reference
+ - `api/tools-api.md` - Tools API reference
+ - `api/config-api.md` - Configuration API
+
+5. **Use templates** from the `templates/` directory:
+ - `templates/installation.md` - Installation templates
+ - `templates/component-usage.md` - Component usage templates
+ - `templates/project-setup.md` - Project setup templates
+
+### 1. Understanding uView Pro
+
+uView Pro is a Vue 3 component library designed for uni-app development, providing rich components and utility methods.
+
+**Key Concepts**:
+- **Vue 3 Support**: Built for Vue 3 with Composition API
+- **uni-app Support**: Optimized for uni-app development
+- **Rich Components**: 100+ components for various use cases
+- **Theme Customization**: Support for theme customization
+- **i18n**: Internationalization support
+- **Tools**: Rich utility methods
+
+### 2. Installation
+
+**Using npm**:
+
+```bash
+npm install uview-pro
+```
+
+**Using yarn**:
+
+```bash
+yarn add uview-pro
+```
+
+**Using pnpm**:
+
+```bash
+pnpm add uview-pro
+```
+
+### 3. Basic Setup
+
+```javascript
+// main.js
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView)
+ return {
+ app
+ }
+}
+```
+
+
+### Doc mapping (one-to-one with official documentation)
+
+**Guide (指南)**:
+- See guide files in `examples/guide/` or `examples/getting-started/` → https://uviewpro.cn/zh/guide/intro.html
+
+**Components (组件)**:
+- See component files in `examples/components/` → https://uviewpro.cn/zh/components/intro.html
+
+## Examples and Templates
+
+This skill includes detailed examples organized to match the official documentation structure. All examples are in the `examples/` directory (see mapping above).
+
+**To use examples:**
+- Identify the topic from the user's request
+- Load the appropriate example file from the mapping above
+- Follow the instructions, syntax, and best practices in that file
+- Adapt the code examples to your specific use case
+
+**To use templates:**
+- Reference templates in `templates/` directory for common scaffolding
+- Adapt templates to your specific needs and coding style
+
+## API Reference
+
+Detailed API documentation is available in the `api/` directory, organized to match the official uView Pro API documentation structure:
+
+### Component API (`api/component-api.md`)
+- Component props and events
+- Component methods
+- Component slots
+
+### Props and Events (`api/props-and-events.md`)
+- Common props
+- Common events
+- Event handling
+
+### Tools API (`api/tools-api.md`)
+- HTTP request methods
+- Storage methods
+- Router methods
+- Validator methods
+- Format methods
+- Color methods
+
+### Configuration API (`api/config-api.md`)
+- Global configuration options
+- Theme configuration
+- i18n configuration
+
+**To use API reference:**
+1. Identify the API you need help with
+2. Load the corresponding API file from the `api/` directory
+3. Find the API signature, parameters, return type, and examples
+4. Reference the linked example files for detailed usage patterns
+5. All API files include links to relevant example files in the `examples/` directory
+
+## Best Practices
+
+1. **Use on-demand import**: Import only the components you need to reduce bundle size
+2. **Use Composition API**: Prefer Composition API for better code organization
+3. **Handle events properly**: Use proper event handlers for component interactions
+4. **Customize theme**: Use theme variables for customization
+5. **Follow design specs**: Follow uView Pro design specifications
+6. **Use tools**: Leverage uView Pro tools for common operations
+7. **Use layouts**: Use layout templates for consistent page structure
+
+## Resources
+
+- **Official Documentation**: https://uviewpro.cn/
+- **Guide**: https://uviewpro.cn/zh/guide/intro.html
+- **Components**: https://uviewpro.cn/zh/components/intro.html
+- **Tools**: https://uviewpro.cn/zh/tools/intro.html
+- **Layout**: https://uviewpro.cn/zh/layout/intro.html
+
+## Keywords
+
+uView Pro, uview-pro, Vue 3, Vue3, uni-app, UI components, component library, 组件库, 按钮, 表单, 列表, 卡片, 提示, 弹窗, 标签页, 导航栏, 日期选择器, 选择器, 开关, 复选框, 单选框, 上传, 分页, 头像, 徽标, 标签, 空状态, 加载, 弹出层, 下拉菜单, 抽屉, HTTP, 存储, 路由, 验证, 格式化, 颜色, 网格布局, 弹性布局, 容器布局, Button, Form, List, Card, Toast, Modal, Tabs, NavBar, DatePicker, Select, Switch, Checkbox, Radio, Upload, Pagination, Avatar, Badge, Tag, Empty, Loading, Popup, Dropdown, Drawer
+
+## 能力边界
+
+### ✅ 适用场景
+- 当你需要使用此技能对应的技术栈时
+- 当项目需要遵循最佳实践时
+- 当需要快速上手或深入理解核心概念时
+
+### ⚠️ 需要注意
+- 复杂业务逻辑需要结合具体场景调整
+- 性能优化需要根据实际数据量评估
+
+### ❌ 不适用场景
+- 不相关的技术栈或框架
+- 需要完全自定义的特殊场景
+
+## 常见陷阱 (Gotchas)
+
+1. **版本兼容性**:注意框架版本与依赖库的兼容性,不同版本 API 可能有差异
+2. **配置文件格式**:配置文件格式错误是最常见的问题,建议使用编辑器的语法检查
+3. **环境变量**:确保所有必要的环境变量已正确设置,敏感信息不要硬编码
+4. **依赖冲突**:多版本共存时注意依赖冲突,使用 lock 文件锁定版本
+5. **性能陷阱**:大数据量场景下注意性能优化,避免 N+1 查询等常见问题
+
+## 使用流程
+
+### Step 1: 环境准备
+确保开发环境已安装必要的依赖和工具。
+
+### Step 2: 配置初始化
+根据项目需求进行基础配置。
+
+### Step 3: 核心功能使用
+按照示例代码实现核心功能。
+
+### Step 4: 测试验证
+运行测试确保功能正常。
+
+### Step 5: 部署上线
+完成开发后进行部署和监控。
diff --git a/.agents/skills/uview-pro-vue3/api/component-api.md b/.agents/skills/uview-pro-vue3/api/component-api.md
new file mode 100644
index 0000000..c109cfb
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/api/component-api.md
@@ -0,0 +1,71 @@
+# Component API
+
+## API Reference
+
+uView Pro component props, events, and methods.
+
+### Common Props
+
+Most components support:
+- `custom-class` - Custom class name
+- `custom-style` - Custom style object
+- `disabled` - Disabled state
+
+### Common Events
+
+Most components emit:
+- `@click` - Click event
+- `@change` - Change event
+- `@focus` - Focus event
+- `@blur` - Blur event
+
+### Button Component
+
+**Props:**
+- `type` - Button type (primary, success, info, warning, error)
+- `size` - Button size (large, normal, small, mini)
+- `disabled` - Disabled state
+- `loading` - Loading state
+- `plain` - Plain button
+- `shape` - Button shape (circle, round)
+- `icon` - Icon name
+
+**Events:**
+- `@click` - Click event
+
+### Input Component
+
+**Props:**
+- `v-model` - Input value
+- `type` - Input type (text, number, password, textarea)
+- `size` - Input size (large, normal, small)
+- `disabled` - Disabled state
+- `readonly` - Readonly state
+- `clearable` - Show clear button
+- `border` - Show border
+- `prefix-icon` - Prefix icon
+- `suffix-icon` - Suffix icon
+
+**Events:**
+- `@input` - Input event
+- `@change` - Change event
+- `@focus` - Focus event
+- `@blur` - Blur event
+
+### Form Component
+
+**Props:**
+- `model` - Form data object
+- `rules` - Validation rules
+- `label-width` - Label width
+
+**Methods:**
+- `validate` - Validate form
+- `validateField` - Validate specific field
+- `resetFields` - Reset form fields
+- `clearValidate` - Clear validation
+
+**Events:**
+- `@validate` - Validation event
+
+**See also:** `examples/components/` for detailed component examples
diff --git a/.agents/skills/uview-pro-vue3/api/config-api.md b/.agents/skills/uview-pro-vue3/api/config-api.md
new file mode 100644
index 0000000..9e127b8
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/api/config-api.md
@@ -0,0 +1,80 @@
+# Configuration API
+
+## API Reference
+
+uView Pro global configuration options.
+
+### Global Config Options
+
+When using `app.use(uView, options)`, you can pass:
+
+```typescript
+interface uViewOptions {
+ locale?: string
+ theme?: string
+}
+```
+
+### ConfigProvider Props
+
+**Props:**
+- `locale` - Global locale (zh-cn, en-us)
+- `theme` - Global theme (light, dark)
+
+### Example: Global Config
+
+```javascript
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView, {
+ locale: 'zh-cn',
+ theme: 'light'
+ })
+ return {
+ app
+ }
+}
+```
+
+### Example: ConfigProvider
+
+```vue
+
+
+ Button
+
+
+
+
+```
+
+### Locale Configuration
+
+**Options:**
+- `'zh-cn'` - Chinese (Simplified)
+- `'en-us'` - English (US)
+
+### Theme Configuration
+
+**Options:**
+- `'light'` - Light theme
+- `'dark'` - Dark theme
+
+### Component Config
+
+Components can be configured individually:
+
+```vue
+Button
+```
+
+**See also:** `examples/guide/config.md` for configuration examples
diff --git a/.agents/skills/uview-pro-vue3/api/props-and-events.md b/.agents/skills/uview-pro-vue3/api/props-and-events.md
new file mode 100644
index 0000000..635a271
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/api/props-and-events.md
@@ -0,0 +1,95 @@
+# Props and Events
+
+## API Reference
+
+Common props and events in uView Pro components.
+
+### Common Props
+
+#### custom-class
+
+**Type:** `string`
+
+Custom CSS class name.
+
+#### custom-style
+
+**Type:** `string | object`
+
+Custom inline style.
+
+#### disabled
+
+**Type:** `boolean`
+
+**Default:** `false`
+
+Whether component is disabled.
+
+### Common Events
+
+#### @click
+
+**Type:** `Function`
+
+Click event handler.
+
+**Example:**
+```vue
+Button
+```
+
+#### @change
+
+**Type:** `Function`
+
+Change event handler.
+
+**Example:**
+```vue
+
+```
+
+#### @focus
+
+**Type:** `Function`
+
+Focus event handler.
+
+**Example:**
+```vue
+
+```
+
+#### @blur
+
+**Type:** `Function`
+
+Blur event handler.
+
+**Example:**
+```vue
+
+```
+
+### Event Object
+
+Event handlers receive an event object:
+
+```javascript
+handleEvent(event) {
+ // event.target - Target element
+ // event.currentTarget - Current target element
+ // event.detail - Event detail (component-specific)
+}
+```
+
+### Component-Specific Events
+
+Different components emit different events:
+- **Input**: @input, @change, @focus, @blur
+- **Select**: @change, @open, @close
+- **Form**: @validate
+- **Button**: @click
+
+**See also:** `examples/components/` for component-specific examples
diff --git a/.agents/skills/uview-pro-vue3/api/tools-api.md b/.agents/skills/uview-pro-vue3/api/tools-api.md
new file mode 100644
index 0000000..3f01d3a
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/api/tools-api.md
@@ -0,0 +1,69 @@
+# Tools API
+
+## API Reference
+
+uView Pro tools and utility methods.
+
+### HTTP Request
+
+**request(options)**
+- `url` - Request URL
+- `method` - Request method (GET, POST, PUT, DELETE)
+- `data` - Request data
+- `header` - Request headers
+- `timeout` - Request timeout
+
+**Interceptors:**
+- `request.interceptors.request.use()` - Request interceptor
+- `request.interceptors.response.use()` - Response interceptor
+
+### Storage
+
+**setStorage(key, value)**
+- Set storage value
+
+**getStorage(key)**
+- Get storage value
+
+**removeStorage(key)**
+- Remove storage value
+
+**clearStorage()**
+- Clear all storage
+
+### Router
+
+**navigateTo(options)**
+- Navigate to page
+
+**redirectTo(options)**
+- Redirect to page
+
+**navigateBack(options)**
+- Navigate back
+
+**switchTab(options)**
+- Switch tab
+
+### Validator
+
+**validate(value, rules)**
+- Validate value with rules
+
+### Format
+
+**formatDate(date, format)**
+- Format date
+
+**formatNumber(number, format)**
+- Format number
+
+### Color
+
+**colorToRgb(color)**
+- Convert color to RGB
+
+**colorToHex(color)**
+- Convert color to HEX
+
+**See also:** `examples/tools/` for detailed tool examples
diff --git a/.agents/skills/uview-pro-vue3/examples/components/button.md b/.agents/skills/uview-pro-vue3/examples/components/button.md
new file mode 100644
index 0000000..4376352
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/components/button.md
@@ -0,0 +1,84 @@
+# Button Component
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates the Button component in uView Pro.
+
+### Key Concepts
+
+- Button types
+- Button sizes
+- Button states
+- Button events
+
+### Example: Basic Button
+
+```vue
+
+ Default
+ Primary
+ Success
+ Info
+ Warning
+ Error
+
+```
+
+### Example: Button Sizes
+
+```vue
+
+ Large
+ Normal
+ Small
+ Mini
+
+```
+
+### Example: Button States
+
+```vue
+
+ Disabled
+ Loading
+ Plain
+ Circle
+ Round
+
+```
+
+### Example: Button Events
+
+```vue
+
+
+ Click Me
+
+
+
+
+```
+
+### Example: Button with Icon
+
+```vue
+
+ Search
+ Like
+
+```
+
+### Key Points
+
+- Multiple types: primary, success, info, warning, error
+- Multiple sizes: large, normal, small, mini
+- Support disabled, loading, plain, shape
+- Icon support
+- Custom styling with custom-class or custom-style
diff --git a/.agents/skills/uview-pro-vue3/examples/components/form.md b/.agents/skills/uview-pro-vue3/examples/components/form.md
new file mode 100644
index 0000000..58c58d1
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/components/form.md
@@ -0,0 +1,106 @@
+# Form Component
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates the Form component in uView Pro.
+
+### Key Concepts
+
+- Form structure
+- Form validation
+- Form rules
+- Form submission
+- Form fields
+
+### Example: Basic Form
+
+```vue
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+```
+
+### Example: Form Validation
+
+```vue
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+```
+
+### Key Points
+
+- Use :model for form data
+- Configure :rules for validation
+- Use prop for form-item validation
+- Handle form submission
+- Support form validation methods
diff --git a/.agents/skills/uview-pro-vue3/examples/components/input.md b/.agents/skills/uview-pro-vue3/examples/components/input.md
new file mode 100644
index 0000000..d04748f
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/components/input.md
@@ -0,0 +1,110 @@
+# Input Component
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates the Input component in uView Pro.
+
+### Key Concepts
+
+- Input types
+- Input sizes
+- Input states
+- Input events
+- Input validation
+
+### Example: Basic Input
+
+```vue
+
+
+
+
+
+```
+
+### Example: Input Types
+
+```vue
+
+
+
+
+
+
+```
+
+### Example: Input Sizes
+
+```vue
+
+
+
+
+
+```
+
+### Example: Input States
+
+```vue
+
+
+
+
+
+
+```
+
+### Example: Input with Prefix/Suffix
+
+```vue
+
+
+
+
+```
+
+### Example: Input Events
+
+```vue
+
+
+
+
+
+```
+
+### Key Points
+
+- Use v-model for two-way binding
+- Support multiple input types
+- Support disabled, readonly, clearable
+- Prefix and suffix icon support
+- Multiple event handlers
diff --git a/.agents/skills/uview-pro-vue3/examples/components/intro.md b/.agents/skills/uview-pro-vue3/examples/components/intro.md
new file mode 100644
index 0000000..bcdda07
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/components/intro.md
@@ -0,0 +1,54 @@
+# Components Introduction
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example provides an introduction to uView Pro components.
+
+### Key Concepts
+
+- Component categories
+- Component list
+- Component usage
+- Component features
+
+### Example: Component Categories
+
+**Basic Components (基础组件)**:
+- Button, Icon, Image, Layout, Cell, Badge, Tag, Text, Fab, RootPortal, ConfigProvider
+
+**Form Components (表单组件)**:
+- Form, Input, Textarea, Calendar, Select, Keyboard, Picker, Rate, Search, NumberBox, Upload, VerificationCode, Field, Checkbox, Radio, Switch, Slider
+
+**Data Components (数据组件)**:
+- CircleProgress, LineProgress, Table, CountDown, CountTo
+
+**Feedback Components (反馈组件)**:
+- ActionSheet, AlertTips, Toast, NoticeBar, TopTips, Collapse, Popup, SwipeAction, Modal, FullScreen
+
+**Layout Components (布局组件)**:
+- Line, Card, Mask, NoNetwork, Grid, Swiper, TimeLine, Skeleton, Sticky, Waterfall, Divider
+
+**Navigation Components (导航组件)**:
+- Dropdown, Tabbar, BackTop, Navbar, Tabs, TabsSwiper, Subsection, IndexList, Steps, Empty, Link, Section, Pagination
+
+**Other Components (其他组件)**:
+- MessageInput, Loadmore, ReadMore, LazyLoad, Gap, Avatar, Loading, LoadingPopup, SafeAreaInset, Todo
+
+### Example: Component Usage
+
+```vue
+
+ Button
+
+```
+
+### Key Points
+
+- 100+ components available
+- Organized by category
+- Auto-import with easycom
+- Support Vue 3 Composition API
+- Consistent API design
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/config.md b/.agents/skills/uview-pro-vue3/examples/guide/config.md
new file mode 100644
index 0000000..ee60219
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/config.md
@@ -0,0 +1,68 @@
+# Configuration
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates how to configure uView Pro globally.
+
+### Key Concepts
+
+- Global config options
+- ConfigProvider
+- Theme configuration
+- Component configuration
+
+### Example: Global Config
+
+```javascript
+// main.js
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView, {
+ locale: 'zh-cn',
+ theme: 'light'
+ })
+ return {
+ app
+ }
+}
+```
+
+### Example: Using ConfigProvider
+
+```vue
+
+
+ Button
+
+
+
+
+```
+
+### Example: Component Config
+
+```vue
+
+ Button
+
+```
+
+### Key Points
+
+- Configure globally via app.use()
+- Use ConfigProvider for component-level config
+- Support theme and locale configuration
+- Component-level config available
+- Follow uni-app configuration patterns
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/i18n.md b/.agents/skills/uview-pro-vue3/examples/guide/i18n.md
new file mode 100644
index 0000000..d32bcf2
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/i18n.md
@@ -0,0 +1,65 @@
+# Internationalization (i18n)
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates how to configure internationalization in uView Pro.
+
+### Key Concepts
+
+- Locale configuration
+- Language switching
+- Component i18n
+
+### Example: Basic i18n Setup
+
+```javascript
+// main.js
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView, {
+ locale: 'zh-cn' // or 'en-us'
+ })
+ return {
+ app
+ }
+}
+```
+
+### Example: Language Switching
+
+```vue
+
+ Switch Language
+
+
+
+```
+
+### Example: Component i18n
+
+uView Pro components support i18n through global configuration.
+
+### Key Points
+
+- Configure locale in main.js
+- Support zh-cn and en-us
+- Switch language dynamically
+- Components automatically use configured locale
+- Customize locale messages if needed
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/installation.md b/.agents/skills/uview-pro-vue3/examples/guide/installation.md
new file mode 100644
index 0000000..181ce14
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/installation.md
@@ -0,0 +1,80 @@
+# Installation
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates how to install uView Pro in a uni-app project.
+
+### Key Concepts
+
+- Package installation
+- uni_modules installation
+- Easycom configuration
+- Style import
+
+### Example: Package Installation
+
+```bash
+# Using npm
+npm install uview-pro
+
+# Using yarn
+yarn add uview-pro
+
+# Using pnpm
+pnpm add uview-pro
+```
+
+### Example: uni_modules Installation
+
+1. Download from uView Pro official website
+2. Copy to `uni_modules` directory
+3. Configure easycom in `pages.json`
+
+### Example: Easycom Configuration
+
+```json
+// pages.json
+{
+ "easycom": {
+ "autoscan": true,
+ "custom": {
+ "^u-(.*)": "uview-pro/components/u-$1/u-$1.vue"
+ }
+ }
+}
+```
+
+### Example: Main.js Setup
+
+```javascript
+// main.js
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView)
+ return {
+ app
+ }
+}
+```
+
+### Example: Style Import
+
+```scss
+// App.vue or main.js
+@import 'uview-pro/index.scss';
+```
+
+### Key Points
+
+- Install via npm or uni_modules
+- Configure easycom for auto-import
+- Import styles in App.vue or main.js
+- Use createSSRApp for uni-app
+- Support Vue 3 Composition API
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/intro.md b/.agents/skills/uview-pro-vue3/examples/guide/intro.md
new file mode 100644
index 0000000..9ed6254
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/intro.md
@@ -0,0 +1,49 @@
+# Introduction
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example provides an introduction to uView Pro.
+
+### Key Concepts
+
+- What is uView Pro
+- Features
+- Platform support
+- Comparison with other libraries
+
+### Example: What is uView Pro
+
+uView Pro is a Vue 3 component library designed for uni-app development, based on uView 1.8.8 with complete refactoring, supporting Vue 3 and TypeScript.
+
+### Example: Features
+
+- **Vue 3 Support**: Built for Vue 3 with Composition API
+- **TypeScript**: Full TypeScript support
+- **Multi-platform**: Support Android, iOS, WeChat Mini Program, Alipay Mini Program, etc.
+- **Rich Components**: 100+ components
+- **Theme Customization**: Support for theme customization
+- **i18n**: Internationalization support
+- **Tools**: Rich utility methods
+
+### Example: Platform Support
+
+uView Pro supports:
+- Android
+- iOS
+- WeChat Mini Program
+- Alipay Mini Program
+- ByteDance Mini Program
+- QQ Mini Program
+- H5
+- And more
+
+### Key Points
+
+- Based on uView 1.8.8, completely refactored
+- Vue 3 and TypeScript support
+- Multi-platform compatibility
+- Rich component ecosystem
+- Active development and maintenance
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/quick-start.md b/.agents/skills/uview-pro-vue3/examples/guide/quick-start.md
new file mode 100644
index 0000000..f00289d
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/quick-start.md
@@ -0,0 +1,88 @@
+# Quick Start
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example provides a quick start guide for uView Pro.
+
+### Key Concepts
+
+- Basic setup
+- First component
+- Component usage
+- Project structure
+
+### Example: Basic Setup
+
+```vue
+
+
+ Button
+
+
+
+
+```
+
+### Example: With Options API
+
+```vue
+
+
+ Click Me
+
+
+
+
+```
+
+### Example: With Composition API
+
+```vue
+
+
+ Click Me
+
+
+
+
+```
+
+### Example: Project Structure
+
+```
+project/
+├── pages/
+│ └── index/
+│ ├── index.vue
+│ └── index.json
+├── uni_modules/
+│ └── uview-pro/
+├── App.vue
+├── main.js
+└── pages.json
+```
+
+### Key Points
+
+- Use u- prefix for components
+- Support both Options API and Composition API
+- Auto-import with easycom
+- Import styles globally
+- Follow uni-app project structure
diff --git a/.agents/skills/uview-pro-vue3/examples/guide/theme.md b/.agents/skills/uview-pro-vue3/examples/guide/theme.md
new file mode 100644
index 0000000..d7a14d7
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/guide/theme.md
@@ -0,0 +1,75 @@
+# Theme Customization
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates how to customize uView Pro theme.
+
+### Key Concepts
+
+- Custom class
+- Custom style
+- Global theme variables
+- Style override
+
+### Example: Custom Class
+
+```vue
+
+ Button
+
+
+
+```
+
+### Example: Custom Style
+
+```vue
+
+
+ Button
+
+
+```
+
+### Example: Global Theme Variables
+
+```scss
+// uni.scss or App.vue
+$u-primary: #409eff;
+$u-success: #67c23a;
+$u-warning: #e6a23c;
+$u-error: #f56c6c;
+$u-info: #909399;
+```
+
+### Example: Style Override with :deep()
+
+```vue
+
+ Button
+
+
+
+```
+
+### Key Points
+
+- Use custom-class for class-based styling
+- Use custom-style for inline styling
+- Override global theme variables
+- Use :deep() for style penetration
+- Support SCSS variables
diff --git a/.agents/skills/uview-pro-vue3/examples/layout/intro.md b/.agents/skills/uview-pro-vue3/examples/layout/intro.md
new file mode 100644
index 0000000..c44bd20
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/layout/intro.md
@@ -0,0 +1,77 @@
+# Layout Introduction
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example provides an introduction to uView Pro layout templates.
+
+### Key Concepts
+
+- Layout categories
+- Layout usage
+- Responsive layout
+
+### Example: Layout Categories
+
+**Grid Layout (网格布局)**:
+- Grid system
+- Responsive grid
+- Grid columns
+
+**Flex Layout (弹性布局)**:
+- Flex container
+- Flex items
+- Flex direction
+
+**Container Layout (容器布局)**:
+- Container
+- Header
+- Content
+- Footer
+
+### Example: Grid Layout
+
+```vue
+
+
+
+ Item 1
+
+
+ Item 2
+
+
+ Item 3
+
+
+
+```
+
+### Example: Flex Layout
+
+```vue
+
+
+ Item 1
+ Item 2
+ Item 3
+
+
+
+
+```
+
+### Key Points
+
+- Multiple layout options
+- Responsive design
+- Easy to use
+- Well documented
+- Flexible configuration
diff --git a/.agents/skills/uview-pro-vue3/examples/tools/http.md b/.agents/skills/uview-pro-vue3/examples/tools/http.md
new file mode 100644
index 0000000..dd24de4
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/tools/http.md
@@ -0,0 +1,101 @@
+# HTTP Request
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example demonstrates HTTP request utilities in uView Pro.
+
+### Key Concepts
+
+- Request methods
+- Request interceptors
+- Response interceptors
+- Error handling
+
+### Example: Basic Request
+
+```javascript
+import { request } from 'uview-pro'
+
+request({
+ url: '/api/user',
+ method: 'GET'
+}).then(res => {
+ console.log('Response:', res)
+}).catch(err => {
+ console.error('Error:', err)
+})
+```
+
+### Example: POST Request
+
+```javascript
+import { request } from 'uview-pro'
+
+request({
+ url: '/api/user',
+ method: 'POST',
+ data: {
+ name: 'John',
+ email: 'john@example.com'
+ }
+}).then(res => {
+ console.log('Response:', res)
+})
+```
+
+### Example: Request Interceptor
+
+```javascript
+import { request } from 'uview-pro'
+
+request.interceptors.request.use(config => {
+ // Add token
+ config.header = {
+ ...config.header,
+ 'Authorization': 'Bearer token'
+ }
+ return config
+})
+```
+
+### Example: Response Interceptor
+
+```javascript
+import { request } from 'uview-pro'
+
+request.interceptors.response.use(
+ response => {
+ return response.data
+ },
+ error => {
+ console.error('Request error:', error)
+ return Promise.reject(error)
+ }
+)
+```
+
+### Example: Request Config
+
+```javascript
+import { request } from 'uview-pro'
+
+request({
+ url: '/api/user',
+ method: 'GET',
+ header: {
+ 'Content-Type': 'application/json'
+ },
+ timeout: 10000
+})
+```
+
+### Key Points
+
+- Support GET, POST, PUT, DELETE methods
+- Request and response interceptors
+- Error handling
+- Configurable timeout
+- Support custom headers
diff --git a/.agents/skills/uview-pro-vue3/examples/tools/intro.md b/.agents/skills/uview-pro-vue3/examples/tools/intro.md
new file mode 100644
index 0000000..10bdeb3
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/examples/tools/intro.md
@@ -0,0 +1,73 @@
+# Tools Introduction
+
+**官方文档**: https://uviewpro.cn
+
+
+## Instructions
+
+This example provides an introduction to uView Pro tools and utilities.
+
+### Key Concepts
+
+- Tool categories
+- Tool usage
+- Tool methods
+
+### Example: Tool Categories
+
+**HTTP Request (HTTP 请求)**:
+- Request methods
+- Request interceptors
+- Response interceptors
+
+**Storage (存储)**:
+- setStorage, getStorage
+- removeStorage, clearStorage
+
+**Router (路由)**:
+- navigateTo, redirectTo
+- navigateBack, switchTab
+
+**Validator (验证)**:
+- Validation methods
+- Form validation
+
+**Format (格式化)**:
+- Date format
+- Number format
+- Text format
+
+**Color (颜色)**:
+- Color conversion
+- Color utilities
+
+### Example: Using Tools
+
+```javascript
+// HTTP Request
+import { request } from 'uview-pro'
+
+request({
+ url: '/api/user',
+ method: 'GET'
+})
+
+// Storage
+import { setStorage, getStorage } from 'uview-pro'
+
+setStorage('key', 'value')
+const value = getStorage('key')
+
+// Router
+import { navigateTo } from 'uview-pro'
+
+navigateTo('/pages/index/index')
+```
+
+### Key Points
+
+- Rich utility methods
+- Easy to use
+- Well documented
+- TypeScript support
+- Optimized for uni-app
diff --git a/.agents/skills/uview-pro-vue3/templates/component-usage.md b/.agents/skills/uview-pro-vue3/templates/component-usage.md
new file mode 100644
index 0000000..cbb78e8
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/templates/component-usage.md
@@ -0,0 +1,103 @@
+# Component Usage Templates
+
+## Button Usage
+
+```vue
+
+
+ Click Me
+
+
+
+
+```
+
+## Form Usage
+
+```vue
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+```
+
+## List Usage
+
+```vue
+
+
+
+ {{ item.title }}
+
+
+
+
+
+```
+
+## Toast Usage
+
+```vue
+
+ Show Toast
+
+
+
+```
diff --git a/.agents/skills/uview-pro-vue3/templates/installation.md b/.agents/skills/uview-pro-vue3/templates/installation.md
new file mode 100644
index 0000000..c9048e6
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/templates/installation.md
@@ -0,0 +1,56 @@
+# Installation Templates
+
+## npm Installation
+
+```bash
+npm install uview-pro
+```
+
+## Easycom Configuration
+
+```json
+// pages.json
+{
+ "easycom": {
+ "autoscan": true,
+ "custom": {
+ "^u-(.*)": "uview-pro/components/u-$1/u-$1.vue"
+ }
+ }
+}
+```
+
+## Main.js Setup
+
+```javascript
+// main.js
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView)
+ return {
+ app
+ }
+}
+```
+
+## Style Import
+
+```scss
+// App.vue
+@import 'uview-pro/index.scss';
+```
+
+## Complete Setup
+
+```bash
+# Install
+npm install uview-pro
+
+# Configure easycom in pages.json
+# Import in main.js
+# Import styles in App.vue
+```
diff --git a/.agents/skills/uview-pro-vue3/templates/project-setup.md b/.agents/skills/uview-pro-vue3/templates/project-setup.md
new file mode 100644
index 0000000..281a98e
--- /dev/null
+++ b/.agents/skills/uview-pro-vue3/templates/project-setup.md
@@ -0,0 +1,70 @@
+# Project Setup Templates
+
+## uni-app Project Setup
+
+```bash
+# Create uni-app project
+# Using HBuilderX or CLI
+
+# Install uView Pro
+npm install uview-pro
+```
+
+## pages.json Configuration
+
+```json
+{
+ "easycom": {
+ "autoscan": true,
+ "custom": {
+ "^u-(.*)": "uview-pro/components/u-$1/u-$1.vue"
+ }
+ },
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "首页"
+ }
+ }
+ ]
+}
+```
+
+## main.js Setup
+
+```javascript
+import { createSSRApp } from 'vue'
+import uView from 'uview-pro'
+import App from './App.vue'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(uView)
+ return {
+ app
+ }
+}
+```
+
+## App.vue Setup
+
+```vue
+
+
+
+
+
+
+
+
+
+```
diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..654c6d4
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets).
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..0d937a9
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "base",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs
new file mode 100644
index 0000000..98ee7df
--- /dev/null
+++ b/.commitlintrc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+}
diff --git a/.cursor/rules/api-http-patterns.mdc b/.cursor/rules/api-http-patterns.mdc
new file mode 100644
index 0000000..79026c3
--- /dev/null
+++ b/.cursor/rules/api-http-patterns.mdc
@@ -0,0 +1,51 @@
+# API 和 HTTP 请求规范
+
+## HTTP 请求封装
+- 可以使用 `简单http` 或者 `alova` 或者 `@tanstack/vue-query` 进行请求管理
+- HTTP 配置在 [src/http/](mdc:src/http/) 目录下
+- `简单http` - [src/http/http.ts](mdc:src/http/http.ts)
+- `alova` - [src/http/alova.ts](mdc:src/http/alova.ts)
+- `vue-query` - [src/http/vue-query.ts](mdc:src/http/vue-query.ts)
+- 请求拦截器在 [src/http/interceptor.ts](mdc:src/http/interceptor.ts)
+- 支持请求重试、缓存、错误处理
+
+## API 接口规范
+- API 接口定义在 [src/api/](mdc:src/api/) 目录下
+- 按功能模块组织 API 文件
+- 使用 TypeScript 定义请求和响应类型
+- 支持 `简单http`、`alova` 和 `vue-query` 三种请求方式
+
+
+## 示例代码结构
+```typescript
+// API 接口定义
+export interface LoginParams {
+ username: string
+ password: string
+}
+
+export interface LoginResponse {
+ token: string
+ userInfo: UserInfo
+}
+
+// alova 方式
+export const login = (params: LoginParams) =>
+ http.Post('/api/login', params)
+
+// vue-query 方式
+export const useLogin = () => {
+ return useMutation({
+ mutationFn: (params: LoginParams) =>
+ http.post('/api/login', params)
+ })
+}
+```
+
+## 错误处理
+- 统一错误处理在拦截器中配置
+- 支持网络错误、业务错误、认证错误等
+- 自动处理 token 过期和刷新
+---
+globs: src/api/*.ts,src/http/*.ts
+---
diff --git a/.cursor/rules/development-workflow.mdc b/.cursor/rules/development-workflow.mdc
new file mode 100644
index 0000000..4da3f43
--- /dev/null
+++ b/.cursor/rules/development-workflow.mdc
@@ -0,0 +1,43 @@
+# 开发工作流程
+
+## 项目启动
+1. 安装依赖:`pnpm install`
+2. 开发环境:
+ - H5: `pnpm dev` 或 `pnpm dev:h5`
+ - 微信小程序: `pnpm dev:mp`
+ - 支付宝小程序: `pnpm dev:mp-alipay`
+ - APP: `pnpm dev:app`
+
+## 代码规范
+- 使用 ESLint 进行代码检查:`pnpm lint`
+- 自动修复代码格式:`pnpm lint:fix`
+- 使用 eslint 格式化代码
+- 遵循 TypeScript 严格模式
+
+## 构建和部署
+- H5 构建:`pnpm build:h5`
+- 微信小程序构建:`pnpm build:mp`
+- 支付宝小程序构建:`pnpm build:mp-alipay`
+- APP 构建:`pnpm build:app`
+- 类型检查:`pnpm type-check`
+
+## 开发工具
+- 推荐使用 VSCode 编辑器
+- 安装 Vue 和 TypeScript 相关插件
+- 使用 uni-app 开发者工具调试小程序
+- 使用 HBuilderX 调试 APP
+
+## 调试技巧
+- 使用 console.log 和 uni.showToast 调试
+- 利用 Vue DevTools 调试组件状态
+- 使用网络面板调试 API 请求
+- 平台差异测试和兼容性检查
+
+## 性能优化
+- 使用懒加载和代码分割
+- 优化图片和静态资源
+- 减少不必要的重渲染
+- 合理使用缓存策略
+---
+description: 开发工作流程和最佳实践指南
+---
diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc
new file mode 100644
index 0000000..f0d613e
--- /dev/null
+++ b/.cursor/rules/project-overview.mdc
@@ -0,0 +1,36 @@
+---
+alwaysApply: true
+---
+# unibest 项目概览
+
+这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
+
+## 项目特点
+- 支持 H5、小程序、APP 多平台开发
+- 使用最新的前端技术栈
+- 内置约定式路由、layout布局、请求封装、登录拦截、自定义tabbar等功能
+- 无需依赖 HBuilderX,支持命令行开发
+
+## 核心配置文件
+- [package.json](mdc:package.json) - 项目依赖和脚本配置
+- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
+- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
+- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
+- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
+
+## 主要目录结构
+- `src/pages/` - 页面文件
+- `src/components/` - 组件文件
+- `src/layouts/` - 布局文件
+- `src/api/` - API 接口
+- `src/http/` - HTTP 请求封装
+- `src/store/` - 状态管理
+- `src/tabbar/` - 底部导航栏
+- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
+
+## 开发命令
+- `pnpm dev` - 开发 H5 版本
+- `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
+- `pnpm dev:app` - 开发 APP 版本
+- `pnpm build` - 构建生产版本
diff --git a/.cursor/rules/styling-css-patterns.mdc b/.cursor/rules/styling-css-patterns.mdc
new file mode 100644
index 0000000..25f14f2
--- /dev/null
+++ b/.cursor/rules/styling-css-patterns.mdc
@@ -0,0 +1,54 @@
+# 样式和 CSS 开发规范
+
+## UnoCSS 原子化 CSS
+- 项目使用 UnoCSS 作为原子化 CSS 框架
+- 配置在 [uno.config.ts](mdc:uno.config.ts)
+- 支持预设和自定义规则
+- 优先使用原子化类名,减少自定义 CSS
+
+## SCSS 规范
+- 使用 SCSS 预处理器
+- 样式文件使用 `lang="scss"` 和 `scoped` 属性
+- 遵循 BEM 命名规范
+- 使用变量和混入提高复用性
+
+## 样式组织
+- 全局样式在 [src/style/](mdc:src/style/) 目录下
+- 组件样式使用 scoped 作用域
+- 图标字体在 [src/style/iconfont.css](mdc:src/style/iconfont.css)
+- 主题变量在 [src/uni_modules/uni-scss/](mdc:src/uni_modules/uni-scss/) 目录下
+
+## 示例代码结构
+```vue
+
+
+ 标题
+
+
+
+
+
+
+
+
+## 响应式设计
+- 使用 rpx 单位适配不同屏幕
+- 支持横屏和竖屏布局
+- 使用 flexbox 和 grid 布局
+- 考虑不同平台的样式差异
+---
+globs: *.vue,*.scss,*.css
+---
diff --git a/.cursor/rules/uni-app-patterns.mdc b/.cursor/rules/uni-app-patterns.mdc
new file mode 100644
index 0000000..e10403e
--- /dev/null
+++ b/.cursor/rules/uni-app-patterns.mdc
@@ -0,0 +1,63 @@
+# uni-app 开发规范
+
+## 页面开发
+- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
+- 使用约定式路由,文件名即路由路径
+- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
+- definePage的顺序在最上面
+
+## 组件开发
+- 组件文件放在 [src/components/](mdc:src/components/) 或者 [src/pages/xx/components/](mdc:src/pages/xx/components/) 目录下
+- 使用 uni-app 内置组件和第三方组件库
+- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
+- 自定义组件遵循 uni-app 组件规范
+
+## 平台适配
+- 使用条件编译处理平台差异
+- 支持 H5、小程序、APP 多平台
+- 注意各平台的 API 差异
+- 使用 uni.xxx API 替代原生 API
+
+## 示例代码结构
+```vue
+
+
+
+
+
+
+
+
+
+ H5 特有内容
+
+
+
+```
+
+## 生命周期
+- 使用 uni-app 页面生命周期
+- onLoad、onShow、onReady、onHide、onUnload
+- 组件生命周期遵循 Vue3 规范
+- 注意页面栈和导航管理
+---
+globs: src/pages/*.vue,src/components/*.vue
+---
diff --git a/.cursor/rules/vue-typescript-patterns.mdc b/.cursor/rules/vue-typescript-patterns.mdc
new file mode 100644
index 0000000..d81cc8f
--- /dev/null
+++ b/.cursor/rules/vue-typescript-patterns.mdc
@@ -0,0 +1,53 @@
+# Vue3 + TypeScript 开发规范
+
+## Vue 组件规范
+- 使用 Composition API 和 `
+
+
+
+
+
+
+
+
+---
+globs: *.vue,*.ts,*.tsx
+---
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7f09864
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*] # 表示所有文件适用
+charset = utf-8 # 设置文件字符集为 utf-8
+indent_style = space # 缩进风格(tab | space)
+indent_size = 2 # 缩进大小
+end_of_line = lf # 控制换行类型(lf | cr | crlf)
+trim_trailing_whitespace = true # 去除行首的任意空白字符
+insert_final_newline = true # 始终在文件末尾插入一个新行
+
+[*.md] # 表示仅 md 文件适用以下规则
+max_line_length = off # 关闭最大行长度限制
+trim_trailing_whitespace = false # 关闭末尾空格修剪
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..201f3d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+*.local
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.hbuilderx
+
+.stylelintcache
+.eslintcache
+
+docs/.vitepress/dist
+docs/.vitepress/cache
+
+src/types
+# 单独把这个文件排除掉,用以解决部分电脑生成的 auto-import.d.ts 的API不完整导致类型提示报错问题
+!src/types/auto-import.d.ts
+src/manifest.json
+src/pages.json
+
+# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
+# pnpm-lock.yaml
+# package-lock.json
+
+# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
+# `git rm -r --cached .` 然后提交 commit 即可。
+
+# git rm -r --cached file1 file2 ## 针对某些文件
+# git rm -r --cached dir1 dir2 ## 针对某些文件夹
+# git rm -r --cached . ## 针对所有文件
+
+# 更新 uni-app 官方版本
+# npx @dcloudio/uvm@latest
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 0000000..36158d9
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+npx --no-install commitlint --edit "$1"
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..c3ec64b
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged --allow-empty
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..f47ca59
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,9 @@
+# registry = https://registry.npmjs.org
+registry = https://registry.npmmirror.com
+
+strict-peer-dependencies=false
+auto-install-peers=true
+shamefully-hoist=true
+ignore-workspace-root-check=true
+install-workspace-root=true
+node-options=--max-old-space-size=8192
diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md
new file mode 100644
index 0000000..9436a74
--- /dev/null
+++ b/.trae/rules/project_rules.md
@@ -0,0 +1,123 @@
+# unibest 项目概览
+
+这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
+
+## 项目特点
+- 支持 H5、小程序、APP 多平台开发
+- 使用最新的前端技术栈
+- 内置约定式路由、layout布局、请求封装等功能
+- 无需依赖 HBuilderX,支持命令行开发
+
+## 核心配置文件
+- [package.json](mdc:package.json) - 项目依赖和脚本配置
+- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
+- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
+- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
+- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
+
+## 主要目录结构
+- `src/pages/` - 页面文件
+- `src/components/` - 组件文件
+- `src/layouts/` - 布局文件
+- `src/api/` - API 接口
+- `src/http/` - HTTP 请求封装
+- `src/store/` - 状态管理
+- `src/tabbar/` - 底部导航栏
+- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
+
+## 开发命令
+- `pnpm dev` - 开发 H5 版本
+- `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
+- `pnpm dev:app` - 开发 APP 版本
+- `pnpm build` - 构建生产版本
+
+## Vue 组件规范
+- 使用 Composition API 和 `
+
+
+
+
+
+
+
+
+ H5 特有内容
+
+
+
+```
+
+## 生命周期
+- 使用 uni-app 页面生命周期
+- onLoad、onShow、onReady、onHide、onUnload
+- 组件生命周期遵循 Vue3 规范
+- 注意页面栈和导航管理
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..883b74d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,15 @@
+{
+ "recommendations": [
+ "vue.volar",
+ "dbaeumer.vscode-eslint",
+ "antfu.unocss",
+ "antfu.iconify",
+ "evils.uniapp-vscode",
+ "uni-helper.uni-helper-vscode",
+ "uni-helper.uni-app-schemas-vscode",
+ "uni-helper.uni-highlight-vscode",
+ "uni-helper.uni-ui-snippets-vscode",
+ "uni-helper.uni-app-snippets-vscode",
+ "streetsidesoftware.code-spell-checker"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..a5c0481
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,102 @@
+{
+ // 配置语言的文件关联
+ "files.associations": {
+ "pages.json": "jsonc", // pages.json 可以写注释
+ "manifest.json": "jsonc" // manifest.json 可以写注释
+ },
+
+ "stylelint.enable": false, // 禁用 stylelint
+ "css.validate": false, // 禁用 CSS 内置验证
+ "scss.validate": false, // 禁用 SCSS 内置验证
+ "less.validate": false, // 禁用 LESS 内置验证
+
+ // 新版本 VsCode 中这个配置已失效
+ "typescript.tsdk": "node_modules/typescript/lib",
+
+ // 配置新版本 VsCode 工作区的 TypeScript 的版本
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
+ "js/ts.tsdk.promptToUseWorkspaceVersion": true,
+
+ "explorer.fileNesting.enabled": true,
+ "explorer.fileNesting.expand": false,
+ "explorer.fileNesting.patterns": {
+ "README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md",
+ "docker.md": "Dockerfile,docker*.md,nginx*,.dockerignore",
+ "pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
+ "package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
+ "eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
+ },
+
+ // Disable the default formatter, use eslint instead
+ "prettier.enable": false,
+ "editor.formatOnSave": false,
+
+ // Auto fix
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.organizeImports": "never"
+ },
+
+ // Silent the stylistic rules in you IDE, but still auto fix them
+ "eslint.rules.customizations": [
+ { "rule": "style/*", "severity": "off", "fixable": true },
+ { "rule": "format/*", "severity": "off", "fixable": true },
+ { "rule": "*-indent", "severity": "off", "fixable": true },
+ { "rule": "*-spacing", "severity": "off", "fixable": true },
+ { "rule": "*-spaces", "severity": "off", "fixable": true },
+ { "rule": "*-order", "severity": "off", "fixable": true },
+ { "rule": "*-dangle", "severity": "off", "fixable": true },
+ { "rule": "*-newline", "severity": "off", "fixable": true },
+ { "rule": "*quotes", "severity": "off", "fixable": true },
+ { "rule": "*semi", "severity": "off", "fixable": true }
+ ],
+
+ // Enable eslint for all supported languages
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ "typescript",
+ "typescriptreact",
+ "vue",
+ "html",
+ "markdown",
+ "json",
+ "jsonc",
+ "yaml",
+ "toml",
+ "xml",
+ "gql",
+ "graphql",
+ "astro",
+ "svelte",
+ "css",
+ "less",
+ "scss",
+ "pcss",
+ "postcss"
+ ],
+ "cSpell.words": [
+ "alova",
+ "Aplipay",
+ "attributify",
+ "chooseavatar",
+ "climblee",
+ "commitlint",
+ "dcloudio",
+ "iconfont",
+ "oxlint",
+ "qrcode",
+ "refresherrefresh",
+ "scrolltolower",
+ "tabbar",
+ "Toutiao",
+ "uniapp",
+ "unibest",
+ "unocss",
+ "uview",
+ "uvui",
+ "Wechat",
+ "WechatMiniprogram",
+ "Weixin"
+ ]
+}
diff --git a/.vscode/vue3.code-snippets b/.vscode/vue3.code-snippets
new file mode 100644
index 0000000..9277498
--- /dev/null
+++ b/.vscode/vue3.code-snippets
@@ -0,0 +1,80 @@
+{
+ // Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
+ // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
+ // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
+ // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
+ // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
+ // Placeholders with the same ids are connected.
+ // Example:
+ // "Print to console": {
+ // "scope": "javascript,typescript",
+ // "prefix": "log",
+ // "body": [
+ // "console.log('$1');",
+ // "$2"
+ // ],
+ // "description": "Log output to console"
+ // }
+ "Print unibest Vue3 SFC": {
+ "scope": "vue",
+ "prefix": "v3",
+ "body": [
+ "\n",
+ "",
+ " $3",
+ "\n",
+ "\n",
+ ],
+ },
+ "Print unibest style": {
+ "scope": "vue",
+ "prefix": "st",
+ "body": [
+ "\n"
+ ],
+ },
+ "Print unibest script with definePage": {
+ "scope": "vue",
+ "prefix": "sc",
+ "body": [
+ "\n"
+ ],
+ },
+ "Print unibest template": {
+ "scope": "vue",
+ "prefix": "te",
+ "body": [
+ "",
+ " $1",
+ "\n"
+ ],
+ },
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9e91d10
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 菲鸽
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7752e70
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+旧仓库 codercup 进不去了,star 也拿不回来,这里也展示一下那个地址的 star.
+
+[](https://github.com/codercup/unibest)
+[](https://github.com/codercup/unibest)
+
+
+
+
+
+[](https://github.com/feige996/unibest)
+[](https://github.com/feige996/unibest)
+[](https://gitee.com/feige996/unibest/stargazers)
+[](https://gitee.com/feige996/unibest/members)
+
+
+
+
+
+
+
+`unibest` —— 最好的 `uniapp` 开发模板,由 `uniapp` + `Vue3` + `Ts` + `Vite5` + `UnoCss` + `wot-ui` + `z-paging` 构成,使用了最新的前端技术栈,无需依靠 `HBuilderX`,通过命令行方式运行 `web`、`小程序` 和 `App`(编辑器推荐 `VSCode`,可选 `webstorm`)。
+
+`unibest` 内置了 `约定式路由`、`layout布局`、`请求封装`、`请求拦截`、`登录拦截`、`UnoCSS`、`i18n多语言` 等基础功能,提供了 `代码提示`、`自动格式化`、`统一配置`、`代码片段` 等辅助功能,让你编写 `uniapp` 拥有 `best` 体验 ( `unibest 的由来`)。
+
+
+
+
+ 📖 文档地址(new)
+ |
+ 📱 DEMO 地址
+
+
+---
+
+注意旧的地址 [codercup](https://github.com/codercup/unibest) 我进不去了,使用新的 [feige996](https://github.com/feige996/unibest)。PR和 issue 也请使用新地址,否则无法合并。
+
+## 平台兼容性
+
+| H5 | IOS | 安卓 | 微信小程序 | 字节小程序 | 快手小程序 | 支付宝小程序 | 钉钉小程序 | 百度小程序 |
+| --- | --- | ---- | ---------- | ---------- | ---------- | ------------ | ---------- | ---------- |
+| √ | √ | √ | √ | √ | √ | √ | √ | √ |
+
+注意每种 `UI框架` 支持的平台有所不同,详情请看各 `UI框架` 的官网,也可以看 `unibest` 文档。
+
+## ⚙️ 环境
+
+- node>=18
+- pnpm>=7.30
+- Vue Official>=2.1.10
+- TypeScript>=5.0
+
+## 新版分支
+- main == base
+- base --> base-i18n
+- base-login --> base-login-i18n
+
+## 📂 快速开始
+
+执行 `pnpm create unibest` 创建项目
+执行 `pnpm i` 安装依赖
+执行 `pnpm dev` 运行 `H5`
+执行 `pnpm dev:mp` 运行 `微信小程序`
+
+## 📦 运行(支持热更新)
+
+- web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
+- weixin平台:`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
+- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来运行到对应的平台。)
+
+## 🔗 发布
+
+- web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
+- weixin平台:`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
+- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来发行到对应的平台。)
+
+## 📄 License
+
+[MIT](https://opensource.org/license/mit/)
+
+Copyright (c) 2025 菲鸽
+
+## 捐赠
+
+
+
+
+
diff --git a/env/.env b/env/.env
new file mode 100644
index 0000000..3d5d4c3
--- /dev/null
+++ b/env/.env
@@ -0,0 +1,42 @@
+VITE_APP_TITLE = 'unibest'
+VITE_APP_PORT = 9000
+
+VITE_UNI_APPID = '__UNI__D1E5001'
+VITE_WX_APPID = 'wxa2abb91f64032a2b'
+
+# 微信开发者工具 CLI 路径,仅当默认安装路径不正确时配置(就是当 pnpm dev:mp 无法自动打开微信开发者工具时,才需要配置,通常是你更改了默认的安装位置导致的,一般出现在windows系统)
+# macOS 示例:
+# WECHAT_DEVTOOLS_CLI_PATH = '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
+# Windows 示例:
+# WECHAT_DEVTOOLS_CLI_PATH = 'C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat'
+
+# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
+# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
+# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
+VITE_APP_PUBLIC_BASE=/
+
+# 默认后台请求地址
+# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。
+# 微信小程序如果没有配置下面的专用地址,也会回退使用这个值。
+VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
+# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
+
+# 微信小程序专用后台请求地址,按微信开发者工具 envVersion 区分。
+# 不配置时会回退使用 VITE_SERVER_BASEURL。
+# VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://dev.xxx.com'
+# VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://trial.xxx.com'
+# VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://prod.xxx.com'
+
+# h5是否需要配置代理
+VITE_APP_PROXY_ENABLE = false
+# 下面的不用修改,只要不跟你后台的统一前缀冲突就行。如果修改了,记得修改 `nginx` 里面的配置
+VITE_APP_PROXY_PREFIX = '/fg-api'
+
+# 第二个请求地址 (目前alova中可以使用)
+VITE_SERVER_BASEURL_SECONDARY = 'https://ukw0y1.laf.run'
+
+# 认证模式,'single' | 'double' ==> 单token | 双token
+VITE_AUTH_MODE = 'single'
+
+# 原生插件资源复制开关,启用后 App 构建会把根目录 nativeplugins 复制到 dist
+VITE_COPY_NATIVE_RES_ENABLE = true
diff --git a/env/.env.development b/env/.env.development
new file mode 100644
index 0000000..bd3b38d
--- /dev/null
+++ b/env/.env.development
@@ -0,0 +1,9 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'development'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = false
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = false
+
+# development mode 后台请求地址
+# VITE_SERVER_BASEURL = 'https://dev.xxx.com'
diff --git a/env/.env.production b/env/.env.production
new file mode 100644
index 0000000..0d2ff4b
--- /dev/null
+++ b/env/.env.production
@@ -0,0 +1,9 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'production'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = true
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = false
+
+# production mode 后台请求地址
+# VITE_SERVER_BASEURL = 'https://prod.xxx.com'
diff --git a/env/.env.test b/env/.env.test
new file mode 100644
index 0000000..8290ad2
--- /dev/null
+++ b/env/.env.test
@@ -0,0 +1,9 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'development'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = false
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = false
+
+# test mode 后台请求地址
+# VITE_SERVER_BASEURL = 'https://test.xxx.com'
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..426985a
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,60 @@
+import uniHelper from '@uni-helper/eslint-config'
+
+export default uniHelper({
+ unocss: true,
+ vue: true,
+ markdown: false,
+ ignores: [
+ // 忽略uni_modules目录
+ '**/uni_modules/',
+ // 忽略原生插件目录
+ '**/nativeplugins/',
+ 'dist',
+ // unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
+ 'auto-import.d.ts',
+ // vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
+ 'uni-pages.d.ts',
+ // 插件生成的文件
+ 'src/pages.json',
+ 'src/manifest.json',
+ // 忽略自动生成文件
+ 'src/service/**',
+ ],
+ // https://eslint-config.antfu.me/rules
+ rules: {
+ 'no-useless-return': 'off',
+ 'no-console': 'off',
+ 'no-unused-vars': 'off',
+ 'vue/no-unused-refs': 'off',
+ 'unused-imports/no-unused-vars': 'off',
+ 'eslint-comments/no-unlimited-disable': 'off',
+ 'jsdoc/check-param-names': 'off',
+ 'jsdoc/require-returns-description': 'off',
+ 'ts/no-empty-object-type': 'off',
+ 'no-extend-native': 'off',
+ // uni 条件编译注释可能包裹 import,自动排序会破坏平台条件边界
+ 'perfectionist/sort-imports': 'off',
+ 'vue/singleline-html-element-content-newline': [
+ 'error',
+ {
+ externalIgnores: ['text'],
+ },
+ ],
+ // vue SFC 调换顺序改这里
+ 'vue/block-order': ['error', {
+ order: [['script', 'template'], 'style'],
+ }],
+ },
+ formatters: {
+ /**
+ * Format CSS, LESS, SCSS files, also the `
diff --git a/src/api/foo-alova.ts b/src/api/foo-alova.ts
new file mode 100644
index 0000000..de35095
--- /dev/null
+++ b/src/api/foo-alova.ts
@@ -0,0 +1,17 @@
+import { API_DOMAINS, http } from '@/http/alova'
+
+export interface IFoo {
+ id: number
+ name: string
+}
+
+export function foo() {
+ return http.Get('/foo', {
+ params: {
+ name: '菲鸽',
+ page: 1,
+ pageSize: 10,
+ },
+ meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址
+ })
+}
diff --git a/src/api/foo.ts b/src/api/foo.ts
new file mode 100644
index 0000000..a500002
--- /dev/null
+++ b/src/api/foo.ts
@@ -0,0 +1,43 @@
+import { http } from '@/http/http'
+
+export interface IFoo {
+ id: number
+ name: string
+}
+
+export function foo() {
+ return http.Get('/foo', {
+ params: {
+ name: '菲鸽',
+ page: 1,
+ pageSize: 10,
+ },
+ })
+}
+
+export interface IFooItem {
+ id: string
+ name: string
+}
+
+/** GET 请求 */
+export async function getFooAPI(name: string) {
+ return await http.get('/foo', { name })
+}
+/** GET 请求;支持 传递 header 的范例 */
+export function getFooAPI2(name: string) {
+ return http.get('/foo', { name }, { 'Content-Type-100': '100' })
+}
+
+/** POST 请求 */
+export function postFooAPI(name: string) {
+ return http.post('/foo', { name })
+}
+/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
+export function postFooAPI2(name: string) {
+ return http.post('/foo', { name }, { a: 1, b: 2 })
+}
+/** POST 请求;支持 传递 header 的范例 */
+export function postFooAPI3(name: string) {
+ return http.post('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' })
+}
diff --git a/src/api/login.ts b/src/api/login.ts
new file mode 100644
index 0000000..7691c9b
--- /dev/null
+++ b/src/api/login.ts
@@ -0,0 +1,85 @@
+import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
+import { http } from '@/http/http'
+
+/**
+ * 登录表单
+ */
+export interface ILoginForm {
+ username: string
+ password: string
+}
+
+/**
+ * 获取验证码
+ * @returns ICaptcha 验证码
+ */
+export function getCode() {
+ return http.get('/user/getCode')
+}
+
+/**
+ * 用户登录
+ * @param loginForm 登录表单
+ */
+export function login(loginForm: ILoginForm) {
+ return http.post('/auth/login', loginForm)
+}
+
+/**
+ * 刷新token
+ * @param refreshToken 刷新token
+ */
+export function refreshToken(refreshToken: string) {
+ return http.post('/auth/refreshToken', { refreshToken })
+}
+
+/**
+ * 获取用户信息
+ */
+export function getUserInfo() {
+ return http.get('/user/info')
+}
+
+/**
+ * 退出登录
+ */
+export function logout() {
+ return http.get('/auth/logout')
+}
+
+/**
+ * 修改用户信息
+ */
+export function updateInfo(data: IUpdateInfo) {
+ return http.post('/user/updateInfo', data)
+}
+
+/**
+ * 修改用户密码
+ */
+export function updateUserPassword(data: IUpdatePassword) {
+ return http.post('/user/updatePassword', data)
+}
+
+/**
+ * 获取微信登录凭证
+ * @returns Promise 包含微信登录凭证(code)
+ */
+export function getWxCode() {
+ return new Promise((resolve, reject) => {
+ uni.login({
+ provider: 'weixin',
+ success: res => resolve(res),
+ fail: err => reject(new Error(err)),
+ })
+ })
+}
+
+/**
+ * 微信登录
+ * @param params 微信登录参数,包含code
+ * @returns Promise 包含登录结果
+ */
+export function wxLogin(data: { code: string }) {
+ return http.post('/auth/wxLogin', data)
+}
diff --git a/src/api/types/login.ts b/src/api/types/login.ts
new file mode 100644
index 0000000..b5f49e8
--- /dev/null
+++ b/src/api/types/login.ts
@@ -0,0 +1,102 @@
+// 认证模式类型
+export type AuthMode = 'single' | 'double'
+
+// 单Token响应类型
+export interface ISingleTokenRes {
+ token: string
+ expiresIn: number // 有效期(秒)
+}
+
+// 双Token响应类型
+export interface IDoubleTokenRes {
+ accessToken: string
+ refreshToken: string
+ accessExpiresIn: number // 访问令牌有效期(秒)
+ refreshExpiresIn: number // 刷新令牌有效期(秒)
+}
+
+/**
+ * 登录返回的信息,其实就是 token 信息
+ */
+export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
+
+/**
+ * 用户信息
+ */
+export type UserRole = string
+
+export interface IUserInfoRes {
+ userId: number
+ username: string
+ nickname: string
+ avatar?: string
+ /** 同时支持单角色和多角色,你自行选择一种就行 */
+ role?: UserRole
+ roles?: UserRole[]
+ [key: string]: any // 允许其他扩展字段
+}
+
+// 认证存储数据结构
+export interface AuthStorage {
+ mode: AuthMode
+ tokens: ISingleTokenRes | IDoubleTokenRes
+ userInfo?: IUserInfoRes
+ loginTime: number // 登录时间戳
+}
+
+/**
+ * 获取验证码
+ */
+export interface ICaptcha {
+ captchaEnabled: boolean
+ uuid: string
+ image: string
+}
+/**
+ * 上传成功的信息
+ */
+export interface IUploadSuccessInfo {
+ fileId: number
+ originalName: string
+ fileName: string
+ storagePath: string
+ fileHash: string
+ fileType: string
+ fileBusinessType: string
+ fileSize: number
+}
+/**
+ * 更新用户信息
+ */
+export interface IUpdateInfo {
+ id: number
+ name: string
+ sex: string
+}
+/**
+ * 更新用户信息
+ */
+export interface IUpdatePassword {
+ id: number
+ oldPassword: string
+ newPassword: string
+ confirmPassword: string
+}
+
+/**
+ * 判断是否为单Token响应
+ * @param tokenRes 登录响应数据
+ * @returns 是否为单Token响应
+ */
+export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
+ return 'token' in tokenRes && !('refreshToken' in tokenRes)
+}
+
+/**
+ * 判断是否为双Token响应
+ * @param tokenRes 登录响应数据
+ * @returns 是否为双Token响应
+ */
+export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
+ return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
+}
diff --git a/src/components/.gitkeep b/src/components/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..b1a4533
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,41 @@
+///
+///
+
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
+
+interface ImportMetaEnv {
+ /** 网站标题,应用名称 */
+ readonly VITE_APP_TITLE: string
+ /** 服务端口号 */
+ readonly VITE_SERVER_PORT: string
+ /** 后台接口地址 */
+ readonly VITE_SERVER_BASEURL: string
+ /** 微信小程序开发版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
+ readonly VITE_SERVER_BASEURL__WEIXIN_DEVELOP?: string
+ /** 微信小程序体验版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
+ readonly VITE_SERVER_BASEURL__WEIXIN_TRIAL?: string
+ /** 微信小程序正式版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
+ readonly VITE_SERVER_BASEURL__WEIXIN_RELEASE?: string
+ /** H5是否需要代理 */
+ readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
+ /** H5是否需要代理,需要的话有个前缀 */
+ readonly VITE_APP_PROXY_PREFIX: string
+ /** 后端是否有统一前缀 /api */
+ readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
+ /** 认证模式,'single' | 'double' ==> 单token | 双token */
+ readonly VITE_AUTH_MODE: 'single' | 'double'
+ /** 是否清除console */
+ readonly VITE_DELETE_CONSOLE: string
+ // 更多环境变量...
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
+
+declare const __VITE_APP_PROXY__: 'true' | 'false'
diff --git a/src/hooks/useRequest.test.ts b/src/hooks/useRequest.test.ts
new file mode 100644
index 0000000..8a205c7
--- /dev/null
+++ b/src/hooks/useRequest.test.ts
@@ -0,0 +1,74 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+import { defineComponent, h } from 'vue'
+import useRequest from './useRequest'
+
+/**
+ * 在 Vue 应用上下文中运行 composable。
+ * composable 的 ref/computed/onMounted 只能在 setup() 内使用,
+ * withSetup 通过挂载一个临时组件来提供这个上下文。
+ */
+function withSetup(composableFn: () => T): T {
+ let result!: T
+ const Comp = defineComponent({
+ setup() {
+ result = composableFn()
+ return () => h('div')
+ },
+ })
+ const wrapper = mount(Comp)
+ wrapper.unmount()
+ return result
+}
+
+describe('useRequest', () => {
+ it('初始状态:loading=false, error=false, data=undefined', () => {
+ const asyncFn = vi.fn().mockResolvedValue('data')
+ const { loading, error, data } = withSetup(() => useRequest(asyncFn))
+
+ expect(loading.value).toBe(false)
+ expect(error.value).toBe(false)
+ expect(data.value).toBeUndefined()
+ })
+
+ it('initialData:初始 data 使用传入的默认值', () => {
+ const asyncFn = vi.fn().mockResolvedValue('new')
+ const { data } = withSetup(() => useRequest(asyncFn, { initialData: 'init' }))
+
+ expect(data.value).toBe('init')
+ })
+
+ it('run 成功:loading 先变 true 后变 false,data 更新为返回值', async () => {
+ const asyncFn = vi.fn().mockResolvedValue('result')
+ const { loading, data, run } = withSetup(() => useRequest(asyncFn))
+
+ const runPromise = run()
+ expect(loading.value).toBe(true)
+
+ await runPromise
+
+ expect(loading.value).toBe(false)
+ expect(data.value).toBe('result')
+ })
+
+ it('run 失败:抛出错误,error 被设置,loading 重置为 false', async () => {
+ const err = new Error('network error')
+ const asyncFn = vi.fn().mockRejectedValue(err)
+ const { loading, error, run } = withSetup(() => useRequest(asyncFn))
+
+ await expect(run()).rejects.toThrow('network error')
+
+ expect(loading.value).toBe(false)
+ expect(error.value).toBe(err)
+ })
+
+ it('immediate=true:组件挂载时立即调用异步函数并更新 data', async () => {
+ const asyncFn = vi.fn().mockResolvedValue('eager')
+ const { data } = withSetup(() => useRequest(asyncFn, { immediate: true }))
+
+ expect(asyncFn).toHaveBeenCalledTimes(1)
+ // 等待 Promise 完成
+ await asyncFn.mock.results[0].value
+ expect(data.value).toBe('eager')
+ })
+})
diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts
new file mode 100644
index 0000000..8ac4bfe
--- /dev/null
+++ b/src/hooks/useRequest.ts
@@ -0,0 +1,54 @@
+import type { Ref } from 'vue'
+import { ref } from 'vue'
+
+interface IUseRequestOptions {
+ /** 是否立即执行 */
+ immediate?: boolean
+ /** 初始化数据 */
+ initialData?: T
+}
+
+interface IUseRequestReturn {
+ loading: Ref
+ error: Ref
+ data: Ref
+ run: (args?: P) => Promise
+}
+
+/**
+ * useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
+ * @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
+ * @param options 包含请求选项的对象 {immediate, initialData}。
+ * @param options.immediate 是否立即执行请求,默认为false。
+ * @param options.initialData 初始化数据,默认为undefined。
+ * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
+ */
+export default function useRequest(
+ func: (args?: P) => Promise,
+ options: IUseRequestOptions = { immediate: false },
+): IUseRequestReturn {
+ const loading = ref(false)
+ const error = ref(false)
+ const data = ref(options.initialData) as Ref
+ const run = async (args?: P) => {
+ loading.value = true
+ return func(args)
+ .then((res) => {
+ data.value = res
+ error.value = false
+ return data.value
+ })
+ .catch((err) => {
+ error.value = err
+ throw err
+ })
+ .finally(() => {
+ loading.value = false
+ })
+ }
+
+ if (options.immediate) {
+ (run as (args: P) => Promise)({} as P)
+ }
+ return { loading, error, data, run }
+}
diff --git a/src/hooks/useScroll.md b/src/hooks/useScroll.md
new file mode 100644
index 0000000..bb2eace
--- /dev/null
+++ b/src/hooks/useScroll.md
@@ -0,0 +1,116 @@
+# 上拉刷新和下拉加载更多
+
+在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
+场景一 页面滚动
+
+```
+definePage({
+ style: {
+ navigationBarTitleText: '上拉刷新和下拉加载更多',
+ enablePullDownRefresh: true,
+ onReachBottomDistance: 100,
+ },
+})
+```
+
+场景二 局部滚动 结合 `scroll-view`
+
+## 关键文件
+
+- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
+- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
+
+## `useScroll` Hook
+
+`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
+
+### 主要功能
+
+- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
+- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
+- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
+- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
+
+### 使用方法
+
+```typescript
+import { useScroll } from '@/hooks/useScroll'
+import { getList } from '@/service/list' // 你的数据请求API
+
+const {
+ list, // 响应式的数据列表
+ loading, // 是否加载中
+ finished, // 是否已全部加载
+ error, // 是否加载失败
+ onScrollToLower, // 滚动到底部时触发的事件
+ onRefresherRefresh, // 下拉刷新时触发的事件
+} = useScroll(getList) // 将获取数据的API函数传入
+```
+
+## `scroll-view` 组件
+
+`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
+
+### 关键属性
+
+- `scroll-y`: 允许纵向滚动。
+- `refresher-enabled`: 启用下拉刷新。
+- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
+- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
+- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
+
+## 示例代码
+
+以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。
+
+```vue
+
+
+
+
+ {{ item.name }}
+
+
+
+ 加载中...
+ 没有更多了
+ 加载失败,请重试
+
+
+
+
+
+
+
+```
+
+## 实现步骤总结
+
+1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
+2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
+3. **模板绑定**:
+ - 使用 `scroll-view` 组件作为滚动容器。
+ - 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
+ - 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
+ - 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
+4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
+5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
+
+通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
\ No newline at end of file
diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts
new file mode 100644
index 0000000..1563223
--- /dev/null
+++ b/src/hooks/useScroll.ts
@@ -0,0 +1,74 @@
+import type { Ref } from 'vue'
+import { onMounted, ref } from 'vue'
+
+interface UseScrollOptions {
+ fetchData: (page: number, pageSize: number) => Promise
+ pageSize?: number
+}
+
+interface UseScrollReturn {
+ list: Ref
+ loading: Ref
+ finished: Ref
+ error: Ref
+ refresh: () => Promise
+ loadMore: () => Promise
+}
+
+export function useScroll({
+ fetchData,
+ pageSize = 10,
+}: UseScrollOptions): UseScrollReturn {
+ const list = ref([]) as Ref
+ const loading = ref(false)
+ const finished = ref(false)
+ const error = ref(null)
+ const page = ref(1)
+
+ const loadData = async () => {
+ if (loading.value || finished.value)
+ return
+
+ loading.value = true
+ error.value = null
+
+ try {
+ const data = await fetchData(page.value, pageSize)
+ if (data.length < pageSize) {
+ finished.value = true
+ }
+ list.value.push(...data)
+ page.value++
+ }
+ catch (err) {
+ error.value = err
+ }
+ finally {
+ loading.value = false
+ }
+ }
+
+ const refresh = async () => {
+ page.value = 1
+ finished.value = false
+ list.value = []
+ await loadData()
+ }
+
+ const loadMore = async () => {
+ await loadData()
+ }
+
+ onMounted(() => {
+ refresh()
+ })
+
+ return {
+ list,
+ loading,
+ finished,
+ error,
+ refresh,
+ loadMore,
+ }
+}
diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts
new file mode 100644
index 0000000..7c9700a
--- /dev/null
+++ b/src/hooks/useUpload.ts
@@ -0,0 +1,171 @@
+import { ref } from 'vue'
+import { getEnvBaseUrl } from '@/utils/index'
+
+const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
+
+type TfileType = 'image' | 'file'
+type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
+type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
+
+interface TOptions {
+ formData?: Record
+ maxSize?: number
+ accept?: T extends 'image' ? TImage[] : TFile[]
+ fileType?: T
+ success?: (params: any) => void
+ error?: (err: any) => void
+}
+
+export default function useUpload(options: TOptions = {} as TOptions) {
+ const {
+ formData = {},
+ maxSize = 5 * 1024 * 1024,
+ accept = ['*'],
+ fileType = 'image',
+ success,
+ error: onError,
+ } = options
+
+ const loading = ref(false)
+ const error = ref(null)
+ const data = ref(null)
+
+ const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
+ if (size > maxSize) {
+ uni.showToast({
+ title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
+ icon: 'none',
+ })
+ return
+ }
+
+ // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
+ // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
+
+ // if (!isTypeValid) {
+ // uni.showToast({
+ // title: `仅支持 ${accept.join(', ')} 格式的文件`,
+ // icon: 'none',
+ // })
+ // return
+ // }
+
+ loading.value = true
+ uploadFile({
+ tempFilePath,
+ formData,
+ onSuccess: (res) => {
+ // 修改这里的解析逻辑,适应不同平台的返回格式
+ let parsedData = res
+ try {
+ // 尝试解析为JSON
+ const jsonData = JSON.parse(res)
+ // 检查是否包含data字段
+ parsedData = jsonData.data || jsonData
+ }
+ catch (e) {
+ // 如果解析失败,使用原始数据
+ console.log('Response is not JSON, using raw data:', res)
+ }
+ data.value = parsedData
+ // console.log('上传成功', res)
+ success?.(parsedData)
+ },
+ onError: (err) => {
+ error.value = err
+ onError?.(err)
+ },
+ onComplete: () => {
+ loading.value = false
+ },
+ })
+ }
+
+ const run = () => {
+ // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
+ // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
+ const chooseFileOptions = {
+ count: 1,
+ success: (res: any) => {
+ console.log('File selected successfully:', res)
+ // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
+ // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
+ // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
+ // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
+ // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
+ let tempFilePath = ''
+ let size = 0
+ // #ifdef MP-WEIXIN
+ tempFilePath = res.tempFiles[0].tempFilePath
+ size = res.tempFiles[0].size
+ // #endif
+ // #ifndef MP-WEIXIN
+ tempFilePath = res.tempFilePaths[0]
+ size = res.tempFiles[0].size
+ // #endif
+ handleFileChoose({ tempFilePath, size })
+ },
+ fail: (err: any) => {
+ console.error('File selection failed:', err)
+ error.value = err
+ onError?.(err)
+ },
+ }
+
+ if (fileType === 'image') {
+ // #ifdef MP-WEIXIN
+ uni.chooseMedia({
+ ...chooseFileOptions,
+ mediaType: ['image'],
+ })
+ // #endif
+
+ // #ifndef MP-WEIXIN
+ uni.chooseImage(chooseFileOptions)
+ // #endif
+ }
+ else {
+ uni.chooseFile({
+ ...chooseFileOptions,
+ type: 'all',
+ })
+ }
+ }
+
+ return { loading, error, data, run }
+}
+
+async function uploadFile({
+ tempFilePath,
+ formData,
+ onSuccess,
+ onError,
+ onComplete,
+}: {
+ tempFilePath: string
+ formData: Record
+ onSuccess: (data: any) => void
+ onError: (err: any) => void
+ onComplete: () => void
+}) {
+ uni.uploadFile({
+ url: VITE_UPLOAD_BASEURL,
+ filePath: tempFilePath,
+ name: 'file',
+ formData,
+ success: (uploadFileRes) => {
+ try {
+ const data = uploadFileRes.data
+ onSuccess(data)
+ }
+ catch (err) {
+ onError(err)
+ }
+ },
+ fail: (err) => {
+ console.error('Upload failed:', err)
+ onError(err)
+ },
+ complete: onComplete,
+ })
+}
diff --git a/src/http/README.md b/src/http/README.md
new file mode 100644
index 0000000..f679106
--- /dev/null
+++ b/src/http/README.md
@@ -0,0 +1,59 @@
+# 请求库
+
+目前 unibest 支持 3 种请求方式:
+
+- 简单版 `http`:路径 `src/http/http.ts`,适合大多数简单项目。
+- `alova`:路径 `src/http/alova.ts`。
+- `vue-query`:路径 `src/http/vue-query.ts`,主要用于自动生成接口,详情见 https://unibest.tech/base/17-generate 。
+
+## 如何选择
+
+如果您以前用过 `alova` 或 `vue-query`,可以优先使用熟悉的方案。
+
+如果项目接口不复杂,简单版 `http` 就够了,也不会增加额外包体积。
+
+## 关于 http 使用
+
+```ts
+import { http } from '@/http/http'
+
+interface IUserInfoRes {
+ id: number
+ nickname: string
+}
+
+export function getUserInfo() {
+ return http.get('/user/info')
+}
+
+export function updateUserInfo(data: Partial) {
+ return http.post('/user/update', data)
+}
+```
+
+响应成功时会返回业务 `data`;业务错误、登录失效、HTTP 状态码异常和网络异常会统一 reject `HttpError`:
+
+```ts
+import type { HttpError } from '@/http/types'
+
+try {
+ const userInfo = await getUserInfo()
+ console.log(userInfo.nickname)
+}
+catch (error) {
+ const httpError = error as HttpError
+ console.log(httpError.type, httpError.message, httpError.statusCode)
+}
+```
+
+如果调用方需要自行处理错误提示,可以传入 `hideErrorToast: true`:
+
+```ts
+http.get('/user/info', undefined, undefined, {
+ hideErrorToast: true,
+})
+```
+
+## roadmap
+
+菲鸽最近在优化脚手架,后续可以选择是否使用第三方请求库,以及选择具体请求库。
diff --git a/src/http/alova.ts b/src/http/alova.ts
new file mode 100644
index 0000000..b7b9ff6
--- /dev/null
+++ b/src/http/alova.ts
@@ -0,0 +1,119 @@
+import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
+import type { IResponse } from './types'
+import AdapterUniapp from '@alova/adapter-uniapp'
+import { createAlova } from 'alova'
+import { createServerTokenAuthentication } from 'alova/client'
+import VueHook from 'alova/vue'
+import { toLoginPage } from '@/utils/toLoginPage'
+import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
+
+// 配置动态Tag
+export const API_DOMAINS = {
+ DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
+ SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
+}
+
+/**
+ * 创建请求实例
+ */
+const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
+ typeof VueHook,
+ typeof uniappRequestAdapter
+>({
+ // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
+ refreshTokenOnError: {
+ isExpired: (error) => {
+ return error.response?.status === ResultEnum.Unauthorized
+ },
+ handler: async () => {
+ try {
+ // await authLogin();
+ }
+ catch (error) {
+ // 切换到登录页
+ toLoginPage({ mode: 'reLaunch' })
+ throw error
+ }
+ },
+ },
+})
+
+/**
+ * alova 请求实例
+ */
+const alovaInstance = createAlova({
+ baseURL: API_DOMAINS.DEFAULT,
+ ...AdapterUniapp(),
+ timeout: 5000,
+ statesHook: VueHook,
+
+ beforeRequest: onAuthRequired((method) => {
+ // 设置默认 Content-Type
+ method.config.headers = {
+ ContentType: ContentTypeEnum.JSON,
+ Accept: 'application/json, text/plain, */*',
+ ...method.config.headers,
+ }
+
+ const { config } = method
+ const ignoreAuth = !config.meta?.ignoreAuth
+ console.log('ignoreAuth===>', ignoreAuth)
+ // 处理认证信息 自行处理认证问题
+ if (ignoreAuth) {
+ const token = 'getToken()'
+ if (!token) {
+ throw new Error('[请求错误]:未登录')
+ }
+ // method.config.headers.token = token;
+ }
+
+ // 处理动态域名
+ if (config.meta?.domain) {
+ method.baseURL = config.meta.domain
+ console.log('当前域名', method.baseURL)
+ }
+ }),
+
+ responded: onResponseRefreshToken((response, method) => {
+ const { config } = method
+ const { requestType } = config
+ const {
+ statusCode,
+ data: rawData,
+ errMsg,
+ } = response as UniNamespace.RequestSuccessCallbackResult
+
+ // 处理特殊请求类型(上传/下载)
+ if (requestType === 'upload' || requestType === 'download') {
+ return response
+ }
+
+ // 处理 HTTP 状态码错误
+ if (statusCode !== 200) {
+ const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
+ console.error('errorMessage===>', errorMessage)
+ uni.showToast({
+ title: errorMessage,
+ icon: 'error',
+ })
+ throw new Error(`${errorMessage}:${errMsg}`)
+ }
+
+ // 处理业务逻辑错误
+ const { code, message, data } = rawData as IResponse
+ // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
+ if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+ if (config.meta?.toast !== false) {
+ uni.showToast({
+ title: message,
+ icon: 'none',
+ })
+ }
+ throw new Error(`请求错误[${code}]:${message}`)
+ }
+ // 处理成功响应,返回业务数据
+ return data
+ }),
+})
+
+export const http = alovaInstance
diff --git a/src/http/http.ts b/src/http/http.ts
new file mode 100644
index 0000000..46a35f6
--- /dev/null
+++ b/src/http/http.ts
@@ -0,0 +1,243 @@
+import type { IDoubleTokenRes } from '@/api/types/login'
+import type { CustomRequestOptions, HttpError, IResponse } from '@/http/types'
+import { nextTick } from 'vue'
+import { useTokenStore } from '@/store/token'
+import { isDoubleTokenMode } from '@/utils'
+import { toLoginPage } from '@/utils/toLoginPage'
+import { createHttpError, getResponseMessage, HttpErrorType, isSuccessResultCode, ResultEnum, ShowMessage } from './tools/enum'
+
+// 刷新 token 状态管理
+let refreshing = false // 防止重复刷新 token 标识
+let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
+
+export function http(options: CustomRequestOptions) {
+ // 1. 返回 Promise 对象
+ return new Promise((resolve, reject) => {
+ uni.request({
+ ...options,
+ dataType: 'json',
+ // #ifndef MP-WEIXIN
+ responseType: 'json',
+ // #endif
+ // 响应成功
+ success: async (res) => {
+ const responseData = res.data as Partial>
+ const code = responseData?.code
+
+ // 检查是否是401错误(包括HTTP状态码401或业务码401)
+ const isTokenExpired = res.statusCode === 401 || code === ResultEnum.Unauthorized
+
+ if (isTokenExpired) {
+ const tokenStore = useTokenStore()
+ if (!isDoubleTokenMode) {
+ // 未启用双token策略,清理用户信息,跳转到登录页
+ tokenStore.logout()
+ toLoginPage()
+ return reject(createHttpError({
+ type: HttpErrorType.Auth,
+ code,
+ statusCode: res.statusCode,
+ message: getResponseMessage(responseData, '登录已过期,请重新登录'),
+ data: responseData?.data,
+ raw: res,
+ }))
+ }
+
+ /* -------- 无感刷新 token ----------- */
+ const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
+ // token 失效的,且有刷新 token 的,才放到请求队列里
+ if (refreshToken) {
+ taskQueue.push(() => {
+ resolve(http(options))
+ })
+ }
+
+ // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
+ if (refreshToken && !refreshing) {
+ refreshing = true
+ try {
+ // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
+ await tokenStore.refreshToken()
+ // 刷新 token 成功
+ refreshing = false
+ nextTick(() => {
+ // 关闭其他弹窗
+ uni.hideToast()
+ uni.showToast({
+ title: 'token 刷新成功',
+ icon: 'none',
+ })
+ })
+ // 将任务队列的所有任务重新请求
+ taskQueue.forEach(task => task())
+ }
+ catch (refreshErr) {
+ console.error('刷新 token 失败:', refreshErr)
+ refreshing = false
+ // 刷新 token 失败,跳转到登录页
+ nextTick(() => {
+ // 关闭其他弹窗
+ uni.hideToast()
+ uni.showToast({
+ title: '登录已过期,请重新登录',
+ icon: 'none',
+ })
+ })
+ // 清除用户信息
+ await tokenStore.logout()
+ // 跳转到登录页
+ setTimeout(() => {
+ toLoginPage()
+ }, 2000)
+ }
+ finally {
+ // 不管刷新 token 成功与否,都清空任务队列
+ taskQueue = []
+ }
+ }
+
+ return reject(createHttpError({
+ type: HttpErrorType.Auth,
+ code,
+ statusCode: res.statusCode,
+ message: getResponseMessage(responseData, '登录已过期,请重新登录'),
+ data: responseData?.data,
+ raw: res,
+ }))
+ }
+
+ // 处理其他成功状态(HTTP状态码200-299)
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ // 处理业务逻辑错误
+ if (!isSuccessResultCode(code as number)) {
+ const httpError = createHttpError({
+ type: HttpErrorType.Business,
+ code,
+ statusCode: res.statusCode,
+ message: getResponseMessage(responseData),
+ data: responseData?.data,
+ raw: responseData,
+ })
+
+ if (!options.hideErrorToast) {
+ uni.showToast({
+ icon: 'none',
+ title: httpError.message,
+ })
+ }
+ return reject(httpError)
+ }
+ return resolve(responseData.data as T)
+ }
+
+ // 处理其他错误
+ const httpError = createHttpError({
+ type: HttpErrorType.Http,
+ code,
+ statusCode: res.statusCode,
+ message: getResponseMessage(responseData, ShowMessage(res.statusCode)),
+ data: responseData?.data,
+ raw: res,
+ })
+
+ if (!options.hideErrorToast) {
+ uni.showToast({
+ icon: 'none',
+ title: httpError.message,
+ })
+ }
+ reject(httpError)
+ },
+ // 响应失败
+ fail(err) {
+ const httpError = createHttpError({
+ type: HttpErrorType.Network,
+ message: '网络错误,换个网络试试',
+ raw: err,
+ } satisfies HttpError)
+
+ if (!options.hideErrorToast) {
+ uni.showToast({
+ icon: 'none',
+ title: httpError.message,
+ })
+ }
+ reject(httpError)
+ },
+ })
+ })
+}
+
+/**
+ * GET 请求
+ * @param url 后台地址
+ * @param query 请求query参数
+ * @param header 请求头,默认为json格式
+ * @returns
+ */
+export function httpGet(url: string, query?: Record, header?: Record, options?: Partial) {
+ return http({
+ url,
+ query,
+ method: 'GET',
+ header,
+ ...options,
+ })
+}
+
+/**
+ * POST 请求
+ * @param url 后台地址
+ * @param data 请求body参数
+ * @param query 请求query参数,post请求也支持query,很多微信接口都需要
+ * @param header 请求头,默认为json格式
+ * @returns
+ */
+export function httpPost(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) {
+ return http({
+ url,
+ query,
+ data,
+ method: 'POST',
+ header,
+ ...options,
+ })
+}
+/**
+ * PUT 请求
+ */
+export function httpPut(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) {
+ return http({
+ url,
+ data,
+ query,
+ method: 'PUT',
+ header,
+ ...options,
+ })
+}
+
+/**
+ * DELETE 请求(无请求体,仅 query)
+ */
+export function httpDelete(url: string, query?: Record, header?: Record, options?: Partial) {
+ return http({
+ url,
+ query,
+ method: 'DELETE',
+ header,
+ ...options,
+ })
+}
+
+// 支持与 axios 类似的API调用
+http.get = httpGet
+http.post = httpPost
+http.put = httpPut
+http.delete = httpDelete
+
+// 支持与 alovaJS 类似的API调用
+http.Get = httpGet
+http.Post = httpPost
+http.Put = httpPut
+http.Delete = httpDelete
diff --git a/src/http/interceptor.ts b/src/http/interceptor.ts
new file mode 100644
index 0000000..bcd0f87
--- /dev/null
+++ b/src/http/interceptor.ts
@@ -0,0 +1,69 @@
+import type { CustomRequestOptions } from '@/http/types'
+import { useTokenStore } from '@/store'
+import { getEnvBaseUrl } from '@/utils'
+import { stringifyQuery } from './tools/queryString'
+
+// 请求基准地址
+const baseUrl = getEnvBaseUrl()
+
+// 拦截器配置
+const httpInterceptor = {
+ // 拦截前触发
+ invoke(options: CustomRequestOptions) {
+ // 如果您使用了alova,则请把下面的代码放开注释
+ // alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
+ // return options
+
+ // 非 alova 请求,正常执行
+ // 接口请求支持通过 query 参数配置 queryString
+ if (options.query) {
+ const queryStr = stringifyQuery(options.query)
+ if (options.url.includes('?')) {
+ options.url += `&${queryStr}`
+ }
+ else {
+ options.url += `?${queryStr}`
+ }
+ }
+ // 非 http 开头需拼接地址
+ if (!options.url.startsWith('http')) {
+ // #ifdef H5
+ if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
+ // 自动拼接代理前缀
+ options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
+ }
+ else {
+ options.url = baseUrl + options.url
+ }
+ // #endif
+ // 非H5正常拼接
+ // #ifndef H5
+ options.url = baseUrl + options.url
+ // #endif
+ // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
+ }
+ // 1. 请求超时
+ options.timeout = 60000 // 60s
+ // 2. (可选)添加小程序端请求头标识
+ options.header = {
+ ...options.header,
+ }
+ // 3. 添加 token 请求头标识
+ const tokenStore = useTokenStore()
+ const token = tokenStore.updateNowTime().validToken
+
+ if (token) {
+ options.header.Authorization = `Bearer ${token}`
+ }
+ return options
+ },
+}
+
+export const requestInterceptor = {
+ install() {
+ // 拦截 request 请求
+ uni.addInterceptor('request', httpInterceptor)
+ // 拦截 uploadFile 文件上传
+ uni.addInterceptor('uploadFile', httpInterceptor)
+ },
+}
diff --git a/src/http/tools/enum.ts b/src/http/tools/enum.ts
new file mode 100644
index 0000000..2e19f5a
--- /dev/null
+++ b/src/http/tools/enum.ts
@@ -0,0 +1,89 @@
+import type { HttpError, IResponse } from '@/http/types'
+
+export enum ResultEnum {
+ // 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
+ Success0 = 0, // 成功
+ Success200 = 200, // 成功
+ Error = 400, // 错误
+ Unauthorized = 401, // 未授权
+ Forbidden = 403, // 禁止访问(原为forbidden)
+ NotFound = 404, // 未找到(原为notFound)
+ MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
+ RequestTimeout = 408, // 请求超时(原为requestTimeout)
+ InternalServerError = 500, // 服务器错误(原为internalServerError)
+ NotImplemented = 501, // 未实现(原为notImplemented)
+ BadGateway = 502, // 网关错误(原为badGateway)
+ ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
+ GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
+ HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
+}
+export enum ContentTypeEnum {
+ JSON = 'application/json;charset=UTF-8',
+ FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
+ FORM_DATA = 'multipart/form-data;charset=UTF-8',
+}
+
+export enum HttpErrorType {
+ Business = 'business',
+ Auth = 'auth',
+ Http = 'http',
+ Network = 'network',
+}
+
+export function isSuccessResultCode(code: number): boolean {
+ return [ResultEnum.Success0, ResultEnum.Success200].includes(code)
+}
+
+export function getResponseMessage(responseData: Partial> | undefined, fallback = '请求错误'): string {
+ return responseData?.msg || responseData?.message || fallback
+}
+
+export function createHttpError(params: HttpError): HttpError {
+ return params
+}
+/**
+ * 根据状态码,生成对应的错误信息
+ * @param {number|string} status 状态码
+ * @returns {string} 错误信息
+ */
+export function ShowMessage(status: number | string): string {
+ let message: string
+ switch (status) {
+ case 400:
+ message = '请求错误(400)'
+ break
+ case 401:
+ message = '未授权,请重新登录(401)'
+ break
+ case 403:
+ message = '拒绝访问(403)'
+ break
+ case 404:
+ message = '请求出错(404)'
+ break
+ case 408:
+ message = '请求超时(408)'
+ break
+ case 500:
+ message = '服务器错误(500)'
+ break
+ case 501:
+ message = '服务未实现(501)'
+ break
+ case 502:
+ message = '网络错误(502)'
+ break
+ case 503:
+ message = '服务不可用(503)'
+ break
+ case 504:
+ message = '网络超时(504)'
+ break
+ case 505:
+ message = 'HTTP版本不受支持(505)'
+ break
+ default:
+ message = `连接出错(${status})!`
+ }
+ return `${message},请检查网络或联系管理员!`
+}
diff --git a/src/http/tools/queryString.ts b/src/http/tools/queryString.ts
new file mode 100644
index 0000000..edf973e
--- /dev/null
+++ b/src/http/tools/queryString.ts
@@ -0,0 +1,29 @@
+/**
+ * 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
+ * 支持基本类型值和数组,不支持嵌套对象
+ * @param obj 要序列化的对象
+ * @returns 序列化后的查询字符串
+ */
+export function stringifyQuery(obj: Record): string {
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
+ return ''
+
+ return Object.entries(obj)
+ .filter(([_, value]) => value !== undefined && value !== null)
+ .map(([key, value]) => {
+ // 对键进行编码
+ const encodedKey = encodeURIComponent(key)
+
+ // 处理数组类型
+ if (Array.isArray(value)) {
+ return value
+ .filter(item => item !== undefined && item !== null)
+ .map(item => `${encodedKey}=${encodeURIComponent(item)}`)
+ .join('&')
+ }
+
+ // 处理基本类型
+ return `${encodedKey}=${encodeURIComponent(value)}`
+ })
+ .join('&')
+}
diff --git a/src/http/types.ts b/src/http/types.ts
new file mode 100644
index 0000000..690044a
--- /dev/null
+++ b/src/http/types.ts
@@ -0,0 +1,53 @@
+/**
+ * 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
+ */
+export type CustomRequestOptions = UniApp.RequestOptions & {
+ query?: Record
+ /** 出错时是否隐藏错误提示 */
+ hideErrorToast?: boolean
+} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
+
+/** 主要提供给 openapi-ts-request 生成的代码使用 */
+export type CustomRequestOptions_ = Omit
+
+export interface HttpRequestResult {
+ promise: Promise
+ requestTask: UniApp.RequestTask
+}
+
+export interface HttpError {
+ type: 'business' | 'auth' | 'http' | 'network'
+ code?: number
+ statusCode?: number
+ message: string
+ data?: T
+ raw?: unknown
+}
+
+// 通用响应格式(兼容 msg + message 字段)
+export type IResponse = {
+ code: number
+ data: T
+ message: string
+ [key: string]: any // 允许额外属性
+} | {
+ code: number
+ data: T
+ msg: string
+ [key: string]: any // 允许额外属性
+}
+
+// 分页请求参数
+export interface PageParams {
+ page: number
+ pageSize: number
+ [key: string]: any
+}
+
+// 分页响应数据
+export interface PageResult {
+ list: T[]
+ total: number
+ page: number
+ pageSize: number
+}
diff --git a/src/http/vue-query.ts b/src/http/vue-query.ts
new file mode 100644
index 0000000..69ca80d
--- /dev/null
+++ b/src/http/vue-query.ts
@@ -0,0 +1,30 @@
+import type { CustomRequestOptions } from '@/http/types'
+import { http } from './http'
+
+/*
+ * openapi-ts-request 工具的 request 跨客户端适配方法
+ */
+export default function request(
+ url: string,
+ options: Omit & {
+ params?: Record
+ headers?: Record
+ },
+) {
+ const requestOptions = {
+ url,
+ ...options,
+ }
+
+ if (options.params) {
+ requestOptions.query = requestOptions.params
+ delete requestOptions.params
+ }
+
+ if (options.headers) {
+ requestOptions.header = options.headers
+ delete requestOptions.headers
+ }
+
+ return http(requestOptions)
+}
diff --git a/src/layouts/default.vue b/src/layouts/default.vue
new file mode 100644
index 0000000..ba4672f
--- /dev/null
+++ b/src/layouts/default.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..8ec1058
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,19 @@
+import { createSSRApp } from 'vue'
+import App from './App.vue'
+import { requestInterceptor } from './http/interceptor'
+import { routeInterceptor } from './router/interceptor'
+
+import store from './store'
+import '@/style/index.scss'
+import 'virtual:uno.css'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(store)
+ app.use(routeInterceptor)
+ app.use(requestInterceptor)
+
+ return {
+ app,
+ }
+}
diff --git a/src/pages/about/about.vue b/src/pages/about/about.vue
new file mode 100644
index 0000000..76a518d
--- /dev/null
+++ b/src/pages/about/about.vue
@@ -0,0 +1,13 @@
+
+
+
+
+ 关于页面
+
+
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
new file mode 100644
index 0000000..292d70a
--- /dev/null
+++ b/src/pages/index/index.vue
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+ {{ greeting }}
+
+
+ {{ today }}
+
+
+
+
+ 🩺
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.icon }}
+
+
+
+ {{ item.label }}
+
+
+
+ {{ item.value }}
+
+
+ {{ item.sub }}
+
+
+
+
+
+
+
+
+
+
+ {{ plan.icon }}
+ {{ plan.label }}
+
+
+ ✓
+
+
+
+
+
+
+
+
+ 选择记录类型
+
+
+
+
+ {{ item.icon }}
+
+ {{ item.label }}
+
+
+
+
+
+
+ ⚖️ 记录体重(Kg)
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/me/me.vue b/src/pages/me/me.vue
new file mode 100644
index 0000000..760dda8
--- /dev/null
+++ b/src/pages/me/me.vue
@@ -0,0 +1,13 @@
+
+
+ 我的页面
+
+
+
+
diff --git a/src/pages/record/record.vue b/src/pages/record/record.vue
new file mode 100644
index 0000000..9fe5ba2
--- /dev/null
+++ b/src/pages/record/record.vue
@@ -0,0 +1,13 @@
+
+
+ 记录
+
+
+
+
diff --git a/src/router/README.md b/src/router/README.md
new file mode 100644
index 0000000..60e3084
--- /dev/null
+++ b/src/router/README.md
@@ -0,0 +1,55 @@
+# 登录 说明
+
+## 登录 2种策略
+- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+- 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
+
+比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。
+
+### 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
+
+比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
+
+### EXCLUDE_LOGIN_PATH_LIST
+`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。
+
+在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。
+
+在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。
+
+### excludeLoginPath
+definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能)
+
+```ts
+definePage({
+ style: {
+ navigationBarTitleText: '关于',
+ },
+ // 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
+ excludeLoginPath: true,
+ // 角色授权(可选):如果需要根据角色授权,就配置这个
+ roleAuth: {
+ field: 'role',
+ value: 'admin',
+ redirect: '/pages/auth/403',
+ },
+})
+```
+
+## 登录注册页路由
+
+登录页 `login.vue` 对应路由是 `/pages/login/login`.
+注册页 `register.vue` 对应路由是 `/pages/login/register`.
+
+## 登录注册页适用性
+
+登录注册页主要适用于 `h5` 和 `App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。
+
+特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。
+
+通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。
diff --git a/src/router/interceptor.ts b/src/router/interceptor.ts
new file mode 100644
index 0000000..44e3b14
--- /dev/null
+++ b/src/router/interceptor.ts
@@ -0,0 +1,59 @@
+/**
+ * by 菲鸽 on 2025-08-19
+ * 路由拦截,通常也是登录拦截
+ * 黑、白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST
+ */
+import { tabbarStore } from '@/tabbar/store'
+import { getLastPage, parseUrlToObj } from '@/utils/index'
+
+export const FG_LOG_ENABLE = false
+
+export const navigateToInterceptor = {
+ // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
+ // 增加对相对路径的处理,BY 网友 @ideal
+ invoke({ url, query }: { url: string, query?: Record }) {
+ if (url === undefined) {
+ return
+ }
+ let { path, query: _query } = parseUrlToObj(url)
+
+ FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------')
+ FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query)
+ const myQuery = { ..._query, ...query }
+ // /pages/route-interceptor/index?name=feige&age=30
+ FG_LOG_ENABLE && console.log('路由拦截器 2: path->', path, ', _query ->', _query)
+ FG_LOG_ENABLE && console.log('路由拦截器 3: myQuery ->', myQuery)
+
+ // 处理相对路径
+ if (!path.startsWith('/')) {
+ const currentPath = getLastPage()?.route || ''
+ const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
+ const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
+ path = `${baseDir}/${path}`
+ }
+
+ // // 处理路由不存在的情况
+ // if (path !== '/' && !getAllPages().some(page => page.path === path)) {
+ // console.warn('路由不存在:', path)
+ // return false // 明确表示阻止原路由继续执行
+ // }
+
+ // // 插件页面
+ // if (url.startsWith('plugin://')) {
+ // FG_LOG_ENABLE && console.log('路由拦截器 4: plugin:// 路径 ==>', url)
+ // path = url
+ // }
+
+ // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
+ tabbarStore.setAutoCurIdx(path)
+ },
+}
+
+export const routeInterceptor = {
+ install() {
+ uni.addInterceptor('navigateTo', navigateToInterceptor)
+ uni.addInterceptor('reLaunch', navigateToInterceptor)
+ uni.addInterceptor('redirectTo', navigateToInterceptor)
+ uni.addInterceptor('switchTab', navigateToInterceptor)
+ },
+}
diff --git a/src/router/permission.ts b/src/router/permission.ts
new file mode 100644
index 0000000..38fa06f
--- /dev/null
+++ b/src/router/permission.ts
@@ -0,0 +1,11 @@
+import { tabbarStore } from '@/tabbar/store'
+
+export const permission = {
+ install(router) {
+ router.beforeEach((to, from, next) => {
+ const path = to.path
+ tabbarStore.setAutoCurIdx(path)
+ next()
+ })
+ },
+}
diff --git a/src/service/index.ts b/src/service/index.ts
new file mode 100644
index 0000000..3bfbb5b
--- /dev/null
+++ b/src/service/index.ts
@@ -0,0 +1,6 @@
+/* eslint-disable */
+// @ts-ignore
+export * from './types';
+
+export * from './listAll';
+export * from './info';
diff --git a/src/service/info.ts b/src/service/info.ts
new file mode 100644
index 0000000..fe09da5
--- /dev/null
+++ b/src/service/info.ts
@@ -0,0 +1,14 @@
+/* eslint-disable */
+// @ts-ignore
+import request from '@/http/vue-query';
+import { CustomRequestOptions_ } from '@/http/types';
+
+import * as API from './types';
+
+/** 用户信息 GET /user/info */
+export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
+ return request('/user/info', {
+ method: 'GET',
+ ...(options || {}),
+ });
+}
diff --git a/src/service/listAll.ts b/src/service/listAll.ts
new file mode 100644
index 0000000..bc1c683
--- /dev/null
+++ b/src/service/listAll.ts
@@ -0,0 +1,18 @@
+/* eslint-disable */
+// @ts-ignore
+import request from '@/http/vue-query';
+import { CustomRequestOptions_ } from '@/http/types';
+
+import * as API from './types';
+
+/** 用户列表 GET /user/listAll */
+export function listAllUsingGet({
+ options,
+}: {
+ options?: CustomRequestOptions_;
+}) {
+ return request('/user/listAll', {
+ method: 'GET',
+ ...(options || {}),
+ });
+}
diff --git a/src/service/types.ts b/src/service/types.ts
new file mode 100644
index 0000000..4e46b61
--- /dev/null
+++ b/src/service/types.ts
@@ -0,0 +1,29 @@
+/* eslint-disable */
+// @ts-ignore
+
+export type InfoUsingGetResponse = {
+ code: number;
+ msg: string;
+ data: UserItem;
+};
+
+export type InfoUsingGetResponses = {
+ 200: InfoUsingGetResponse;
+};
+
+export type ListAllUsingGetResponse = {
+ code: number;
+ msg: string;
+ data: UserItem[];
+};
+
+export type ListAllUsingGetResponses = {
+ 200: ListAllUsingGetResponse;
+};
+
+export type UserItem = {
+ userId: number;
+ username: string;
+ nickname: string;
+ avatar: string;
+};
diff --git a/src/static/app/icons/1024x1024.png b/src/static/app/icons/1024x1024.png
new file mode 100644
index 0000000000000000000000000000000000000000..08dbd5f4a09da5a8e4f54ebb1fccfbe622ee2008
GIT binary patch
literal 59336
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