Merge pull request #505 from fastapiadmin/dev

Dev
This commit is contained in:
fastapiadmin
2026-08-16 21:17:12 +08:00
committed by GitHub
233 changed files with 44404 additions and 154 deletions
+7 -15
View File
@@ -37,7 +37,6 @@ cython_debug
venv venv
.venv .venv
# ENV
node_modules node_modules
.npm .npm
@@ -47,33 +46,26 @@ node_modules
backend/logs backend/logs
backend/*.db backend/*.db
backend/alembic/versions/* backend/alembic/versions/*
backend/env/.env.dev
backend/env/.env.prod
backend/env/.env.test
!backend/alembic/versions/.gitkeep !backend/alembic/versions/.gitkeep
docker/.env
docker/nginx/app/dist docker/nginx/app/dist
docker/nginx/docs/dist docker/nginx/docs/dist
docker/nginx/web/dist docker/nginx/web/dist
docker/nginx/ssl/* docker/nginx/ssl/*
!docker/nginx/ssl/.gitkeep !docker/nginx/ssl/.gitkeep
# 环境变量文件(真实配置,不提交)
docker/.env
backend/env/.env.dev
backend/env/.env.prod
backend/env/.env.test
frontend/.env.development
frontend/.env.production
frontend/web/.env frontend/web/.env
frontend/web/.env.development frontend/web/.env.development
frontend/web/.env.production frontend/web/.env.production
frontend/node_modules # frontend/docs/*
frontend/dist
frontend/src/types/auto-imports.d.ts
frontend/src/types/components.d.ts
frontend/docs/*
!frontend/docs/.gitkeep !frontend/docs/.gitkeep
frontend/app/* # frontend/app/*
!frontend/app/.gitkeep !frontend/app/.gitkeep
.plan .plan
@@ -169,6 +169,7 @@ async def oauth_login_redirect_controller(
@AuthRouter.get("/oauth/{provider}/callback", summary="第三方OAuth回调", include_in_schema=False) @AuthRouter.get("/oauth/{provider}/callback", summary="第三方OAuth回调", include_in_schema=False)
async def oauth_callback_controller( async def oauth_callback_controller(
request: Request, request: Request,
background_tasks: BackgroundTasks,
redis: Annotated[Redis, Depends(redis_getter)], redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
provider: Annotated[OAuthProvider, Path(description="wechat | qq | github | gitee")], provider: Annotated[OAuthProvider, Path(description="wechat | qq | github | gitee")],
@@ -205,6 +206,7 @@ async def oauth_callback_controller(
provider=provider, provider=provider,
code=code, code=code,
state=state, state=state,
background_tasks=background_tasks,
) )
success_url = oauth_service_frontend_redirect_from_token(fe, token) success_url = oauth_service_frontend_redirect_from_token(fe, token)
return RedirectContentResponse(url=success_url, status_code=302) return RedirectContentResponse(url=success_url, status_code=302)
@@ -224,6 +226,7 @@ async def wx_mini_login_controller(
redis: Annotated[Redis, Depends(redis_getter)], redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
body: WxLoginSchema, body: WxLoginSchema,
background_tasks: BackgroundTasks,
) -> JSONResponse: ) -> JSONResponse:
"""微信小程序登录(code2Session)。 """微信小程序登录(code2Session)。
@@ -252,6 +255,7 @@ async def wx_mini_login_controller(
redis=redis, redis=redis,
user=user, user=user,
login_type="wx_mini", login_type="wx_mini",
background_tasks=background_tasks,
) )
user_info = { user_info = {
@@ -282,6 +286,7 @@ async def wx_mini_phone_login_controller(
redis: Annotated[Redis, Depends(redis_getter)], redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)], db: Annotated[AsyncSession, Depends(db_getter)],
body: WxPhoneLoginSchema, body: WxPhoneLoginSchema,
background_tasks: BackgroundTasks,
) -> JSONResponse: ) -> JSONResponse:
"""微信小程序手机号快速登录。 """微信小程序手机号快速登录。
@@ -335,6 +340,7 @@ async def wx_mini_phone_login_controller(
redis=redis, redis=redis,
user=user, user=user,
login_type="wx_mini_phone", login_type="wx_mini_phone",
background_tasks=background_tasks,
) )
user_info = { user_info = {
@@ -13,7 +13,7 @@ from typing import Any, Literal
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
import httpx import httpx
from fastapi import Request from fastapi import BackgroundTasks, Request
from redis.asyncio.client import Redis from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -347,6 +347,7 @@ async def complete_oauth_login(
provider: OAuthProvider, provider: OAuthProvider,
code: str, code: str,
state: str, state: str,
background_tasks: BackgroundTasks | None = None,
) -> tuple[JWTOutSchema, str]: ) -> tuple[JWTOutSchema, str]:
rc = RedisCURD(redis) rc = RedisCURD(redis)
raw = await rc.get(f"{STATE_PREFIX}{state}") raw = await rc.get(f"{STATE_PREFIX}{state}")
@@ -394,7 +395,7 @@ async def complete_oauth_login(
raise CustomException(msg="用户不存在") raise CustomException(msg="用户不存在")
login_type = f"oauth_{provider}" login_type = f"oauth_{provider}"
token = await LoginService.create_token(request=request, redis=redis, user=user, login_type=login_type) token = await LoginService.create_token(request=request, redis=redis, user=user, login_type=login_type, background_tasks=background_tasks)
return token, frontend return token, frontend
finally: finally:
await rc.delete(f"{STATE_PREFIX}{state}") await rc.delete(f"{STATE_PREFIX}{state}")
@@ -84,6 +84,30 @@ async def _async_fill_login_location(redis, login_log_id: int, ip: str | None) -
logger.warning(f"异步补全登录归属地失败: {e}") logger.warning(f"异步补全登录归属地失败: {e}")
async def _async_fill_session_location(redis, session_id: str, ip: str | None) -> None:
"""后台异步补全会话缓存中的归属地(微信/OAuth 登录不写登录日志,需单独更新 Redis 会话)。"""
if not ip:
return
try:
location = await IpLocalUtil.resolve_location_async(redis, ip)
logger.info(f"异步解析IP归属地结果: ip={ip}, session_id={session_id}, location={location}")
if location == "归属地查询中" or not location:
return
key = f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}"
raw = await RedisCURD(redis).get(key)
if not raw:
return
if isinstance(raw, bytes):
raw = raw.decode("utf-8")
session_dict = json.loads(raw)
session_dict["login_location"] = location
ttl = await RedisCURD(redis).ttl(key)
await RedisCURD(redis).set(key, json.dumps(session_dict, default=str), expire=max(int(ttl), 1))
logger.info(f"会话归属地已更新: session_id={session_id}, location={location}")
except Exception as e:
logger.warning(f"异步补全会话归属地失败: {e}")
class LoginService: class LoginService:
"""登录认证服务""" """登录认证服务"""
@@ -275,7 +299,14 @@ class LoginService:
} }
@classmethod @classmethod
async def create_token(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema: async def create_token(
cls,
request: Request,
redis: Redis,
user: UserModel,
login_type: str,
background_tasks: BackgroundTasks | None = None,
) -> JWTOutSchema:
"""创建访问令牌和刷新令牌""" """创建访问令牌和刷新令牌"""
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
ua_result = ua_parser.parse(request.headers.get("user-agent") or "") ua_result = ua_parser.parse(request.headers.get("user-agent") or "")
@@ -336,6 +367,10 @@ class LoginService:
expire=int(refresh_expires.total_seconds()), expire=int(refresh_expires.total_seconds()),
) )
# 归属地为待解析时后台补全会话中的 login_location(微信/OAuth 登录路径)
if background_tasks and login_location == "归属地查询中":
background_tasks.add_task(_async_fill_session_location, redis, session_id, request_ip)
return JWTOutSchema( return JWTOutSchema(
access_token=access_token, access_token=access_token,
refresh_token=refresh_token, refresh_token=refresh_token,
+1 -1
View File
@@ -144,7 +144,7 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
HTTPX_DEFAULT_TIMEOUT: float = 10.0 # 对外 HTTP 请求默认超时(秒) HTTPX_DEFAULT_TIMEOUT: float = 10.0 # 对外 HTTP 请求默认超时(秒)
IP_LOCATION_ENABLE: bool = True # 是否启用 IP 归属地查询(登录时对外发起 HTTP 请求) IP_LOCATION_ENABLE: bool = True # 是否启用 IP 归属地查询(登录时对外发起 HTTP 请求)
IP_LOCATION_CACHE_TTL: int = 604800 # IP 归属地缓存时间(秒,默认 7 天) IP_LOCATION_CACHE_TTL: int = 2592000 # IP 归属地缓存时间(秒,默认 30 天)
IP_LOCATION_QUERY_TIMEOUT: float = 3.0 # IP 归属地查询单次 HTTP 超时(秒) IP_LOCATION_QUERY_TIMEOUT: float = 3.0 # IP 归属地查询单次 HTTP 超时(秒)
# ================================================= # # ================================================= #
+64 -26
View File
@@ -16,6 +16,12 @@ _IP_CACHE_TTL: int = settings.IP_LOCATION_CACHE_TTL
# 硬超时(秒),避免外网查询阻塞主流程 # 硬超时(秒),避免外网查询阻塞主流程
_IP_QUERY_TIMEOUT: float = settings.IP_LOCATION_QUERY_TIMEOUT _IP_QUERY_TIMEOUT: float = settings.IP_LOCATION_QUERY_TIMEOUT
# 归属地降级返回文案(登录日志/会话共用,调用方按字面量比较)
LOCATION_INTRANET = "内网IP"
LOCATION_DISABLED = "未解析(已关闭归属地查询)"
LOCATION_PENDING = "归属地查询中"
LOCATION_UNKNOWN = "未知"
def get_client_ip(request: Request) -> str: def get_client_ip(request: Request) -> str:
"""从请求中提取客户端真实 IP(优先取反向代理透传的头部,返回空字串表示无法识别)。""" """从请求中提取客户端真实 IP(优先取反向代理透传的头部,返回空字串表示无法识别)。"""
@@ -31,10 +37,12 @@ def get_client_ip(request: Request) -> str:
class IpLocalUtil: class IpLocalUtil:
"""获取 IP 归属地工具类(带 Redis 缓存、硬超时、降级)。""" """获取 IP 归属地工具类(带 Redis 缓存、查询决策、硬超时、降级)。"""
@classmethod @classmethod
def is_valid_ip(cls, ip: str) -> bool: def is_valid_ip(cls, ip: str | None) -> bool:
if not ip:
return False
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)
return True return True
@@ -42,11 +50,18 @@ class IpLocalUtil:
return False return False
@classmethod @classmethod
def is_private_ip(cls, ip: str) -> bool: def is_private_ip(cls, ip: str | None) -> bool:
"""判断是否为非公网地址(内网/回环/链路本地/保留段)。
这类地址无法通过外网归属地 API 解析,用 ``is_global`` 取反判断最稳妥。
"""
if not ip:
return False
try: try:
return ipaddress.ip_address(ip).is_private addr = ipaddress.ip_address(ip)
except ValueError: except ValueError:
return False return False
return not addr.is_global
@classmethod @classmethod
async def _is_location_enabled(cls, redis) -> bool: async def _is_location_enabled(cls, redis) -> bool:
@@ -60,44 +75,67 @@ class IpLocalUtil:
payload = json.loads(raw) payload = json.loads(raw)
cv = payload.get("config_value", "off") cv = payload.get("config_value", "off")
return cv in (True, "true", "1", "yes", "on") return cv in (True, "true", "1", "yes", "on")
except (json.JSONDecodeError, TypeError, Exception): except Exception:
pass pass
return False return False
@classmethod @classmethod
async def resolve_location_for_log(cls, redis, ip: str | None) -> str | None: async def _should_query(cls, redis, ip: str | None) -> bool:
"""登录日志写入入口:仅返回可同步获取的值(内网/缓存/降级), """判断 IP 是否需要发起外网归属地查询(前置条件 + 开关 + IP 类型)。
外网查询由后台任务异步执行(见 ``resolve_location_async``)。 不查询的情形(短路返回 False):
1. IP 为空或非法 —— 无法查询
2. Redis 不可用 —— 读不到开关与缓存,直接降级
3. 开关关闭(ip_location_enable != on)—— 功能总闸
4. 内网/回环/保留地址 —— 外网 API 无法解析
缓存命中与否由调用方处理(命中直接返回,未命中才需要查)。
""" """
if not ip: if not cls.is_valid_ip(ip):
return None return False
if not redis:
return False
if not await cls._is_location_enabled(redis): if not await cls._is_location_enabled(redis):
return "内网IP" if cls.is_private_ip(ip) else "未解析(已关闭归属地查询)" return False
if cls.is_private_ip(ip): if cls.is_private_ip(ip):
return "内网IP" return False
return True
@classmethod
async def resolve_location_for_log(cls, redis, ip: str | None) -> str | None:
"""登录日志写入入口:仅返回可同步获取的值(开关/内网/缓存)。
外网查询由后台任务异步执行(见 ``resolve_location_async``),
此处只做查询决策与缓存读取,不发起任何外网请求。
"""
if not cls.is_valid_ip(ip):
return None
assert ip is not None # is_valid_ip 已确保非空
if not await cls._should_query(redis, ip):
return LOCATION_INTRANET if cls.is_private_ip(ip) else LOCATION_DISABLED
if redis: if redis:
cached = await cls._cache_get(redis, ip) cached = await cls._cache_get(redis, ip)
if cached is not None: if cached is not None:
return cached return cached
return "归属地查询中" return LOCATION_PENDING
@classmethod @classmethod
async def resolve_location_async(cls, redis, ip: str) -> str: async def resolve_location_async(cls, redis, ip: str) -> str:
"""异步查询归属地(含缓存、降级、硬超时)。""" """异步查询归属地(含决策、缓存、降级、硬超时)。仅供后台任务调用。
仅查询成功的结果写入缓存池(IP 池),失败(未知)不写缓存,
下次登录仍会重试,避免缓存池被无效值污染。
"""
if not cls.is_valid_ip(ip): if not cls.is_valid_ip(ip):
return "未知" return LOCATION_UNKNOWN
if not await cls._is_location_enabled(redis): if not await cls._should_query(redis, ip):
return "未解析(已关闭归属地查询)" return LOCATION_INTRANET if cls.is_private_ip(ip) else LOCATION_DISABLED
if cls.is_private_ip(ip):
return "内网IP"
cached = await cls._cache_get(redis, ip) if redis else None
if cached is not None:
return cached
result = await cls._query_with_timeout(ip)
if redis: if redis:
cached = await cls._cache_get(redis, ip)
if cached is not None:
return cached
result = await cls._query_with_timeout(ip)
if redis and result != LOCATION_UNKNOWN:
await cls._cache_set(redis, ip, result) await cls._cache_set(redis, ip, result)
return result return result
@@ -119,7 +157,7 @@ class IpLocalUtil:
return location return location
except Exception as e: except Exception as e:
logger.warning(f"IP 归属地 API 失败: {url} - {e}") logger.warning(f"IP 归属地 API 失败: {url} - {e}")
return "未知" return LOCATION_UNKNOWN
@staticmethod @staticmethod
def _parse_ipapi(data: dict) -> str | None: def _parse_ipapi(data: dict) -> str | None:
@@ -0,0 +1,202 @@
---
name: create-wot-ui-theme
description: '为 wot-ui 生成单文件主题 SCSS,并在用户明确要求接入时追加 App.vue 的 `@use`。当用户要做品牌主题、语义变量落地、单文件主题接入时使用。'
argument-hint: '主题名、主题风格、主色阶、自定义范围、是否需要接入 App.vue'
---
# Create Wot-ui Theme Skill
这个 skill 用于在 `wot-ui` 项目中生成“单文件主题 SCSS”方案。它约束主题文件在 `src/themes/styles` 下完成语义变量定义与挂载,`App.vue` 只负责 `@use` 引入,不扩展成 light/dark 双文件结构,也不改造 `uni_modules/wot-ui/styles/theme/index.scss`。
## 适用场景
- 用户明确要求为 `wot-ui` 生成品牌主题、业务主题或定制语义变量主题。
- 用户希望新增 `src/themes/styles/{主题名}.scss`,并把挂载逻辑也收进主题文件。
- 用户要求 `App.vue` 只保留 `@use './themes/styles/{主题名}.scss' as {主题名};` 这一类引入。
- 用户强调不要生成 dark 主题文件、不要拆成双文件主题结构、不要改内置主题入口。
## 不适用场景
- 用户只是想知道 `wd-config-provider` 或普通 CSS 变量怎么用,这种情况优先使用 `wot-ui-v2` skill。
- 用户要做 light/dark 双主题、动态换肤系统、运行时 token 注入,这不属于本 skill 的目标结构。
- 当前仓库没有实际业务 `src/App.vue` 或主题目录时,不要臆造文件;应基于用户目标项目路径执行,或先向用户确认目标工程位置。
## 核心约束
- 只生成一个主题文件:`src/themes/styles/{主题名}.scss`。
- 主题文件内必须同时包含:
- 一个 `@mixin {主题名}-theme-vars`
- `page`、`.wot-theme-{主题名}`、`.wot-theme-{主题名} .wd-root-portal` 的挂载选择器
- `App.vue` 只负责 `@use` 引入,不重复写挂载选择器。
- 所有语义变量都必须填写固定色值或 `transparent`,不要写成 `var(--wot-xxx)`。
- 不生成 `dark.scss`,不修改 `uni_modules/wot-ui/styles/theme/index.scss`。
- 如果用户没有明确要求接入 `App.vue`,默认只生成主题文件并给出应追加的 `@use` 语句。
## 执行前确认
生成前优先确认这些信息;缺少时先问清楚再动手:
1. 主题名称,例如 `antd`、`ocean`、`forest`
2. 主题风格描述,例如品牌化、偏中性、偏高对比
3. 主色 10 阶是否全部自定义
4. `danger`、`success`、`warning` 是否沿用默认值
5. 文本、边框、填充、反馈态是否需要整体调性调整
6. 是否需要同步接入 `src/App.vue`
## 推荐流程
1. 先查看项目里是否已存在 `src/themes/styles/*.scss` 与 `src/App.vue`,确认接入位置和现有顺序。
2. 若用户未给足主题信息,先收集主题名、风格和 token 调整范围。
3. 创建 `src/themes/styles/{主题名}.scss`,按本 skill 的完整模板输出全部语义变量。
4. 如用户明确要求接入,再在 `src/App.vue` 的 `<style lang="scss">` 中只追加 `@use './themes/styles/{主题名}.scss' as {主题名};`。
5. 检查变量是否完整、值是否全为固定色值、挂载选择器是否在主题文件内。
## 主题文件要求
### 结构要求
- 只保留一个 mixin,命名为 `{主题名}-theme-vars`
- 注释风格尽量贴近 `antd.scss` 这种语义分组写法
- 挂载选择器直接写在同一文件末尾
- mixin 调用形式固定为 `@include {主题名}-theme-vars();`
### App.vue 要求
- 只追加 `@use './themes/styles/{主题名}.scss' as {主题名};`
- 不在 `App.vue` 里重复写 `page, .wot-theme-{主题名}` 这些挂载块
- 如果已有其他主题 `@use`,保持原有顺序和已有块不被破坏,只追加当前主题
## 完整变量模板
```scss
@mixin {主题名}-theme-vars {
/* {主题描述} semantic tokens */
/* Primary */
--wot-primary-1: #F5F8FFFF;
--wot-primary-2: #E5EDFFFF;
--wot-primary-3: #B8CFFFFF;
--wot-primary-4: #7CA4FFFF;
--wot-primary-5: #4480FFFF;
--wot-primary-6: #1C64FDFF;
--wot-primary-7: #164ED1FF;
--wot-primary-8: #1341ADFF;
--wot-primary-9: #0F3285FF;
--wot-primary-10: #0A235CFF;
/* Danger */
--wot-danger-main: #F14646FF;
--wot-danger-hover: #FB7C7CFF;
--wot-danger-clicked: #DC2C2CFF;
--wot-danger-disabled: #FFC9C9FF;
--wot-danger-particular: #FFE3E3FF;
--wot-danger-surface: #FFF5F5FF;
/* Success */
--wot-success-main: #12B886FF;
--wot-success-hover: #59CDAAFF;
--wot-success-clicked: #0F956CFF;
--wot-success-disabled: #B8EADBFF;
--wot-success-particular: #E7F8F3FF;
--wot-success-surface: #F3FBF9FF;
/* Warning */
--wot-warning-main: #F57F00FF;
--wot-warning-hover: #FFA94DFF;
--wot-warning-clicked: #D05706FF;
--wot-warning-disabled: #FFD8A8FF;
--wot-warning-particular: #FFE8CCFF;
--wot-warning-surface: #FFF6EBFF;
/* Text */
--wot-text-main: #1D1F29FF;
--wot-text-secondary: #4E5369FF;
--wot-text-auxiliary: #868A9CFF;
--wot-text-disabled: #C9CBD4FF;
--wot-text-placeholder: #A9ACB8FF;
--wot-text-white: #FFFFFFFF;
/* Icon */
--wot-icon-main: #1D1F29FF;
--wot-icon-secondary: #4E5369FF;
--wot-icon-auxiliary: #868A9CFF;
--wot-icon-disabled: #C9CBD4FF;
--wot-icon-placeholder: #A9ACB8FF;
--wot-icon-white: #FFFFFFFF;
/* Border */
--wot-border-extra-strong: #868A9CFF;
--wot-border-strong: #C9CBD4FF;
--wot-border-main: #E5E6EBFF;
--wot-border-light: #F2F3F5FF;
--wot-border-white: #FFFFFFFF;
--wot-border-zero: transparent;
/* Filled */
--wot-filled-extra-strong: #C9CBD4FF;
--wot-filled-strong: #E5E6EBFF;
--wot-filled-content: #F2F3F5FF;
--wot-filled-bottom: #F7F8FAFF;
--wot-filled-oppo: #FFFFFFFF;
--wot-filled-zero: transparent;
/* Divider */
--wot-divider-main: #00000014;
--wot-divider-light: #0000000A;
--wot-divider-strong: #00000026;
--wot-divider-white: #FFFFFFFF;
/* Feedback */
--wot-feedback-hover: #0000000A;
--wot-feedback-active: #00000014;
--wot-feedback-accent: #1C64FD14;
/* Opacity filled */
--wot-opacfilled-tooltip-toast-cover: #000000BF;
--wot-opacfilled-main-cover: #0000008C;
--wot-opacfilled-light-cover: #0000004D;
/* Picker view mask */
--wot-picker-view-mask-start-color: #FFFFFFD9;
--wot-picker-view-mask-end-color: #FFFFFF33;
/* Classify application */
--wot-classifyapplication-yellow-background: #FFFAF1FF;
--wot-classifyapplication-yellow-border: #FDD78CFF;
--wot-classifyapplication-yellow-content: #FAAD14FF;
--wot-classifyapplication-Cyan-background: #F4FBFDFF;
--wot-classifyapplication-Cyan-border: #BDEAF1FF;
--wot-classifyapplication-Cyan-content: #22B8CFFF;
--wot-classifyapplication-Purple-background: #F9F8FFFF;
--wot-classifyapplication-Purple-border: #D0BFFFFF;
--wot-classifyapplication-Purple-content: #8059F3FF;
--wot-classifyapplication-Grape-background: #FBF6FDFF;
--wot-classifyapplication-Grape-border: #EEBEFAFF;
--wot-classifyapplication-Grape-content: #AE3EC9FF;
--wot-classifyapplication-Pink-background: #FFF0F6FF;
--wot-classifyapplication-Pink-border: #FCC2D7FF;
--wot-classifyapplication-Pink-content: #FF357CFF;
}
page,
.wot-theme-{主题名},
.wot-theme-{主题名} .wd-root-portal {
@include {主题名}-theme-vars();
}
```
## 输出检查清单
- [ ] 主题文件位于 `src/themes/styles/{主题名}.scss`
- [ ] 文件包含一个 `@mixin {主题名}-theme-vars`
- [ ] 文件内完成 `page`、`.wot-theme-{主题名}`、`.wot-theme-{主题名} .wd-root-portal` 挂载
- [ ] 包含主色、功能色、文字、图标、边框、填充、分割线、反馈、透明填充、Picker View 遮罩、分类色全部变量
- [ ] 所有值都是固定色值或 `transparent`
- [ ] `App.vue` 中若接入,只追加 `@use './themes/styles/{主题名}.scss' as {主题名};`
- [ ] mixin 调用形式为 `@include {主题名}-theme-vars();`
## 回答与实现规则
- 用户未明确要求接入时,不要主动改 `App.vue`
- 用户未明确要求时,不要顺手生成 dark 主题、变量映射层或额外目录结构
- 生成代码时优先保持最小改动,不破坏现有主题文件顺序、选择器范围和项目风格
- 如果发现项目里已有不同主题体系,先说明差异,再确认是否仍按本单文件结构落地
@@ -0,0 +1,43 @@
---
name: starter-cleaner
description: 将 wot-starter v2 模板精简为最小可开发状态,移除文档、演示分包、生成文件和 monorepo 配置,并同步清理相关 Vite 与 package.json 配置。用户要求“清理模板”“移除示例”“生成最小模板”“精简 wot-starter v2”时使用。
---
# Starter Cleaner
使用随技能提供的确定性脚本清理 `wot-starter` v2,保留基础页面、Wot UI、`uni-echarts` 和 `echarts` 能力。
## 执行流程
1. 在项目根目录运行 `git status --short`,确认并告知用户未提交改动会被保留,但待清理目录中的改动会随目录一起删除。
2. 如果用户尚未明确要求执行清理,先说明这是破坏性操作并取得确认。
3. 先预览清理计划:
```sh
node .agents/skills/starter-cleaner/scripts/clean.js --dry-run
```
4. 确认目标是 `wot-starter` v2 后执行。脚本会删除旧的 `src/pages.json`,然后受控运行 `pnpm dev:h5`,等新的最小 `src/pages.json` 生成后退出 dev server:
```sh
node .agents/skills/starter-cleaner/scripts/clean.js
```
5. 执行 `pnpm install --lockfile-only` 更新锁文件,再运行 `pnpm type-check` 和 `pnpm lint`。如果依赖不可用,至少检查 `git diff --check` 和清理后的 diff,并将未运行的验证明确告知用户。
## 清理范围
脚本执行以下操作,且支持重复运行:
- 删除 `docs/`、`src/subPages/`、`src/subEcharts/`、`src/subAsyncEcharts/`。
- 删除旧的 `src/pages.json`,再通过 `pnpm dev:h5` 触发 `vite-plugin-uni-pages` 重新生成最小页面配置。
- 删除 `pnpm-workspace.yaml`,将项目从文档 workspace 恢复为单包项目。
- 从 `vite.config.ts` 的 `UniHelperPages` 配置中移除上述三个示例分包入口。
- 从 `package.json` 中移除 `docs:*` 与 `lint:docs` 脚本。
- 保留 `uni-echarts`、`echarts` 依赖和相关 Vite 配置,方便业务继续使用图表。
## 安全约束
- 不直接拼接或扩展删除路径;仅使用脚本内的固定清单。
- 不在非 `wot-starter` v2 项目中运行。仅在用户明确确认目标项目后使用 `--force` 跳过项目身份检查。
- 不用 `--force` 绕过未提交改动提示;该参数只用于项目身份检查。
@@ -0,0 +1,5 @@
# eslint-disable yaml/plain-scalar
interface:
display_name: 'FastapiAdmin Cleaner'
short_description: '将 FastapiAdmin 清理为最小可开发模板'
default_prompt: '使用 $starter-cleaner 将当前 FastapiAdmin 模板精简至最小状态。'
@@ -0,0 +1,229 @@
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
const dryRun = process.argv.includes('--dry-run')
const force = process.argv.includes('--force')
const pathsToRemove = [
'docs',
'src/subPages',
'src/subEcharts',
'src/subAsyncEcharts',
'src/pages.json',
'pnpm-workspace.yaml',
]
const subPackagePaths = [
'src/subPages',
'src/subEcharts',
'src/subAsyncEcharts',
]
function findProjectRoot(directory) {
const packageJsonPath = path.join(directory, 'package.json')
if (fs.existsSync(packageJsonPath)) {
return directory
}
const parentDirectory = path.dirname(directory)
if (parentDirectory === directory) {
throw new Error('找不到项目根目录(未发现 package.json)')
}
return findProjectRoot(parentDirectory)
}
function readPackageJson(packageJsonPath) {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
}
function assertWotStarterV2(packageJson) {
const isWotStarter = packageJson.name === 'wot-starter'
const isV2 = String(packageJson.version ?? '').startsWith('2.')
if (!force && (!isWotStarter || !isV2)) {
throw new Error(
`目标项目不是 wot-starter v2(name=${packageJson.name ?? 'unknown'}, version=${packageJson.version ?? 'unknown'})。确认目标无误后可使用 --force。`,
)
}
}
function removePath(projectRoot, relativePath) {
const targetPath = path.join(projectRoot, relativePath)
if (!fs.existsSync(targetPath)) {
console.log(`跳过(不存在):${relativePath}`)
return false
}
console.log(`${dryRun ? '计划删除' : '删除'}:${relativePath}`)
if (!dryRun) {
fs.rmSync(targetPath, { recursive: true, force: true })
}
return true
}
function updateTextFile(filePath, transform) {
if (!fs.existsSync(filePath)) {
console.log(`跳过(不存在):${path.basename(filePath)}`)
return false
}
const originalContent = fs.readFileSync(filePath, 'utf8')
const nextContent = transform(originalContent)
if (nextContent === originalContent) {
console.log(`跳过(无需修改):${path.basename(filePath)}`)
return false
}
console.log(`${dryRun ? '计划修改' : '修改'}:${path.basename(filePath)}`)
if (!dryRun) {
fs.writeFileSync(filePath, nextContent, 'utf8')
}
return true
}
function cleanViteConfig(projectRoot) {
const viteConfigPath = path.join(projectRoot, 'vite.config.ts')
updateTextFile(viteConfigPath, (content) => {
const lines = content.split('\n').filter((line) => {
return !subPackagePaths.some((subPackagePath) => {
return line.includes(`'${subPackagePath}'`) || line.includes(`"${subPackagePath}"`)
})
})
return lines.join('\n').replace(/subPackages:\s*\[\s*\],/g, 'subPackages: [],')
})
}
function cleanPackageJson(projectRoot, packageJson) {
const scriptNames = Object.keys(packageJson.scripts ?? {})
const scriptsToRemove = scriptNames.filter(name => name.startsWith('docs:') || name === 'lint:docs')
if (scriptsToRemove.length === 0) {
console.log('跳过(无需修改):package.json')
return
}
for (const scriptName of scriptsToRemove) {
delete packageJson.scripts[scriptName]
}
console.log(`${dryRun ? '计划修改' : '修改'}:package.json(移除 ${scriptsToRemove.join(', ')})`)
if (!dryRun) {
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
`${JSON.stringify(packageJson, null, 2)}\n`,
'utf8',
)
}
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function isGeneratedPagesReady(projectRoot) {
const pagesJsonPath = path.join(projectRoot, 'src/pages.json')
if (!fs.existsSync(pagesJsonPath)) {
return false
}
const content = fs.readFileSync(pagesJsonPath, 'utf8')
return content.includes('"pages"') && !subPackagePaths.some(subPackagePath => content.includes(subPackagePath))
}
function stopProcess(childProcess) {
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
return
}
if (process.platform === 'win32') {
childProcess.kill('SIGTERM')
return
}
try {
process.kill(-childProcess.pid, 'SIGTERM')
}
catch {
childProcess.kill('SIGTERM')
}
}
async function regeneratePagesJson(projectRoot) {
console.log('生成:src/pages.json(pnpm dev:h5)')
const childProcess = spawn('pnpm', ['dev:h5'], {
cwd: projectRoot,
detached: process.platform !== 'win32',
env: {
...process.env,
BROWSER: 'none',
FORCE_COLOR: '0',
},
stdio: ['ignore', 'pipe', 'pipe'],
})
let output = ''
childProcess.stdout.on('data', (chunk) => {
output += chunk.toString()
})
childProcess.stderr.on('data', (chunk) => {
output += chunk.toString()
})
const timeoutAt = Date.now() + 30000
try {
while (Date.now() < timeoutAt) {
if (isGeneratedPagesReady(projectRoot)) {
console.log('已生成:src/pages.json')
return
}
if (childProcess.exitCode !== null) {
break
}
await sleep(250)
}
throw new Error(`未能通过 pnpm dev:h5 生成 src/pages.json。\n${output.trim()}`)
}
finally {
stopProcess(childProcess)
}
}
async function main() {
const projectRoot = findProjectRoot(scriptDirectory)
const packageJsonPath = path.join(projectRoot, 'package.json')
const packageJson = readPackageJson(packageJsonPath)
assertWotStarterV2(packageJson)
console.log(`${dryRun ? '预览清理' : '开始清理'}:${projectRoot}`)
for (const relativePath of pathsToRemove) {
removePath(projectRoot, relativePath)
}
cleanViteConfig(projectRoot)
cleanPackageJson(projectRoot, packageJson)
if (!dryRun) {
await regeneratePagesJson(projectRoot)
}
console.log(dryRun ? '预览完成,未修改任何文件。' : '清理完成。请运行 pnpm install --lockfile-only 更新锁文件。')
}
main().catch((error) => {
console.error(error.message)
process.exitCode = 1
})
@@ -0,0 +1,73 @@
---
name: wot-ui-cli
description: '回答、使用、调试 @wot-ui/cli 时使用。关键词:wot、@wot-ui/cli、CLI、MCP、doctor、usage、lint、list、info、doc、demo、token、changelog、extract、wot mcp。适用于命令查询、参数说明、MCP 接入、本地调试、数据提取与 open-wot 仓库维护。'
argument-hint: '命令名、参数、MCP 场景、调试问题或数据提取需求'
---
# Wot UI CLI Skill
这个 skill 用于让 Agent 在处理 `@wot-ui/cli` 本身相关的问题时,优先基于本仓库 README 与实际命令能力回答,而不是把它误当成纯组件库文档。
## 适用场景
- 用户询问 `wot` 命令怎么用。
- 用户需要区分 `list`、`info`、`doc`、`demo`、`token`、`changelog`、`doctor`、`usage`、`lint`、`mcp`、`extract` 的用途。
- 用户要接入 MCP Server,或需要 `wot mcp` 的配置与调试方法。
- 用户要在本仓库中调试 `@wot-ui/cli`、验证构建产物、重新提取数据。
- 用户的问题本质上是 open-wot 仓库维护问题,而不是单纯的 wot-ui 组件使用问题。
## 适用范围
- 关注对象是 `@wot-ui/cli` 这个工具包,以及仓库 `open-wot` 的开发维护流程。
- 重点覆盖命令能力、通用参数、MCP、离线数据来源、提取流程、本地调试和发布包边界。
- 如果任务是生成 `wd-*` 页面代码、解释组件 props 或给出主题定制方案,应优先使用 `wot-ui-v2` skill。
## 推荐流程
1. 先确认用户是在问 CLI 工具本身,还是在借 CLI 查询组件知识。
2. 如果是命令使用问题,优先按命令类别回答:组件知识、项目分析、MCP、数据提取、仓库开发。
3. 如果是仓库维护问题,优先给出本仓库里的实际调试命令,而不是泛泛而谈。
4. 如果涉及组件内容本身,可引导或切换到 `wot-ui-v2` skill。
## 命令分组
### 组件知识查询
- `wot list`
- `wot info <Component>`
- `wot doc <Component>`
- `wot demo <Component> [name]`
- `wot token [Component]`
- `wot changelog [version] [component]`
### 项目分析
- `wot doctor [dir]`
- `wot usage [dir]`
- `wot lint [dir]`
### MCP
- `wot mcp`
### 数据提取与仓库维护
- `pnpm extract:cli --wot-dir ../wot-ui --output data/v2.json`
- `pnpm extract:clone`
- `pnpm exec tsx src/index.ts <command>`
- `pnpm build`
- `node dist/index.mjs <command>`
## 工作规则
- 包名是 `@wot-ui/cli`,实际可执行命令是 `wot`。
- 回答命令问题时,优先用仓库 README 中已承诺的行为和参数,不臆造未声明子命令。
- 回答本地调试问题时,优先给源码入口:`pnpm exec tsx src/index.ts ...`。
- 回答构建产物问题时,再给 `node dist/index.mjs ...`。
- 回答 MCP 问题时,要说明 `wot mcp` 走 stdio,终端无交互输出通常是正常现象。
- 回答提取逻辑问题时,要说明数据主要来自上游 `wot-ui/wot-ui` 的 markdown 与 SCSS 源码。
- 当用户问的是组件知识但入口是 CLI,也要保留“这是通过 CLI 查询组件知识”这一层语义。
## 参考资料
- [Wot UI CLI 概览](./references/overview.md)
@@ -0,0 +1,195 @@
# Wot UI CLI Overview
本文件根据本仓库 README 整理,目标是让 Agent 快速理解 `@wot-ui/cli` 的能力边界、命令分组、MCP 接入方式、开发调试路径与数据来源。
## Package Identity
- 包名:`@wot-ui/cli`
- 仓库:open-wot
- 可执行命令:`wot`
- 核心定位:wot-ui 的 AI 工具链仓库,提供 CLI、MCP Server、离线组件知识库与数据提取脚本。
## Repository Positioning
- 面向 wot-ui v2 的组件知识查询工具。
- 面向本地项目的组件使用分析与 lint 工具。
- 面向 AI 客户端的 MCP stdio 服务。
- 面向仓库维护者的数据提取与同步工作流。
## Core Capabilities
### Component Knowledge
- `list`:列出可用组件。
- `info <Component>`:查看 props、events、slots、CSS 变量。
- `doc <Component>`:输出组件 markdown 文档。
- `demo <Component> [name]`:查看 demo 列表或指定 demo 源码。
- `token [Component]`:查看组件 CSS 变量与默认值。
- `changelog [version] [component]`:查看版本更新记录。
### Project Analysis
- `doctor [dir]`:检查项目依赖、运行环境与基础集成情况。
- `usage [dir]`:统计 `.vue` 文件中的 `wd-*` 使用情况。
- `lint [dir]`:检查未知组件、空按钮等规则。
### MCP Server
- `mcp`:启动 MCP stdio server。
## Typical User Flows
### Query Component Knowledge Through CLI
常用顺序:
1. `wot list`
2. `wot info Button`
3. `wot demo Button basic`
4. `wot doc Button`
5. `wot token Button`
### Analyze A Local Project
常用顺序:
1. `wot doctor ./my-project`
2. `wot usage ./my-project`
3. `wot lint ./my-project`
### Run MCP In A Client
典型配置:
```json
{
"mcpServers": {
"wot-ui": {
"command": "wot",
"args": ["mcp"]
}
}
}
```
当前 README 明确列出的 MCP tools 有:
- `wot_list`
- `wot_info`
- `wot_doc`
- `wot_demo`
- `wot_token`
- `wot_changelog`
- `wot_lint`
## Common Flags
多数查询命令支持:
- `--format text`
- `--format json`
- `--version v2`
## Install And Run
### Global Install
```bash
npm install -g @wot-ui/cli
```
安装后直接用 `wot`。
### Source Mode In This Repo
```bash
pnpm exec tsx src/index.ts list
pnpm exec tsx src/index.ts info Button
pnpm exec tsx src/index.ts mcp
```
适合本地调试源码,不依赖全局安装。
### Built Artifact Mode
```bash
pnpm build
node dist/index.mjs list
```
适合验证构建产物行为。
## MCP Operational Notes
- `wot mcp` 走 stdio。
- 终端里没有交互输出通常是正常现象。
- 若要调试 tool 与 prompt 调用过程,建议配合 MCP Inspector 或编辑器内置 MCP 客户端。
## Data Source And Extraction
当前版本聚焦 `wot-ui v2`。
离线数据主要提取自上游 `wot-ui/wot-ui` 的:
- `docs/component/*.md`
- `docs/guide/changelog.md`
- `src/uni_modules/wot-ui/components/*/index.scss`
重新生成数据有两种方式:
### Use A Local Wot UI Repo
```bash
pnpm extract:cli --wot-dir ../wot-ui --output data/v2.json
```
### Clone Latest Upstream And Extract
```bash
pnpm extract:clone
```
## Repo Layout
- `src`:CLI、MCP 与项目分析源码。
- `data`:离线组件元数据。
- `scripts`:提取脚本。
- `skills`:面向 Agent 的技能说明。
- `test`:根包测试。
## Local Development Commands
### Environment
- Node.js `>= 20`
- pnpm `10.x`
### Install
```bash
pnpm install
```
### Common Validation
```bash
pnpm lint
pnpm test:all
pnpm build:all
pnpm typecheck:all
```
### Package-Level Commands
```bash
pnpm build
pnpm test
pnpm typecheck
```
## Agent Guidance
- 如果用户问的是命令怎么用,按“命令组 + 示例命令 + 输出用途”来回答。
- 如果用户问的是仓库维护或调试,优先给本仓库中的真实命令和目录。
- 如果用户问的是组件本身怎么写页面,不要停留在 CLI 层,应切换到 `wot-ui-v2` skill。
- 不要把 `wot` 命令和 `@wot-ui/ui` 组件库 API 混为一谈。
@@ -0,0 +1,153 @@
---
name: "wot-ui-unocss-preset-guide"
description: "指导安装、配置并使用 @wot-ui/unocss-preset。Invoke when 用户询问该预设的接入、配置、使用示例或常见问题排查。"
---
# Wot UnoCSS Preset 使用指南
## 适用场景
当用户询问以下内容时,优先使用本 Skill:
- 如何安装 `@wot-ui/unocss-preset`
- 如何在 `unocss.config.ts` 配置 `presetWot`
- `prefix`、`preflight`、`baseTokens` 怎么用
- 为什么类名不生效、自动补全不出现、CI 与本地结果不一致
目标是给出可直接复制的最佳实践,帮助用户快速完成接入并稳定运行。
## 最小安装步骤
```bash
pnpm add -D unocss
pnpm add @wot-ui/unocss-preset
```
## 推荐配置(完整示例)
```ts
import { presetWot } from '@wot-ui/unocss-preset'
import { defineConfig } from 'unocss'
export default defineConfig({
presets: [
presetWot({
prefix: 'wot',
preflight: true,
baseTokens: false,
}),
],
})
```
## 配置项说明
- `prefix`:工具类前缀,默认 `wot`。示例:`wot-text-primary`、`wot-m-main`。
- `preflight`:是否注入 wot-ui CSS 变量,默认 `true`。
- `baseTokens`:是否开放基础色板和原始 token 类名,默认 `false`。
## 常用类名示例
- 颜色:`wot-text-primary`、`wot-bg-danger-surface`、`wot-border-border-main`
- 间距:`wot-m-main`、`wot-gap-tight`、`wot-gap-x-loose`
- 内边距:`wot-p-main`、`wot-px-tight`、`wot-pb-loose`
- 圆角:`wot-rounded-md`、`wot-rounded-full`
- 字重:`wot-font-medium`、`wot-font-semibold`
- 排版:`wot-text-body-main`、`wot-text-title-large`
- 透明度:`wot-opacity-disabled`
- 描边:`wot-border-stroke-main`
## 上手示例(可复制)
### 1) 一个“卡片”示例(uni-app / Vue)
```vue
<template>
<view class="wot-bg-filled-oppo wot-rounded-2xl wot-p-super-loose wot-border-border-main wot-border-stroke-main">
<text class="wot-text-title-large wot-text-text-main wot-font-semibold">
Wot UnoCSS Preset
</text>
<view class="wot-mt-tight">
<text class="wot-text-body-main wot-text-text-secondary">
wot-text-body-main + wot-text-text-secondary
</text>
</view>
<view class="wot-mt-loose wot-bg-primary wot-rounded-full wot-px-main wot-py-extra-tight">
<text class="wot-text-label-large wot-text-text-white wot-font-semibold">
wot-bg-primary
</text>
</view>
</view>
</template>
```
### 2) 间距/布局示例(常见组合)
- 外边距:`wot-mt-tight`、`wot-mt-main`、`wot-mt-super-loose`
- 内边距:`wot-p-loose`、`wot-px-main`、`wot-py-extra-tight`
- 横向/纵向组合:`wot-mx-main`、`wot-my-loose`
- gap:`wot-gap-tight`、`wot-gap-x-main`、`wot-gap-y-loose`
### 3) 语义色示例(背景/文字/边框)
- 背景:`wot-bg-primary`、`wot-bg-success-surface`、`wot-bg-warning-surface`、`wot-bg-danger-surface`
- 文字:`wot-text-text-main`、`wot-text-text-secondary`、`wot-text-primary`、`wot-text-success-main`
- 边框:`wot-border-border-main`、`wot-border-success-main`、`wot-border-warning-main`、`wot-border-danger-main`
### 4) 暗黑模式示例
预设会输出暗色变量选择器 `.wot-theme-dark ...`,你只需要在根节点加 class:
```vue
<template>
<view :class="dark ? 'wot-theme-dark' : ''">
<view class="wot-bg-filled-oppo wot-p-main wot-rounded-lg">
<text class="wot-text-text-main">当前主题:{{ dark ? 'Dark' : 'Light' }}</text>
</view>
</view>
</template>
```
### 5) 打开 baseTokens(可选)
当你希望使用基础色板/原始 token 时启用:
```ts
presetWot({
baseTokens: true,
})
```
启用后会额外提供类似 `wot-base-black` 这类 token(用于 `theme.colors` 与颜色规则匹配)。
## 轻量提醒
- 历史项目若仍使用 `w-` 前缀,可通过 `prefix: 'w'` 兼容。
- 若 playground/子包构建异常,优先检查是否:
- 使用包名导入(如 `@wot-ui/unocss-preset`)
- 在依赖主包产物的场景下先执行主包构建
- 若需要同步上游 wot-ui 变量,可使用:
- `pnpm generate:css-vars:clone`
## 常见问题排查
1. 类名不生效
- 确认项目已启用 UnoCSS 且 `presetWot()` 已加入 `presets`。
- 确认类名前缀与配置一致(默认 `wot-`)。
2. VS Code 没有自动补全
- 确认安装 UnoCSS 官方扩展。
- 确认编辑器能定位到项目 `unocss.config.ts`。
- 确认类名写在扩展可扫描的文件类型内。
3. CI 能复现但本地不复现(或反过来)
- 对齐 Node/pnpm 版本与锁文件。
- 对齐执行顺序(例如先构建主包,再构建依赖主包产物的 playground)。
## 响应风格要求
- 输出简洁、可执行,优先给“可复制配置 + 最短排查路径”。
- 当用户目标明确时,先给结论和配置,再补充原因说明。
- 不擅自引入本项目未使用的额外工具链或复杂抽象。
@@ -0,0 +1,46 @@
---
name: wot-ui-v2
description: '回答、生成、重构、排查 wot-ui v2 相关代码时使用。关键词:wot-ui、uni-app、Vue3、wd-、ConfigProvider、useToast、useDialog、Form、Popup、theme、llms-full。适用于组件选型、API 查询、示例页面生成、主题定制、常见坑排查。'
argument-hint: '组件名、页面场景、问题描述或主题定制需求'
---
# Wot UI V2 Skill
这个 skill 用于让 Agent 在处理 wot-ui v2 相关任务时,优先采用组件库既有能力、遵守 uni-app 场景限制,并结合本仓库提供的 `wot` CLI 查询离线知识。
## 适用场景
- 用户询问某个 `wd-*` 组件的基础用法、属性、事件、插槽或样式变量。
- 需要生成或重构 `uni-app + Vue 3 + TypeScript` 的 wot-ui 页面或组件代码。
- 需要在 `ConfigProvider`、主题变量、暗黑模式、国际化、反馈类 hooks、表单等场景下给出正确做法。
- 需要排查文档中常见的 `Toast`、`Dialog`、`Popup`、`Tabs`、`Slider`、样式覆盖问题。
- 用户只是泛化地提到主题定制,但还没有明确要求按“单文件主题 SCSS + App.vue 只 `@use`”的结构生成时。
## 推荐流程
1. 先用本仓库 CLI 或 MCP 工具查组件知识。
2. 再根据项目实际安装方式决定导入路径与集成方式。
3. 优先复用现成的 `wd-*` 组件、hooks、主题变量与组合模式,不要退化成原生标签堆砌。
4. 如果问题涉及约束或坑位,再查阅 [参考知识](./references/overview.md)。
5. 如果用户明确要求生成 `src/themes/styles/{主题名}.scss` 单文件主题,并把挂载逻辑收进主题文件,优先切换到 `create-wot-ui-theme` skill。
## 查询顺序
1. `wot list` 找组件名。
2. `wot info <Component>` 看 props、events、slots、CSS 变量。
3. `wot demo <Component>` 看 demo 名称或具体 demo 代码。
4. `wot doc <Component>` 看完整 markdown 文档。
5. `wot token <Component>` 看主题变量。
## 工作规则
- 默认把 wot-ui 视为 `uni-app + Vue 3 + TypeScript` 组件库。
- 写页面时优先输出 `script setup` 风格。
- 反馈类能力如 `useToast`、`useDialog`、`useNotify`、`useImagePreview`、`useVideoPreview`,除了 hook 调用外,通常还需要页面内显式声明对应组件实例。
- 文档里经常出现 `@/uni_modules/wot-ui` 导入路径;如果用户项目采用 npm 安装,应切换成 `@wot-ui/ui`。
- 主题定制优先走 `ConfigProvider` 和 CSS 变量,不优先建议深度覆盖内部类名。
- 生成代码时尽量沿用组件库文档里的命名和交互模式,例如 `v-model:visible`、`before-confirm`、`confirm`、`change`、`custom-class`、`custom-style`。
## 参考资料
- [Wot UI V2 概览](./references/overview.md)
@@ -0,0 +1,85 @@
# Wot UI V2 Overview
本文件根据 wot-ui v2 的 `llms-full.txt` 与本仓库现有 CLI 工作流提炼,目标是帮助 Agent 快速掌握适合生成代码与回答问题的高价值知识,而不是逐字复制官方文档。
## Product Positioning
- Wot UI v2 是面向 `uni-app` 的 `Vue 3 + TypeScript` 组件库。
- 覆盖微信小程序、支付宝小程序、钉钉小程序、H5、APP 等平台。
- 组件命名统一为 `wd-*`。
- 组件库强调 AI 友好、主题定制、暗黑模式、国际化与跨端一致性。
## Installation And Integration
- npm 安装:`pnpm add @wot-ui/ui`
- 使用前需要安装 `sass`。
- `uni_modules` 安装模式天然支持 easycom 自动引入。
- npm 安装模式通常需要配置 vite resolver 或 easycom。
- CLI 项目在 npm 模式下可在 `tsconfig.json` 中加入 `@wot-ui/ui/global` 以增强全局组件类型提示。
## Import Rules
- npm 安装项目:组合式函数、类型和工具优先从 `@wot-ui/ui` 导入。
- `uni_modules` 安装项目:文档中的 `@/uni_modules/wot-ui` 路径通常可直接使用。
- 官方文档示例很多基于 `uni_modules` 路径,回答时要按用户项目实际安装方式转换。
## High Value Conventions
- 反馈类组件不能依赖全局挂载。页面内通常需要显式写出 `wd-toast`、`wd-dialog`、`wd-notify`、`wd-image-preview`、`wd-video-preview` 等实例。
- `useToast`、`useDialog`、`useNotify`、`useQueue` 等 hooks 基于 `provide/inject`,应在 `setup` 中调用。
- 页面内如果存在多个 `wd-dialog` 或 `wd-toast`,需要通过 `selector` 区分,否则可能出现实例冲突或重复弹出。
- 自定义组件中如果要覆盖 wot-ui 内部样式,小程序环境通常需要把组件配置为 `styleIsolation: 'shared'`。
- 在 `Popup`、`ActionSheet`、`DropDownItem` 等延迟渲染弹层里使用 `Slider`、`Tabs` 等依赖尺寸计算的组件时,打开后应调用实例方法重新初始化,例如 `initSlider()` 或 `updateLineStyle()`。
## Theme And Styling
- 主题定制优先走 CSS 变量。
- Design Token 分三层:基础变量、语义变量、组件变量。
- 局部或全局主题可通过 `wd-config-provider` 的 `theme` 和 `theme-vars` 控制。
- 深色模式通过 `wd-config-provider theme="dark"` 开启。
- 更推荐覆盖语义变量或组件变量,不推荐优先依赖深层 class 选择器覆盖。
## Common UI Patterns
- 表单场景优先组合 `wd-form`、`wd-form-item`、`wd-input`、`wd-textarea`、`wd-picker`、`wd-calendar`、`wd-select-picker`。
- 弹层类场景优先使用 `wd-popup`、`wd-dialog`、`wd-action-sheet`、`wd-tooltip`、`wd-popover`。
- 反馈类场景优先使用 `useToast`、`useDialog`、`useNotify`,不要直接手写临时弹层。
- 列表和展示类场景优先考虑 `wd-cell`、`wd-card`、`wd-tag`、`wd-badge`、`wd-empty`、`wd-loadmore`、`wd-skeleton`。
- 导航与布局类场景优先考虑 `wd-navbar`、`wd-tabs`、`wd-tabbar`、`wd-sidebar`、`wd-row`、`wd-col`、`wd-gap`。
## Interaction Patterns
- 大量组件采用 `v-model` 或 `v-model:visible` 控制状态。
- 表单和选择类组件普遍提供 `confirm`、`change`、`close` 等事件。
- 反馈组件常通过 hook 返回方法对象,例如 `toast.success()`、`dialog.confirm()`。
- `Popover`、`Tooltip`、`SwipeAction` 等场景常与 `useQueue().closeOutside()` 配合,实现点击外部关闭。
## Component Selection Hints
- 需要主操作按钮时用 `wd-button`,不要先写原生 `button`。
- 需要列表入口、设置页、表单容器时优先用 `wd-cell` 和 `wd-cell-group`。
- 需要轻量提示时优先用 `useToast`;需要确认交互时优先用 `useDialog`。
- 需要单选或多选弹层时优先用 `wd-select-picker`、`wd-picker`、`wd-cascader`。
- 需要统一主题或暗黑模式时优先用 `wd-config-provider`。
## Common Pitfalls
- `Toast`、`Dialog` 等函数式调用没有效果,先检查页面里是否声明了对应组件实例。
- 同一个页面里多个无 `selector` 的反馈组件可能相互干扰。
- npm 模式使用国际化时,开发态可能需要在 Vite 的 `optimizeDeps.exclude` 中排除 `@wot-ui/ui`。
- 文档里的导入路径和项目实际安装方式不一致时,回答要主动修正。
- 在弹层中直接渲染依赖尺寸测量的组件时,初始化时机往往比 API 本身更关键。
## AI Response Heuristics
- 回答基础用法时,优先给最小可运行模板,再补充常用 props。
- 生成页面时,优先给完整的 `template + script setup + style` 结构。
- 如果项目已使用 wot-ui,不要建议换用其他 UI 库。
- 如果 wot-ui 已有现成组件,就不要用原生结构重复造轮子。
- 如果用户只问某个组件,优先给该组件最常见 3 到 5 个用法,不要把整份文档全部展开。
## Repo-Specific Workflow
- 在本仓库中,优先使用 `wot list`、`wot info`、`wot doc`、`wot demo`、`wot token` 获取组件知识。
- 当仓库数据与线上文档不一致时,以用户目标为准,并明确指出仓库离线数据可能需要重新提取。
- 若要补数据或修提取逻辑,关注 `scripts/extract.ts`、`data/v2.json`、`src/data/*` 与相应命令实现。
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+25
View File
@@ -0,0 +1,25 @@
# 开发环境变量
# 在开发模式下使用 (npm run dev)
# API 基础 URL - 开发环境
# VITE_API_BASE_URL=http://localhost:8001
VITE_API_BASE_URL=https://service.fastapiadmin.com
# 环境名称
VITE_ENV_NAME=development
# 项目名称
VITE_APP_TITLE=fastapiadmin
# 代理前缀
VITE_APP_BASE_API=/api/v1
# 应用端口(vite dev server 启动端口,对应 vite.config.ts 的 server.port,缺省 vite 默认 5173)
VITE_APP_PORT=5180
# 超时时间
VITE_TIMEOUT=10000
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
# VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
VITE_APP_WS_ENDPOINT = wss://service.fastapiadmin.com
+23
View File
@@ -0,0 +1,23 @@
# 生产环境变量
# 在生产构建时使用 (npm run build)
# API 基础 URL - 生产环境
VITE_API_BASE_URL=https://service.fastapiadmin.com
# 环境名称
VITE_ENV_NAME=production
# 项目名称
VITE_APP_TITLE=fastapiadmin
# 代理前缀
VITE_APP_BASE_API=/api/v1
# 应用端口
VITE_APP_PORT=5180
# 超时时间
VITE_TIMEOUT=10000
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
+70
View File
@@ -0,0 +1,70 @@
{
"disableEmoji": false,
"list": ["test", "feat", "fix", "chore", "docs", "refactor", "style", "ci", "perf", "release", "revert", "build"],
"maxMessageLength": 64,
"minMessageLength": 3,
"questions": ["type", "scope", "subject", "body", "breaking", "issues", "lerna"],
"scopes": [],
"types": {
"chore": {
"description": "Chore | 构建/工程依赖/工具",
"emoji": "🚀",
"value": "chore"
},
"ci": {
"description": "Continuous Integration | CI 配置",
"emoji": "👷",
"value": "ci"
},
"docs": {
"description": "Documentation | 文档",
"emoji": "✏️ ",
"value": "docs"
},
"feat": {
"description": "Features | 新功能",
"emoji": "✨",
"value": "feat"
},
"fix": {
"description": "Bug Fixes | Bug 修复",
"emoji": "🐛",
"value": "fix"
},
"perf": {
"description": "Performance Improvements | 性能优化",
"emoji": "⚡",
"value": "perf"
},
"refactor": {
"description": "Code Refactoring | 代码重构",
"emoji": "♻️ ",
"value": "refactor"
},
"release": {
"description": "Create a release commit | 发版提交",
"emoji": "🏹",
"value": "release"
},
"style": {
"description": "Styles | 风格",
"emoji": "💄",
"value": "style"
},
"revert": {
"description": "Revert | 回退",
"emoji": "⏪",
"value": "revert"
},
"build": {
"description": "Build System | 打包构建",
"emoji": "📦",
"value": "build"
},
"test": {
"description": "Tests | 测试",
"emoji": "✅",
"value": "test"
}
}
}
+1
View File
@@ -0,0 +1 @@
custom: ['https://github.com/Moonofweisheng/sponsors']
+87
View File
@@ -0,0 +1,87 @@
name: 向 FastapiAdmin 反馈 Bug
description: 创建一个 Issue 描述你遇到的问题。
title: '[Bug 上报] 请在此填写标题'
labels: ['🐞bug: need confirm']
body:
- type: markdown
attributes:
value: |
在向我们提交 Bug 报告前,请优先使用以下方式尝试解决问题:
- 在组件文档 [wot-starter](https://starter.wot-ui.cn/) 确认使用方法是否正确
- 尝试在 [Issue](https://github.com/wot-ui/wot-starter/issues) 列表中搜索相同问题
- type: input
id: version
attributes:
label: FastapiAdmin 版本号
description: 你正在使用的模板版本号(请填写 package.json 里版本)
placeholder: 例如:0.1.1
validations:
required: true
- type: dropdown
id: platform
attributes:
label: 平台
multiple: true
description: 选择对应的平台
options:
- h5
- 微信小程序
- 支付宝小程序
- APP
- 钉钉小程序
- 其他小程序
validations:
required: true
- type: input
id: reproduce
attributes:
label: 复现Demo地址(如不提供,将被直接关闭)
description: |
我们需要你提供一个最小重现demo,以便于我们帮你排查问题。你可以通过 fork 本项目,快速创建一个 wot-ui 项目,并添加相关复现逻辑来提供。不要随便填写一个东西,这会导致你的 issue 被直接关闭。即使在你看来问题很容易复现,也请认真对待,因为一个完整复现demo可以大大提高我们排查问题的效率。
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: 重现步骤
description: |
请提供一个最简洁清晰的重现步骤,方便我们快速重现问题。
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望的结果是什么?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: 实际的结果是什么?
validations:
required: true
- type: textarea
id: uni-app
attributes:
label: 环境信息
description: |
在这里填写你的环境信息
- 发行平台: [如 微信小程序、H5平台、App等]
- 操作系统 [如 iOS 12.1.2、Android 7.0]
- HBuilderX版本 [如使用HBuilderX,则需提供 HBuilderX 版本号]
- uni-app版本 [如使用Vue-cli创建/运行项目,则提供`npm run info`的运行结果]
- 设备信息 [如 iPhone8 Plus]
- type: textarea
id: extra
attributes:
label: 其他补充信息
description: |
根据你的分析,出现这个问题的原因可能在哪里,或者你认为可能产生关联的信息:比如 Vue 版本、vite 版本、Node 版本、采用哪种自动引入方案等,或者进行了哪些配置,使用了哪些插件等信息。
@@ -0,0 +1,33 @@
name: 向 FastapiAdmin 提出新功能需求
description: 创建一个 Issue 描述一下你的功能需求。
title: '[新功能需求] 请在此填写标题'
labels: ['feature: need confirm']
body:
- type: markdown
attributes:
value: |
在提交功能需求前,请注意:
- 确认这是一个通用功能,并且无法通过现有的 API 或 Slot 实现。
- 尝试在 [Issue](https://github.com/wot-ui/wot-starter/issues)列表中搜索,并且没有发现同样的需求。
- 请确保描述清楚你的需求,以便其他开发者更好地理解你的需求。
- type: textarea
id: description
attributes:
label: 这个功能解决了什么问题?
description: 请尽可能详细地说明这个功能的使用场景。
validations:
required: true
- type: textarea
id: api
attributes:
label: 你期望的 API 是什么样子的?
description: 描述一下这个新功能的 API,并提供一些代码示例。
placeholder: |
```xml
<wd-interesting some-prop="xxx" />
```
validations:
required: true
+43
View File
@@ -0,0 +1,43 @@
<!-- (将"[ ]"更新为"[x]"以勾选一个框) -->
### 🤔 这个 PR 的性质是?(至少选择一个)
- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 站点、文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] TypeScript 定义更新
- [ ] CI/CD 改进
- [ ] 包体积优化
- [ ] 性能优化
- [ ] 功能增强
- [ ] 国际化改进
- [ ] 代码重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他改动(是关于什么的改动?)
### 🔗 相关 Issue
<!--
1. 描述相关需求的来源,如相关的 issue 讨论链接。
-->
### 💡 需求背景和解决方案
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
### ☑️ 请求合并前的自查清单
⚠️ 请自检并全部**勾选全部选项**。⚠️
- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
+119
View File
@@ -0,0 +1,119 @@
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
build-and-test:
runs-on: windows-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.9.0
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies (workspace recursive)
run: pnpm -r install --frozen-lockfile
- name: Test WeChat Mini Program dev mode
shell: bash
run: |
echo "Testing WeChat Mini Program dev mode startup..."
timeout 30s pnpm dev:mp-weixin || true
echo "✅ WeChat Mini Program dev mode test completed"
- name: Test H5 dev mode
shell: bash
run: |
echo "Testing H5 dev mode startup..."
timeout 30s pnpm dev:h5 || true
echo "✅ H5 dev mode test completed"
- name: Test iOS dev mode
shell: bash
run: |
echo "Testing iOS dev mode startup..."
timeout 30s pnpm dev:app-ios || true
echo "✅ iOS dev mode test completed"
- name: Build WeChat Mini Program
run: pnpm build:mp-weixin
- name: Build H5
run: pnpm build:h5
- name: Build iOS
run: pnpm build:app-ios
- name: Upload WeChat Mini Program artifacts
uses: actions/upload-artifact@v4
with:
name: wechat-miniprogram-build
path: dist/build/mp-weixin/
retention-days: 7
- name: Upload H5 artifacts
uses: actions/upload-artifact@v4
with:
name: h5-build
path: dist/build/h5/
retention-days: 7
- name: Upload iOS artifacts
uses: actions/upload-artifact@v4
with:
name: ios-build
path: dist/build/app-ios/
retention-days: 7
- name: Build summary
run: |
echo "## Build and Test Summary" >> $GITHUB_STEP_SUMMARY
echo "### Development Mode Tests" >> $GITHUB_STEP_SUMMARY
echo "✅ WeChat Mini Program dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "✅ H5 dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "✅ iOS dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "### Build Results" >> $GITHUB_STEP_SUMMARY
echo "✅ WeChat Mini Program build completed" >> $GITHUB_STEP_SUMMARY
echo "✅ H5 build completed" >> $GITHUB_STEP_SUMMARY
echo "✅ iOS build completed" >> $GITHUB_STEP_SUMMARY
echo "📦 All artifacts uploaded successfully" >> $GITHUB_STEP_SUMMARY
- name: Build Docs (VitePress)
run: pnpm -C docs run docs:build
- name: Upload Docs artifacts
uses: actions/upload-artifact@v4
with:
name: docs-build
path: docs/.vitepress/dist/
retention-days: 7
+54
View File
@@ -0,0 +1,54 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.9.0
run_install: false
- name: Create Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate GitHub Changelog
run: pnpx changelogithub
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release summary
run: |
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
echo "### Version: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "### Release Status" >> $GITHUB_STEP_SUMMARY
echo "✅ Release created successfully" >> $GITHUB_STEP_SUMMARY
+21
View File
@@ -0,0 +1,21 @@
# 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?
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged --allow-empty $1
+1
View File
@@ -0,0 +1 @@
22
+64
View File
@@ -0,0 +1,64 @@
{
"header": "# 更新日志 \n\n",
"types": [{
"type": "feat",
"section": "✨ Features | 新功能",
"hidden": false
},
{
"type": "fix",
"section": "🐛 Bug Fixes | Bug 修复",
"hidden": false
},
{
"type": "init",
"section": "🎉 Init | 初始化",
"hidden": true
},
{
"type": "docs",
"section": "✏️ Documentation | 文档",
"hidden": false
},
{
"type": "style",
"section": "💄 Styles | 风格",
"hidden": true
},
{
"type": "refactor",
"section": "♻️ Code Refactoring | 代码重构",
"hidden": true
},
{
"type": "perf",
"section": "⚡ Performance Improvements | 性能优化",
"hidden": false
},
{
"type": "test",
"section": "✅ Tests | 测试",
"hidden": true
},
{
"type": "revert",
"section": "⏪ Revert | 回退",
"hidden": true
},
{
"type": "build",
"section": "📦‍ Build System | 打包构建",
"hidden": true
},
{
"type": "chore",
"section": "🚀 Chore | 构建/工程依赖/工具",
"hidden": true
},
{
"type": "ci",
"section": "👷 Continuous Integration | CI 配置",
"hidden": true
}
]
}
+158
View File
@@ -0,0 +1,158 @@
# 更新日志
## [2.0.0](https://github.com/wot-ui/wot-starter/compare/v1.5.0...v2.0.0) (2026-04-21)
### ✨ Features | 新功能
* ✨ 集成 @wot-ui/vitepress-theme ([341f935](https://github.com/wot-ui/wot-starter/commit/341f935fef0da676c7f002506b51662baff09025))
* ✨ 支持 wot-ui v2 ([874455f](https://github.com/wot-ui/wot-starter/commit/874455f80abdd97d8ef11b4624d169ac3020a100))
### ✏️ Documentation | 文档
* ✏️ 更新全局反馈组件文档,增加使用前提和示例 ([8866c45](https://github.com/wot-ui/wot-starter/commit/8866c459a0b33d933a1cbfecaacb4d3673a85351))
### 🐛 Bug Fixes | Bug 修复
* 🐛 更新 banner URLs 为 wot-starter-v2-banner.json ([aa773d4](https://github.com/wot-ui/wot-starter/commit/aa773d409db38c041ba0e32b68602dc55f3fd2ec))
## [1.5.0](https://github.com/wot-ui/wot-starter/compare/v1.4.0...v1.5.0) (2026-04-07)
### ✨ Features | 新功能
* ✨ add wot-ui skill ([2b9b664](https://github.com/wot-ui/wot-starter/commit/2b9b664a087ca25fba26bcde22d1337a8d00309d))
### ✏️ Documentation | 文档
* ✏️ add wot-ui skill docs ([1c15e03](https://github.com/wot-ui/wot-starter/commit/1c15e03c47a1e6cdc3eb0e53cd65b41ef2dadf01))
## [1.4.0](https://github.com/wot-ui/wot-starter/compare/v1.3.2...v1.4.0) (2026-01-25)
### ✏️ Documentation | 文档
* ✏️ remove gitee-vote-2025 ([ed32556](https://github.com/wot-ui/wot-starter/commit/ed32556db46d7922cde1a60d1efc32bfb1c87d63))
### 🐛 Bug Fixes | Bug 修复
* 🐛 仅在微信小程序端开启 optimization 修复运行到支付宝小程序报错的问题 ([420aff4](https://github.com/wot-ui/wot-starter/commit/420aff484878ff88934b913d9aa84916a36c2de8))
### ✨ Features | 新功能
* ✨ 添加基于本项目实际使用场景的 Agent Skills ([f2a58f7](https://github.com/wot-ui/wot-starter/commit/f2a58f748c8758b4a083fa7181862dc1ad97e303))
* ✨ 新增清理演示页面提供精简模板的 skill starter-cleaner ([8b4c4c4](https://github.com/wot-ui/wot-starter/commit/8b4c4c4d0c9abdee1616484a435444c9ae4ce000))
### [1.3.2](https://github.com/wot-ui/wot-starter/compare/v1.3.1...v1.3.2) (2026-01-07)
### ✨ Features | 新功能
* ✨ 升级 @uni-ku/bundle-optimizer 至 2.0 并处理相关迁移配置 ([e0ab94c](https://github.com/wot-ui/wot-starter/commit/e0ab94cfd7e9971b52c929de5d89e7c5ab11eb3a))
### [1.3.1](https://github.com/wot-ui/wot-starter/compare/v1.3.0...v1.3.1) (2026-01-05)
### ✨ Features | 新功能
* ✨ 更新 @wot-ui/router 以修复 route 类型问题和 afterEach多次触发的问题 ([fd4f585](https://github.com/wot-ui/wot-starter/commit/fd4f58524bc580a3f3cc249a9447d7c3d0c556d5))
## [1.3.0](https://github.com/wot-ui/wot-starter/compare/v1.2.2...v1.3.0) (2026-01-04)
### ✨ Features | 新功能
* ✨ 更新 wot-ui 到 v1.14.0 版本 ([4fd2532](https://github.com/wot-ui/wot-starter/commit/4fd25328ba5c3d3e7ea202919071cd8fb98ffd74))
### [1.2.2](https://github.com/wot-ui/wot-starter/compare/v1.2.1...v1.2.2) (2025-12-29)
### ✨ Features | 新功能
* ✨ 替换 uni-mini-router 为 @wot-ui/router ([171f054](https://github.com/wot-ui/wot-starter/commit/171f054ac7bb0976ee26edcbf4028c80cc4387a2))
### ✏️ Documentation | 文档
* ✏️ 调整路由文档和演示demo ([73a9cf3](https://github.com/wot-ui/wot-starter/commit/73a9cf36b5b770a556e2ea74be9c5c21602ff661))
* ✏️ 更新 readme ([6bf2618](https://github.com/wot-ui/wot-starter/commit/6bf261833a6a8485c749c0123e3e8ba7b306156e))
* ✏️ 更新分包调整后demo的地址 ([5e358ec](https://github.com/wot-ui/wot-starter/commit/5e358ec27997c5437043ec3137cfbd4513154fd4))
* ✏️ 更新文档首页介绍内容 ([3a1a0a4](https://github.com/wot-ui/wot-starter/commit/3a1a0a409effcc730064d1acc1edc64fe58fd7d1))
* ✏️ 添加 PageSpy 远程的教程 ([f29a53c](https://github.com/wot-ui/wot-starter/commit/f29a53cd01cdc80c45e858e409f1564cf7716f73))
* ✏️ add about me ([046897a](https://github.com/wot-ui/wot-starter/commit/046897a862e402fd8ebbd993cac525de5e830f25))
* ✏️ add gitee vote 2025 ([14af7a0](https://github.com/wot-ui/wot-starter/commit/14af7a04cf1e26772782f0cad0d426e220dba40a))
### [1.2.1](https://github.com/wot-ui/wot-starter/compare/v1.2.0...v1.2.1) (2025-12-04)
### 🐛 Bug Fixes | Bug 修复
* **manualTheme:** 修复跟随系统自动切换主题失效的问题 ([380c702](https://github.com/wot-ui/wot-starter/commit/380c7026eeb37a12e9a2866b18bd70880efdfecc))
### ✨ Features | 新功能
* **index:** 首页设置中新增"跟随系统"按钮 ([d031788](https://github.com/wot-ui/wot-starter/commit/d031788d4c9b31a7d030f17856f69f2d177eb1b8))
* **logo:** 更新logo ([550caa2](https://github.com/wot-ui/wot-starter/commit/550caa243e423969745d62c3872dfae14976409a))
### ✏️ Documentation | 文档
* ✏️ 更新 logo ([0e57c45](https://github.com/wot-ui/wot-starter/commit/0e57c45b94284320d5fdc3be6c082faf12ddcbad))
* ✏️ 首页添加 uni-ku 插件入口 ([a5b05c9](https://github.com/wot-ui/wot-starter/commit/a5b05c9091f5396c27c556ed23eed87b7f06fef0))
* ✏️ 文档增加显示版本号 ([10b7078](https://github.com/wot-ui/wot-starter/commit/10b707810529af53bd2a85fca6624807b5b2d9ec))
* ✏️ 移动非主包必需示例页面到分包中 ([#43](https://github.com/wot-ui/wot-starter/issues/43)) ([3d7a076](https://github.com/wot-ui/wot-starter/commit/3d7a07619cf4b84c26a91b5028b2635bcc6d44ff)), closes [#35](https://github.com/wot-ui/wot-starter/issues/35)
* ✏️ update logo ([6b9e4f9](https://github.com/wot-ui/wot-starter/commit/6b9e4f9e66d7be10b1e678b46472c9e44a270fc3))
## [1.2.0](https://github.com/wot-ui/wot-starter/compare/v1.1.0...v1.2.0) (2025-11-26)
### ✨ Features | 新功能
* ✨ 合并模板与文档项目开发便利性优化 ([#41](https://github.com/wot-ui/wot-starter/issues/41)) ([646d215](https://github.com/wot-ui/wot-starter/commit/646d2158c96dcf83518ed22bc27cc8e20f2ed0d2)), closes [#35](https://github.com/wot-ui/wot-starter/issues/35)
* ✨ 支持 esm 并更新 unocss 和 [@uni-helper](https://github.com/uni-helper) 插件 ([#39](https://github.com/wot-ui/wot-starter/issues/39)) ([f433b49](https://github.com/wot-ui/wot-starter/commit/f433b49023c572488254b18584a4dbca0ba66336))
### ✏️ Documentation | 文档
* ✏️ 添加 vite base ([b6f5cf8](https://github.com/wot-ui/wot-starter/commit/b6f5cf83084d0cc04bc94c9714722a7a4c9e6327))
* ✏️ 增加更新日志入口 ([7037d8f](https://github.com/wot-ui/wot-starter/commit/7037d8f10a9aa0274d5cc5d7afc8978342f96f1a))
## [1.1.0](https://github.com/wot-ui/wot-starter/compare/v1.0.0...v1.1.0) (2025-11-12)
### ✨ Features | 新功能
* ✨ 添加 uni_modules 插件引入示例 ([8493761](https://github.com/wot-ui/wot-starter/commit/8493761ad6ea4e6478d3b7764b43b813e5178e86))
* ✨ 支持 harmony next 自定义 tabbar ([f71e8ba](https://github.com/wot-ui/wot-starter/commit/f71e8ba62504a4c0b79d02e61979b52e1f538e59))
## 1.0.0 (2025-10-28)
### 🐛 Bug Fixes | Bug 修复
* 🐛 修复分包路由未注册的问题 ([3da843b](https://github.com/wot-ui/wot-starter/commit/3da843ba33bf62d2a8032dabf3061b2ce87e46a9))
### ✏️ Documentation | 文档
* ✏️ 更新 README ([a630274](https://github.com/wot-ui/wot-starter/commit/a63027496f9f75e8106437bf7e4285164a7f91b1))
* ✏️ 更新README ([abde3bc](https://github.com/wot-ui/wot-starter/commit/abde3bca57cbee293d0751dd6f273366425e2474))
* ✏️ 添加分包示例 ([809b65b](https://github.com/wot-ui/wot-starter/commit/809b65b8384029d9ed2c7807709023d65bf6bb4c))
* **README:** ✏️ 更新 vitesse-uni-app 项目链接 ([#11](https://github.com/wot-ui/wot-starter/issues/11)) ([6f1585a](https://github.com/wot-ui/wot-starter/commit/6f1585a6da97a9aeed4071125e6b618f30a50bb7))
### ✨ Features | 新功能
* ✨ 全局反馈组件兼容支付宝小程序 ([7f04d43](https://github.com/wot-ui/wot-starter/commit/7f04d43d44b6eaedabcf32d6c5842b056a0ac8ba))
* ✨ 新增主题切换示例 ([#4](https://github.com/wot-ui/wot-starter/issues/4)) ([c39e756](https://github.com/wot-ui/wot-starter/commit/c39e756821b08ba9934c88f5576d6eabda8fd449))
* ✨ 引入 @uni-ku/root 解决使用 page-meta 和根组件的问题 ([989e9fd](https://github.com/wot-ui/wot-starter/commit/989e9fd05a9c5a3b103608c941e5e30040a19f32))
* ✨ 引入 uni-echarts 支持图表功能,增加分包异步化示例 ([f933a61](https://github.com/wot-ui/wot-starter/commit/f933a6143d5fe02783ade63c669001245970756e))
* ✨ 引入 vite-plugin-uni-pages 的 definePage 宏,优化开发体验 ([498df4f](https://github.com/wot-ui/wot-starter/commit/498df4f26b1a84e8e91827178167ff853ae1f1a9))
* 升级 uni-echarts 版本 ([#17](https://github.com/wot-ui/wot-starter/issues/17)) ([af94964](https://github.com/wot-ui/wot-starter/commit/af9496440e440afae589277c12999644ccfffe3e))
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-PRESENT KeJun
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.
+243
View File
@@ -0,0 +1,243 @@
<div align="center">
<p align="center">
<img src="src/static//logo.png" width="200" />
</p>
<h1 align="center">
FastApp
<sup style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.4em; vertical-align: super; margin-left: 5px;">v3.1.0</sup>
</h1>
<p align="center">
基于 uni-app + Vue 3 + TypeScript 的现代化移动端跨平台开发模板
</p>
<p align="center">
<img src="https://img.shields.io/badge/Vue-3.5.22-green.svg" alt="Vue">
<img src="https://img.shields.io/badge/TypeScript-5.9.2-blue.svg" alt="TypeScript">
<img src="https://img.shields.io/badge/uni--app-3.0.0-orange.svg" alt="uni-app">
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License">
</p>
</div>
> **与仓库根文档的关系**:项目总览、一键前后端启动、演示账号、Docker 部署等请以 [根目录 README.md](../../README.md) 为准;**本文档**侧重 `frontend/app/` 移动端开发说明。
## 项目介绍
FastApp 是 FastapiAdmin 项目的移动端应用,基于 uni-app 框架开发,支持一套代码多端运行。采用 Vue 3 + TypeScript + Vite 等现代化技术栈,集成了完善的代码规范和开发工具链,为开发者提供开箱即用的移动端开发解决方案。
## 特性
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
- 🐂 [Wot UI V2](https://github.com/wot-ui/wot-ui) - 一个轻量、美观、AI友好的 uni-app 组件库
- 🚦 [@wot-ui/router](https://github.com/wot-ui/my-uni) - 适用于uni-app&vue3的轻量级路由库
- 🔄 [Uni Mini CI](https://github.com/Moonofweisheng/uni-mini-ci) - 一个小程序端持续集成的插件
- 🌐 [Alova](https://alova.js.org/zh-CN/) - 极致高效的请求工具集
- 🆒 [Uni Ku](https://uni-ku.js.org/) - 非常酷的 uni-app 插件库
- 📊 [Uni Echarts](https://uni-echarts.xiaohe.ink/) - 适用于 uni-app 的 Apache ECharts 组件
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
- 😃 [各种图标集为你所用](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
- 🔥 使用 [新的 `<script setup>` 语法](https://github.com/vuejs/rfcs/pull/227)
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) - 保证代码质量
## 项目结构
```bash
src/
├── api/ # API 接口定义
├── components/ # 公共组件
│ ├── DateQuery/ # 日期查询组件
│ └── Picker/ # 选择器组件
├── composables/ # 组合式函数
├── constants/ # 常量定义
├── enums/ # 枚举定义
├── http/ # HTTP 请求相关
│ ├── adapters/ # 请求适配器
│ ├── tools/ # 工具函数
│ └── types.ts # 类型定义
├── layouts/ # 布局组件
├── pages/ # 页面目录
│ ├── index/ # 首页
│ ├── login/ # 登录页
│ ├── work/ # 工作台
│ └── mine/ # 个人中心
├── router/ # 路由配置
├── store/ # 状态管理
├── types/ # TypeScript 类型
├── utils/ # 工具函数
├── App.vue # 应用根组件
├── main.ts # 应用入口文件
├── manifest.json # 应用配置文件
├── pages.json # 页面路由配置
└── theme.json # 主题配置
```
## 在线演示
- 📱 移动端:[https://service.fastapiadmin.com/app](https://service.fastapiadmin.com/app)
- 📖 在线文档:[https://service.fastapiadmin.com/](https://service.fastapiadmin.com/)
## 快速开始
### 环境要求
- **Node.js** >= 20
- **pnpm** >= 9
### 安装与运行
```bash
cd frontend/app
pnpm install
pnpm run dev:h5 # 启动 H5 开发服务器
pnpm run build:h5 # 构建 H5 应用
```
### 其他命令
```bash
pnpm run lint:eslint # ESLint 检查并自动修复
pnpm run lint:prettier # Prettier 格式化
pnpm run lint:stylelint # Stylelint 检查样式
pnpm run type-check # TypeScript 类型检查
```
## 截图
| 登录 | 首页 | 个人中心 |
| ---- | ---- | -------- |
| ![移动端登录](../../web/public/app_login.png) | ![移动端首页](../../web/public/app_home.png) | ![移动端个人中心](../../web/public/app_mine.png) |
## 鸣谢
- [uni-app](https://uniapp.dcloud.net.cn/) - 跨平台应用开发框架
- [Vue 3](https://cn.vuejs.org/) - 渐进式 JavaScript 框架
- [Vite](https://cn.vitejs.dev/) - 下一代前端构建工具
- [uni-helper](https://github.com/uni-helper) - 感谢 uni-helper 团队为 uni-app 开发体验优化做出的贡献。
- [vitesse-uni-app](https://github.com/uni-helper/vitesse-uni-app) - 感谢 vitesse-uni-app 提供的快速起手项目。
- [uni-ku](https://uni-ku.js.org/) - 感谢 uni-ku 团队为 uni-app 插件生态做出的贡献。
- [wot-ui-intellisense](https://github.com/wot-ui/wot-ui-intellisense) - wot-ui vscode 代码提示插件
- [awesome-uni-app](https://github.com/uni-helper/awesome-uni-app) - 多端统一开发框架 uni-app 优秀开发资源汇总
- [create-uni](https://github.com/uni-helper/create-uni) - 快速创建 uni-app 项目
- [wot-starter-retail](https://github.com/Moonofweisheng/wot-starter-retail) - 基于 wot-ui 的 uni-app 零售行业模板
- [uni-mini-ci](https://github.com/Moonofweisheng/uni-mini-ci) - 一个 uni-app 小程序端构建后支持 CI(持续集成)的插件
- [@wot-ui/router](https://github.com/wot-ui/my-uni) - 一个基于 vue3 和 Typescript 的轻量级 uni-app 路由库
- [uni-ku-root](https://github.com/uni-ku/root) - 一个模拟 App.vue 原有能力的根组件插件
- [uni-echarts](https://uni-echarts.xiaohe.ink/) - 适用于 uni-app 的 Apache ECharts 组件
## 许可证
本项目采用 [MIT](LICENSE) 许可证。
[![Star History Chart](https://api.star-history.com/svg?repos=FastapiAdmin/FastapiAdmin&type=Date)](https://star-history.com/#FastapiAdmin/FastapiAdmin&Date)
---
**如果这个项目对你有帮助,请给一个 ⭐ Star**
Made with ❤️ by FastApp Team
## wot-UI 组件库
基础组件:
wd-button 按钮
wd-icon 图标
wd-text 文本
Layout 布局:wd-row、wd-col提供了 24列 栅格,通过在 wd-col 上设置 span 属性,通过计算当前内容所占百分比进行分栏
wd-cell-group、wd-cell 单元格
wd-fab 悬浮按钮
wd-transition 过渡动画
wd-resize 监听元素尺寸变化
wd-config-provider 组件Wot全局配置
wd-root-portal 根节点传送,是否从页面中脱离出来,用于解决各种 fixed 失效问题,主要用于制作弹窗、弹出层等。
导航类组件:
wd-navbar 导航栏
wd-tabbar、wd-tabbar-item 标签栏
wd-tabs 标签页
wd-segmented 分段控制器
wd-sidebar、wd-sidebar-item 侧边栏
wd-pagination 分页组件
wd-index-bar、wd-index-anchor 索引栏
wd-backtop 回到顶部
录入类组件:
wd-form、wd-form-item 表单
wd-input 输入框
wd-textarea 文本域
wd-password-input 密码输入框
wd-keyboard 键盘输入框
wd-input-number 数字输入框
wd-search 搜索框
wd-checkbox-group、wd-checkbox 复选框
wd-radio-group、wd-radio 单选框
wd-switch 开关按钮
wd-rate 评分
wd-slider 滑块
wd-picker 选择器
wd-picker-view 选择器视图
wd-select-picker 单复选选择器
wd-cascader 级联选择器
wd-calendar 日历选择器
wd-calendar-view 日历面板
wd-datetime-picker 日期时间选择器
wd-upload 图片、视频和文件上传组件
wd-signature 签名组件
wd-slide-verify 滑动验证组件
反馈组件:
wd-popup 弹出层,用于展示弹窗、信息提示等内容。
wd-overlay 遮罩层,用于在弹出层显示时,遮挡背景,防止用户操作。
wd-dialog 弹出对话框,常用于消息提示、操作确认和输入收集,支持函数式调用
wd-action-sheet 操作表单,从底部弹出的动作菜单面板
wd-drop-menu wd-drop-menu-item 下拉菜单
wd-popover 气泡,常用于展示提示信息或菜单操作
wd-tooltip 文字提示,用于展示简短提示信息,支持多方向定位、受控显隐、自定义内容和动态更新位置
wd-floating-panel 浮动在页面底部的面板,用户可以通过上下拖动秒板来浏览内容,常用于地图导航
wd-loading 加载中组件,用于在异步操作进行中显示加载状态,防止用户操作
wd-progress 进度条,用于展示任务完成进度
wd-circle 圆形进度条,用于展示任务完成进度,支持自定义进度条颜色、进度条宽度、进度条高度等
wd-toast 轻提示,轻提示组件,用于消息通知、加载提示和操作结果反馈,支持组件挂载点配合 useToast() 进行函数式调用。
wd-notify 消息通知,用于在页面顶部展示通知信息。
wd-notice-bar 通知栏,用于在页面顶部展示通知信息,支持自定义内容和样式
wd-swipe-action 滑动操作,用于在列表项上添加滑动操作按钮,支持自定义按钮内容和样式
wd-sort-button 排序按钮,用于在列表项上添加排序按钮,支持自定义按钮内容和样式
wd-empty 空状态组件,用于展示无数据或无结果的情况,一般用于兜底占位展示
wd-count-down 倒计时组件,用于实时展示倒计时数值,支持毫秒级渲染与手动控制
wd-count-to 数字滚动组件,用于展示数值变化,支持自定义滚动速度、滚动时间等
展示类组件:
wd-avatar 头像, 用来代表用户或事物,支持图片、文本或图标展示
wd-badge 徽标,用于展示未读消息、未完成任务等数量,支持自定义数量、颜色、位置等
wd-tag 标签,用于展示分类、状态、标签等信息,支持自定义标签内容、样式、位置等
wd-card 卡片,用于展示信息、操作、内容等,支持自定义卡片内容、样式、位置等
wd-divider 分割线,用于分隔不同内容区域,支持自定义分割线内容、样式、位置等
wd-gap 间距组件,用于在元素之间添加间距,支持自定义间距大小、方向等
wd-grid 宫格,用于在页面上创建网格布局,支持自定义网格列数、网格间距等
wd-collapse、wd-collapse-item 折叠面板,将一组内容放置在多个折叠面板中,点击面板标题可展开或收起内容
wd-steps、wd-step 步骤条,用于引导用户按照流程完成任务,或向用户展示当前所处的步骤状态
wd-sticky 粘性组件,用于在页面滚动时保持元素在顶部或底部,不被内容遮挡
wd-skeleton 骨架屏组件,用于在数据加载中展示占位符,防止用户操作
wd-loadmore 加载更多组件,用于在列表底部添加加载更多按钮,点击后加载更多数据
wd-img 增强版图片组件,支持填充模式、懒加载、加载态/失败态插槽,以及点击预览。
wd-image-preview 图片预览组件,用于在点击图片时预览图片,支持自定义预览位置、预览大小等。
wd-video-preview 视频预览组件,用于在点击视频时预览视频,支持自定义预览位置、预览大小等。
wd-img-cropper 图片剪裁组件,用于图片裁剪,支持拖拽、缩放、旋转等操作
wd-swiper 轮播图组件,用于展示图片、视频等内容,支持自动播放、手动切换、循环播放等
wd-table wd-table-column 用于展示多条结构类似的数据,支持固定列、排序、合并单元格与虚拟滚动等能力
wd-watermark 在页面或组件上添加指定的图片或文字,可用于版权保护、品牌宣传等场景
wd-curtain 幕帘组件,用于在页面上创建一个透明的遮罩层,一般用于公告类图片弹窗展示
组合式api
useUpload:用于处理文件上传和选择相关的逻辑
useCountDown:用于处理倒计时相关的逻辑
useToast:用于处理轻提示相关的逻辑
useDialog:用于处理弹窗相关的逻辑,useDialog 用于函数式调用 wd-dialog,支持 alert、confirm、prompt、show 和 close
useImagePreview:用于处理图片预览相关的逻辑,useImagePreview 用于函数式调用 wd-image-preview,支持自定义预览位置、预览大小等。
useVideoPreview:用于处理视频预览相关的逻辑,useVideoPreview 用于函数式调用 wd-video-preview,支持自定义预览位置、预览大小等。
useConfigProvider:用于处理全局配置相关的逻辑,useConfigProvider 用于函数式调用 wd-config-provider,支持自定义全局配置项,用于在 JS 逻辑中注入全局配置(如主题变量),解决在微信小程序等环境中,由于组件渲染机制限制(如原生插槽作用域隔离)或使用 root-portal 导致无法获取父级 ConfigProvider 配置的问题。
提示:需要和 ConfigProvider 组件配合使用,使用 ConfigProvider 组件包裹你的组件。用于解决小程序端依赖注入的限制,导致部分场景下无法获取父级 ConfigProvider 配置的问题。
+12
View File
@@ -0,0 +1,12 @@
/*
* @Author: weisheng
* @Date: 2024-10-29 20:12:08
* @LastEditTime: 2024-10-29 20:12:20
* @LastEditors: weisheng
* @Description:
* @FilePath: \salary-calculator\commitlint.config.js
* 记得注释
*/
export default {
extends: ['@commitlint/config-conventional'],
}
+17
View File
@@ -0,0 +1,17 @@
import uni from '@uni-helper/eslint-config'
export default uni(
{
unocss: true,
rules: {
'no-console': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
ignores: [
'src/uni_modules/**/*',
'docs/.vitepress/dist',
'docs/.vitepress/cache',
'**/*.md',
],
},
)
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="static/favicon.ico">
<script>
const coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)')
|| CSS.supports('top: constant(a)'))
document.write(
`<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0${
coverSupport ? ', viewport-fit=cover' : ''}" />`)
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
/*
* @Author: weisheng
* @Date: 2025-08-28 20:59:43
* @LastEditTime: 2025-11-17 14:28:09
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/manifest.config.ts
* 记得注释
*/
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': 'wot-starter',
'appid': '__UNI__1208592',
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
'transformPx': false,
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
optimization: {
subPackages: true,
},
// TODO: 替换为实际的微信小程序 appid(在微信公众平台 → 开发管理 → 开发设置 中获取)
appid: 'wxf306aa91619d2e1c',
setting: {
urlCheck: false,
},
usingComponents: true,
darkmode: true,
themeLocation: 'theme.json',
},
'app-harmony': {},
'mp-harmony': {},
'mp-alipay': {
usingComponents: true,
compileOptions: {
globalObjectMode: 'enable',
treeShaking: true,
},
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'h5': {
darkmode: true,
themeLocation: 'theme.json',
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})
+152
View File
@@ -0,0 +1,152 @@
{
"name": "fastapp",
"type": "module",
"version": "3.1.0",
"private": true,
"packageManager": "pnpm@9.9.0",
"license": "MIT",
"engines": {
"node": ">=20.19.0 || >=22.12.0 || >=24.0.0"
},
"scripts": {
"dev": "uni",
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:h5:development": "uni --mode development",
"dev:h5:production": "uni --mode production",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build": "uni build",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:h5:development": "uni build --mode development",
"build:h5:production": "uni build --mode production",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepare": "cd ../.. && husky install frontend/app/.husky",
"commit": "git-cz",
"release-major": "npm version major",
"release-minor": "npm version minor",
"release-patch": "npm version patch",
"alova-gen": "alova gen -f"
},
"dependencies": {
"@alova/adapter-uniapp": "^2.0.16",
"@alova/shared": "^1.3.2",
"@dcloudio/uni-app": "3.0.0-4080520251106001",
"@dcloudio/uni-app-harmony": "3.0.0-4080520251106001",
"@dcloudio/uni-app-plus": "3.0.0-4080520251106001",
"@dcloudio/uni-components": "3.0.0-4080520251106001",
"@dcloudio/uni-h5": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-alipay": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-baidu": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-harmony": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-jd": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-lark": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-qq": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-weixin": "3.0.0-4080520251106001",
"@dcloudio/uni-mp-xhs": "3.0.0-4080520251106001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4080520251106001",
"@vueuse/core": "^11.0.3",
"@vueuse/shared": "11.0.3",
"@wot-ui/router": "^1.1.2",
"@wot-ui/ui": "^2.2.0",
"@wot-ui/unocss-preset": "0.0.1-beta.4",
"alova": "^3.5.1",
"echarts": "^6.0.0",
"pinia": "^2.3.1",
"tslib": "2.3.0",
"uni-echarts": "^2.2.5",
"vue": "~3.4.38",
"vue-demi": "0.14.10",
"vue-i18n": "9.14.5",
"zrender": "6.1.0"
},
"devDependencies": {
"@alova/wormhole": "^1.5.0",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@dcloudio/types": "^3.4.12",
"@dcloudio/uni-automator": "3.0.0-4080520251106001",
"@dcloudio/uni-cli-shared": "3.0.0-4080520251106001",
"@dcloudio/uni-stacktracey": "3.0.0-4080520251106001",
"@dcloudio/uni-vue-devtools": "3.0.0-4020420240722002",
"@dcloudio/vite-plugin-uni": "3.0.0-4080520251106001",
"@iconify-json/carbon": "^1.1.37",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^20.16.2",
"@uni-helper/eslint-config": "^0.5.0",
"@uni-helper/plugin-uni": "^0.1.0",
"@uni-helper/uni-env": "^0.2.0",
"@uni-helper/uni-types": "^1.1.0",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@uni-helper/vite-plugin-uni-components": "^0.3.2",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.12",
"@uni-helper/vite-plugin-uni-pages": "^0.3.23",
"@uni-helper/volar-service-uni-pages": "^0.3.24",
"@uni-ku/bundle-optimizer": "^2.1.0",
"@uni-ku/root": "^1.4.1",
"@unocss/eslint-config": "^66.5.6",
"@vue/runtime-core": "^3.4.38",
"@vue/tsconfig": "^0.5.1",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.39.1",
"git-cz": "^4.9.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.9",
"miniprogram-api-typings": "^3.12.3",
"sass": "^1.99.0",
"typescript": "~5.5.4",
"unocss": "66.0.0",
"unplugin-auto-import": "^0.18.2",
"vite": "^5.2.8",
"vue-tsc": "^2.0.29"
},
"pnpm": {
"overrides": {
"@intlify/message-compiler": "9.14.5",
"@intlify/shared": "9.14.5",
"unconfig": "7.3.2"
},
"peerDependencyRules": {
"allowedVersions": {
"vue": "3.4.38",
"@dcloudio/types": "3.4.31"
}
}
},
"lint-staged": {
"*": "eslint --fix"
}
}
+56
View File
@@ -0,0 +1,56 @@
/*
* @Author: weisheng
* @Date: 2025-06-23 22:23:05
* @LastEditTime: 2025-06-27 13:04:54
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/pages.config.ts
* 记得注释
*/
import process from 'node:process'
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
// 按构建平台动态裁剪配置(vite-plugin-uni-pages 不处理条件编译注释,用 JS 逻辑)
const UNI_PLATFORM = process.env.UNI_PLATFORM || ''
const isWeixin = UNI_PLATFORM === 'mp-weixin'
const isAlipay = UNI_PLATFORM === 'mp-alipay'
export default defineUniPages({
pages: [],
globalStyle: {
// 导航栏配置
navigationBarBackgroundColor: '@navBgColor',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: 'FastapiAdmin',
// 页面背景配置
backgroundColor: '@bgColor',
backgroundTextStyle: '@bgTxtStyle',
backgroundColorTop: '@bgColorTop',
backgroundColorBottom: '@bgColorBottom',
// 下拉刷新配置
enablePullDownRefresh: false,
onReachBottomDistance: 50,
// 页面切换动画(微信小程序 window 不支持 animationType/animationDuration,构建时排除)
...(isWeixin ? {} : { animationType: 'pop-in', animationDuration: 300 }),
},
tabBar: {
custom: true,
// customize/overlay 为支付宝小程序 tabBar 配置(微信端编译进 app.json 会告警,按平台裁剪)
...(isAlipay ? { customize: true, overlay: true } : {}),
height: '0',
color: '@tabColor',
selectedColor: '@tabSelectedColor',
backgroundColor: '@tabBgColor',
borderStyle: '@tabBorderStyle',
list: [{
pagePath: 'pages/index/index',
}, {
pagePath: 'pages/work/index',
}, {
pagePath: 'pages/mine/index',
}],
},
})
+15738
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
# 单包项目,无子包;必须声明 packages,否则 pnpm 报 "packages field missing or empty"
packages: []
# pnpm 11.5+ 的 build 白名单(map 形式,取代旧 onlyBuiltDependencies 数组)
allowBuilds:
'@parcel/watcher': true
'@uni-helper/unocss-preset-uni': true
core-js: true
core-js-pure: true
esbuild: true
vue-demi: true
# pnpm 11+ 配置中心(.npmrc 仅保留 auth/registry;package.json 的 pnpm 字段已废弃)
# 从 package.json#pnpm 与 .npmrc 迁移而来,勿改回旧位置
# 依赖版本覆盖(原 package.json#pnpm.overrides)
overrides:
# vue-i18n@9.14.5 的 core-base/message-compiler 依赖需要 9.14.5 的 @intlify/*;
# 但公共提升区混有 uni 工具链内置的 9.1.9,H5 按 symlink 路径解析会命中旧版(缺 join/CompileWarnCodes 等导出),统一版本解决
'@intlify/message-compiler': 9.14.5
'@intlify/shared': 9.14.5
unconfig: 7.3.2
+25
View File
@@ -0,0 +1,25 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxf306aa91619d2e1c",
"editorSetting": {}
}
+14
View File
@@ -0,0 +1,14 @@
{
"libVersion": "3.17.0",
"projectname": "app",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["github>uni-helper/renovate-config"]
}
+25
View File
@@ -0,0 +1,25 @@
{
"version": 1,
"skills": {
"create-wot-ui-theme": {
"source": "wot-ui/open-wot",
"sourceType": "github",
"computedHash": "0557819ce03183457bd196af213996963b5713641bf76546306d826501bab188"
},
"wot-ui-cli": {
"source": "wot-ui/open-wot",
"sourceType": "github",
"computedHash": "b2e370a30ff8f1e31c4b1a0ac91d16928d6fc90e68f02913e659986c94d88b0c"
},
"wot-ui-unocss-preset-guide": {
"source": "wot-ui/open-wot",
"sourceType": "github",
"computedHash": "bf1f6f752d0fef15063b56df11a902ff110ae177d867cef76b953ef237c05a70"
},
"wot-ui-v2": {
"source": "wot-ui/open-wot",
"sourceType": "github",
"computedHash": "207723b8a0e23cb5657ce9dae012ab92c0d6397212d56b07fafbdbc1ba9c3c2e"
}
}
}
+46
View File
@@ -0,0 +1,46 @@
<script setup lang="ts">
const { themeVars, theme, currentThemeColor } = useTheme()
/** 根节点类名:page-wraper + 主题模式 + 主题色(供全局水滴渐变按主题色换肤,如 theme-blue) */
const rootClass = computed(() => `page-wraper ${theme.value} theme-${currentThemeColor.value.value}`)
/** 主题色 → 原生页面背景色(与水滴渐变基底中段色一致,用于下拉/回弹区无缝衔接) */
const THEME_PAGE_BG: Record<string, string> = {
blue: '#F2FAFF',
orange: '#FFFBF6',
green: '#F5FDF8',
pink: '#FFF9FC',
purple: '#FAF7FF',
red: '#FFF9F9',
}
/**
同步原生页面背景色:亮色取当前主题色基底色,暗色取黑,消除下拉/回弹时的中性灰断层
(uni.setBackgroundColor 仅小程序端支持,H5/APP 由 CSS 渐变直接铺满,无需调用)
*/
function syncPageBackground() {
// 暗色用 --wot-filled-content 对应色(coolgrey-9 #272B3B),与导航栏/页面背景一致
const bg = theme.value === 'dark' ? '#272B3B' : (THEME_PAGE_BG[currentThemeColor.value.value] ?? '#F2FAFF')
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU
uni.setBackgroundColor({ backgroundColor: bg, backgroundColorTop: bg, backgroundColorBottom: bg })
// #endif
}
watch(() => [theme.value, currentThemeColor.value.value], syncPageBackground, { immediate: true })
</script>
<template>
<wd-config-provider :theme-vars="themeVars" :theme="theme" :custom-class="rootClass">
<ku-root-view />
<wd-notify />
<wd-dialog />
<wd-toast />
<global-loading />
<global-toast />
<global-message />
<global-dialog />
<!-- #ifdef MP-WEIXIN -->
<privacy-popup />
<!-- #endif -->
</wd-config-provider>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
onLaunch(() => {})
</script>
<style lang="scss">
/* 全局样式按职责拆分:
- theme.scss:玻璃水滴渐变主题变量(6 套主题色)
- base.scss:页面容器 / 暗色背景等基础全局样式
页面级样式(登录表单输入框、滑块等)保留在各页面 scoped 内 */
@import './styles/theme.scss';
@import './styles/base.scss';
</style>
+92
View File
@@ -0,0 +1,92 @@
/**
* AI 聊天 API 模块(对应后端 module_ai 插件)
*/
import { http } from '@/http'
const AI_BASE = '/ai/chat'
export const ChatAPI = {
/** 获取会话列表 */
getSessions(params?: Record<string, any>): Promise<PageResult<ChatSession>> {
return http.Get(`${AI_BASE}/list`, params)
},
/** 创建会话 */
createSession(title?: string): Promise<ChatSession> {
return http.Post(`${AI_BASE}/create`, { title: title || '新对话' })
},
/** 获取会话详情(含消息列表) */
getDetail(sessionId: number): Promise<{ messages: ChatMessage[] }> {
return http.Get(`${AI_BASE}/detail/${sessionId}`)
},
/** 更新会话标题 */
updateSession(sessionId: number, data: Record<string, any>): Promise<void> {
return http.Put(`${AI_BASE}/update/${sessionId}`, data)
},
/** 删除会话 */
removeSession(ids: number[]): Promise<void> {
return http.Delete(`${AI_BASE}/delete`, { ids: JSON.stringify(ids) })
},
/** 发送消息 (非流式) */
sendMessage(sessionId: number, content: string): Promise<ChatMessage> {
// silent:聊天页将错误内联展示为 AI 消息,避免与全局 toast 重复提示
return http.Post(`${AI_BASE}/ai-chat`, { session_id: sessionId, content }, { meta: { silent: true } })
},
/** 获取 AI 模型配置列表 */
getModels(): Promise<AIModelList> {
return http.Get(`${AI_BASE}/model`)
},
/** 新增 AI 模型配置 */
createModel(data: AIModelForm): Promise<AIModelConfig> {
return http.Post(`${AI_BASE}/model`, data)
},
/** 更新 AI 模型配置 */
updateModel(configId: number, data: AIModelForm): Promise<AIModelConfig> {
return http.Put(`${AI_BASE}/model/${configId}`, data)
},
/** 删除 AI 模型配置 */
deleteModel(configId: number): Promise<void> {
return http.Delete(`${AI_BASE}/model/${configId}`)
},
/** 切换激活的 AI 模型配置 */
activateModel(configId: number): Promise<void> {
return http.Post(`${AI_BASE}/model/${configId}/activate`)
},
}
/* ==================== 类型定义 ==================== */
export interface ChatSession {
id: number
session_name?: string
title?: string
created_at?: number
}
export interface ChatMessage {
id?: string
role: string
content: string
created_at?: number
time?: string
}
export interface AIModelForm {
name: string
base_url: string
api_key: string
model_id: string
temperature?: number
}
export interface AIModelConfig extends AIModelForm {
config_id: number
is_active?: boolean
created_time?: string
updated_time?: string
}
/** 模型配置列表响应:items 为配置数组,active_id 为当前激活的配置 id */
export interface AIModelList {
items: AIModelConfig[]
active_id: number | null
}
@@ -0,0 +1,39 @@
import { http } from '@/http'
const MONITOR_BASE = '/monitor'
/**
* 运营大盘 API
* 与 web 端 module_monitor/dashboard.ts 对齐
*/
export const DashboardAPI = {
getStats(): Promise<DashboardStats> {
return http.Get(`${MONITOR_BASE}/online/stats`)
},
}
export interface RecentLoginItem {
username: string
status: number
login_time: string
login_ip?: string
login_location?: string
}
export interface DashboardStats {
online_users: number
total_users: number
today_login_count: number
today_unique_users: number
week_user_created: number
login_trend: LoginTrendItem[]
recent_logins: RecentLoginItem[]
}
/** 登录趋势(按天聚合,近7天) */
export interface LoginTrendItem {
day: string
logins: number
unique_users: number
new_users: number
}
@@ -0,0 +1,71 @@
import { http } from '@/http'
const MONITOR_BASE = '/monitor'
/**
* 服务器监控 API
* 与 web 端 module_monitor/server.ts 对齐(补全完整字段)
*/
export const ServerAPI = {
getInfo(): Promise<ServerInfo> {
return http.Get(`${MONITOR_BASE}/server/info`)
},
}
export interface CpuInfo {
/** 逻辑核心数 */
cpu_num: number
/** user 占用百分比 */
used: number
/** system 占用百分比 */
sys: number
/** idle 百分比 */
free: number
}
export interface MemoryInfo {
total: string
used: string
free: string
usage: number
}
export interface DiskInfo {
dir_name: string
sys_type_name: string
type_name: string
total: string
used: string
free: string
/** 使用率百分比 */
usage: number
}
export interface SysInfo {
computer_ip: string
computer_name: string
os_arch: string
os_name: string
user_dir: string
}
export interface PyInfo {
name: string
version: string
start_time: string
run_time: string
home: string
memory_total: string
memory_used: string
memory_free: string
/** 进程内存占用率百分比(rss / available) */
memory_usage: number
}
export interface ServerInfo {
cpu?: CpuInfo
mem?: MemoryInfo
sys?: SysInfo
py?: PyInfo
disks?: DiskInfo[]
}
+180
View File
@@ -0,0 +1,180 @@
import { http } from '@/http'
import { ContentTypeEnum } from '@/http/tools/enum'
const AUTH_BASE_URL = '/system/auth'
/** 方案提供方 */
export type OAuthProvider = 'wechat' | 'qq' | 'github' | 'gitee'
/**
* 认证 API
* 与 web 端 module_system/auth.ts 对齐(完整字段定义)
*/
const AuthAPI = {
/**
* 登录
* @param body 登录表单数据
* @returns 登录结果
*/
login(body: LoginFormData): Promise<LoginResult> {
return http.Post(`${AUTH_BASE_URL}/login`, body, {
headers: {
'Content-Type': ContentTypeEnum.FORM_URLENCODED,
},
// authRole: 'login':登录 401(密码错误)按普通错误处理,不触发 token 刷新逻辑
meta: { ignoreAuth: true, authRole: 'login' },
})
},
/**
* 刷新令牌
* @param body 刷新令牌请求体
* @returns 新的访问令牌
*/
refreshToken(body: RefreshToekenBody): Promise<LoginResult> {
// authRole: 'refreshToken':让 http 层 401 处理器识别刷新请求,自身 401 时不触发刷新逻辑(避免死循环)
// silent:刷新失败由 http 层统一跳转登录,无需全局 toast
return http.Post(`${AUTH_BASE_URL}/token/refresh`, body, {
meta: { ignoreAuth: true, silent: true, authRole: 'refreshToken' },
})
},
/**
* 获取验证码
* @returns 验证码信息
*/
getCaptcha(): Promise<CaptchaInfo> {
// 添加随机参数防止缓存
const timestamp = new Date().getTime()
return http.Get(`${AUTH_BASE_URL}/captcha/get?timestamp=${timestamp}`, { meta: { ignoreAuth: true, authRole: 'visitor' } })
},
/**
* 登出
* 后端 logout 接口 body 为纯字符串(JWT 原文,Annotated[str, Body]),
* 需显式 JSON.stringify 使请求体成为合法 JSON 字符串(uni.request 对字符串原样发送)
* @param token 访问令牌
*/
logout(token: string): Promise<void> {
return http.Post(`${AUTH_BASE_URL}/logout`, JSON.stringify(token))
},
/**
* 获取第三方 OAuth 登录跳转 URL
* @param provider oauth 提供商: wechat / qq / github / gitee
* @returns 跳转 URL
*/
getOAuthLoginUrl(provider: OAuthProvider): Promise<{ url: string }> {
return http.Get(`${AUTH_BASE_URL}/oauth/${provider}/login`, { meta: { ignoreAuth: true, authRole: 'visitor' } })
},
/**
* 滑块验证码完成
* 后端仅标记 captcha_key 状态为 verified,不校验 x 坐标值(x 为占位字段)
* @param data 验证数据
* @param data.captcha_key 验证码 key
* @param data.x 滑块 x 坐标(占位,后端未使用)
* @returns 验证结果 { captcha_key, verified }
*/
completeSliderCaptcha(data: { captcha_key: string, x: number }): Promise<{ captcha_key: string, verified: boolean }> {
return http.Post(`${AUTH_BASE_URL}/captcha/slider/complete`, data, { meta: { ignoreAuth: true, authRole: 'visitor' } })
},
/**
* 微信小程序登录
* 前端通过 uni.login 获取 code,后端调用 code2Session 换取 openid 后返回 JWT
* @param data 微信登录数据
* @param data.code uni.login 返回的 code
* @param data.nickname 用户昵称(可选,来自 getUserProfile)
* @param data.avatar 头像 URL(可选)
* @returns JWT 登录结果
*/
wxLogin(data: WxLoginData): Promise<LoginResult> {
return http.Post(`${AUTH_BASE_URL}/wx-login`, data, { meta: { ignoreAuth: true, authRole: 'visitor' } })
},
/**
* 微信小程序手机号快速登录
* 用户点击<button open-type="getPhoneNumber">后,回调 e.detail.code 发送给后端
* 后端通过 getuserphonenumber API 直接获取手机号(2023+ 新方案,无需 AES 解密)
* @param data 手机号登录数据
* @param data.code getPhoneNumber 回调返回的动态令牌 code
* @returns JWT 登录结果
*/
wxPhoneLogin(data: WxPhoneLoginData): Promise<LoginResult> {
return http.Post(`${AUTH_BASE_URL}/wx-phone-login`, data, { meta: { ignoreAuth: true, authRole: 'visitor' } })
},
/**
* 生成小程序码
* 调用后端接口,后端通过微信 getWXACodeUnlimit API 生成无限制小程序码
* @param data 生成参数
* @param data.scene 场景参数(最大32字符,如 invite_123)
* @param data.page 小程序页面路径(可选,默认主页)
* @param data.width 图片宽度(px,默认 430)
* @returns 包含 base64 图片 URL 的结果
*/
generateWxQrCode(data: WxQrCodeParams): Promise<WxQrCodeResult> {
return http.Post(`${AUTH_BASE_URL}/wx-qrcode/generate`, data)
},
}
export default AuthAPI
/** 登录表单数据 */
export interface LoginFormData {
username: string
password: string
captcha_key?: string
captcha?: string
remember?: boolean
login_type?: string
}
/** 刷新令牌请求体 */
export interface RefreshToekenBody {
refresh_token: string
}
/** JWT 响应 */
export interface LoginResult {
access_token: string
refresh_token: string
token_type: string
expires_in: number
}
/** 验证码信息(滑块模式:img_base 为空字符串) */
export interface CaptchaInfo {
enable: boolean
key: string
img_base: string
}
/** 微信小程序登录数据 */
export interface WxLoginData {
code: string
nickname?: string
avatar?: string
}
/** 微信手机号登录数据(2023+ 新方案:仅传 code) */
export interface WxPhoneLoginData {
code: string
}
/** 小程序码生成参数 */
export interface WxQrCodeParams {
/** 场景值(最大32字符,如 invite_123) */
scene: string
/** 目标页面路径(不带 /,如 pages/index/index),为空则默认主页 */
page?: string
/** 宽度(px),默认 430,范围 280-1280 */
width?: number
}
/** 小程序码生成结果 */
export interface WxQrCodeResult {
/** 小程序码图片 URL(data:image/png;base64,...) */
url: string
}
@@ -0,0 +1,42 @@
import type { BatchSetStatus } from './user'
import { http } from '@/http'
const SYSTEM_BASE = '/system'
/**
* 公告管理 API
* 与 web 端 module_system/notice.ts 对齐(完整字段定义)
*/
export const NoticeAPI = {
getPage(params?: Record<string, any>) {
return http.Get<PageResult<NoticeItem>>(`${SYSTEM_BASE}/notice/list`, params)
},
getDetail(id: number): Promise<NoticeItem> {
return http.Get(`${SYSTEM_BASE}/notice/detail/${id}`)
},
create(data: NoticeForm): Promise<NoticeItem> {
return http.Post(`${SYSTEM_BASE}/notice/create`, data)
},
update(id: number, data: NoticeForm): Promise<NoticeItem> {
return http.Put(`${SYSTEM_BASE}/notice/update/${id}`, data)
},
remove(ids: number[]): Promise<void> {
return http.Delete(`${SYSTEM_BASE}/notice/delete`, { ids: JSON.stringify(ids) })
},
batchStatus(data: BatchSetStatus): Promise<void> {
return http.Patch(`${SYSTEM_BASE}/notice/status/batch`, data)
},
getAvailable(): Promise<NoticeItem[]> {
return http.Get(`${SYSTEM_BASE}/notice/available`)
},
}
export interface NoticeForm extends BaseFormType {
notice_title?: string
notice_type?: string
notice_content?: string
status?: number
description?: string
}
export interface NoticeItem extends BaseType, NoticeForm {}
@@ -0,0 +1,34 @@
import { http } from '@/http'
const PARAM_BASE_URL = '/system/param'
/** 系统参数项(与后端 ParamsOutSchema 对齐) */
export interface ParamsItem {
id: number
config_name: string
config_key: string
config_value: string | null
config_type: boolean
status: number
description?: string | null
created_time?: string
update_time?: string
}
/** 系统参数管理 API */
const ParamsAPI = {
/**
* 获取初始化缓存参数(系统配置列表)
*
* 免认证接口:登录页(无 token)也会调用,需标记 meta.ignoreAuth 跳过鉴权注入
*
* @returns 系统配置项列表,config_key 唯一标识
*/
getInitConfig(): Promise<ParamsItem[]> {
return http.Get(`${PARAM_BASE_URL}/info`, {
meta: { ignoreAuth: true },
})
},
}
export default ParamsAPI
@@ -0,0 +1,86 @@
import { http } from '@/http'
const SYSTEM_BASE = '/system'
/**
* 工单管理 API
* 与 web 端 module_system/ticket.ts 对齐(完整字段定义)
*/
export const TicketAPI = {
getPage(params?: Record<string, any>) {
return http.Get<PageResult<TicketItem>>(`${SYSTEM_BASE}/ticket/list`, params)
},
getDetail(id: number): Promise<TicketItem> {
return http.Get(`${SYSTEM_BASE}/ticket/detail/${id}`)
},
create(data: TicketForm): Promise<TicketItem> {
return http.Post(`${SYSTEM_BASE}/ticket/create`, data)
},
update(id: number, data: Record<string, any>): Promise<TicketItem> {
return http.Put(`${SYSTEM_BASE}/ticket/update/${id}`, data)
},
remove(ids: number[]): Promise<void> {
return http.Delete(`${SYSTEM_BASE}/ticket/delete`, { ids: JSON.stringify(ids) })
},
batch(data: { ids: number[], status: number, assigned_id?: number }): Promise<void> {
return http.Put(`${SYSTEM_BASE}/ticket/batch`, data)
},
exportTickets(params?: Record<string, any>): Promise<unknown> {
return http.Post(`${SYSTEM_BASE}/ticket/export`, params)
},
getComments(ticketId: number, params?: Record<string, any>): Promise<PageResult<TicketComment>> {
return http.Get(`${SYSTEM_BASE}/ticket/${ticketId}/comments`, params)
},
createComment(ticketId: number, data: { content: string }): Promise<TicketComment> {
return http.Post(`${SYSTEM_BASE}/ticket/${ticketId}/comments`, data)
},
/**
* 工单聚合统计(待处理/处理中/已完成),由后端一次聚合返回,
* 替代前端 3 次分页请求计数。alova 缓存 30 秒,work/mine 页共享。
*/
getStats() {
const method = http.Get<TicketStats>(`${SYSTEM_BASE}/ticket/stats`)
method.config.cacheFor = 30_000
return method
},
}
export interface TicketForm extends BaseFormType {
title: string
ticket_content?: string
summary?: string
ticket_type: string
images?: string
reply?: string
assigned_id?: number
status?: number
description?: string
}
export interface TicketItem extends BaseType {
title?: string
ticket_content?: string
summary?: string
ticket_type?: string
status?: string | number
assigned_id?: number
assigned_by?: CommonType
images?: string
reply?: string
description?: string
}
export interface TicketComment extends BaseType {
ticket_id?: number
user_id?: number
username?: string
content?: string
created_by_name?: string
}
/** 工单聚合统计结果(后端一次性返回) */
export interface TicketStats {
pending: number
processing: number
done: number
}
+331
View File
@@ -0,0 +1,331 @@
import { http } from '@/http'
const USER_BASE_URL = '/system/user'
/**
* 用户管理 API
* 与 web 端 module_system/user.ts 对齐(完整字段定义)
*/
const UserAPI = {
/**
* 个人中心用户信息
*
* @returns 登录用户昵称、头像信息,包括角色和权限
*/
getCurrentUserInfo(): Promise<UserInfo> {
return http.Get(`${USER_BASE_URL}/current/info`)
},
/**
* 当前用户头像上传
*
* @param body 上传参数
* @param body.filePath 本地临时文件路径(uni.chooseImage 选择结果)
* @param body.name 上传字段名,后端约定为 file
* @returns uni.uploadFile 成功回调(statusCode + data 响应体字符串,需调用方解析)
*/
uploadCurrentUserAvatar(body: { filePath: string, name?: string }): Promise<{ statusCode: number, data: string }> {
return http.Post(`${USER_BASE_URL}/current/avatar/upload`, body, { requestType: 'upload' })
},
/**
* 修改个人中心用户信息
*
* @param body
* @returns 修改后的用户信息
*/
updateCurrentUserInfo(body: UserProfileForm): Promise<UserInfo> {
return http.Put(`${USER_BASE_URL}/current/info/update`, body)
},
/**
* 修改个人中心用户密码
*
* @param body
* @returns 修改后的用户信息
*/
changeCurrentUserPassword(body: PasswordChangeForm): Promise<void> {
return http.Put(`${USER_BASE_URL}/current/password/change`, body)
},
/**
* 注册用户(公开接口)
*
* @param body 注册参数
* @param body.username 用户名(字母开头,3-32 位)
* @param body.password 密码(6-128 位)
* @param body.name 昵称(可选)
* @returns 注册结果
*/
registerUser(body: RegisterForm): Promise<void> {
return http.Post(`${USER_BASE_URL}/register`, body, { meta: { ignoreAuth: true } })
},
/**
* 忘记密码(公开接口)
*
* @param body 重置参数
* @param body.username 用户名(字母开头,3-32 位)
* @param body.new_password 新密码(6-128 位)
* @returns 重置结果
*/
forgetPassword(body: ForgetPasswordForm): Promise<void> {
return http.Post(`${USER_BASE_URL}/password/forget`, body, { meta: { ignoreAuth: true } })
},
/**
* 重置用户密码
*
* @param id 用户ID
* @param body 新密码
* @param body.password 新密码
*/
resetPassword(id: number, body: { password: string }): Promise<void> {
return http.Put(`${USER_BASE_URL}/password/reset/${id}`, body)
},
/**
* 批量修改用户状态
*
* @param body 批量操作参数
* @param body.ids 用户ID列表
* @param body.status 目标状态
*/
batchStatus(body: BatchSetStatus): Promise<void> {
return http.Patch(`${USER_BASE_URL}/status/batch`, body)
},
/**
* 导出用户
*
* @param params 导出参数
*/
exportUsers(params?: Record<string, any>): Promise<unknown> {
return http.Post(`${USER_BASE_URL}/export`, params)
},
/**
* 获取用户导入模板
*/
getImportTemplate(): Promise<unknown> {
return http.Get(`${USER_BASE_URL}/import/template`)
},
/**
* 导入用户数据
*
* @param body 导入数据
*/
importData(body: Record<string, any>): Promise<void> {
return http.Post(`${USER_BASE_URL}/import/data`, body)
},
/**
* 获取用户分页列表
*
* @param queryParams 查询参数
*/
getUserPage(queryParams: UserPageQuery): Promise<PageResult<UserInfo>> {
return http.Get(`${USER_BASE_URL}/list`, queryParams)
},
/**
* 获取用户表单详情
*
* @param userId 用户ID
* @returns 用户表单详情
*/
getUserDetail(userId: number): Promise<UserForm> {
return http.Get(`${USER_BASE_URL}/detail/${userId}`)
},
/**
* 添加用户
*
* @param body 用户表单数据
*/
addUser(body: UserForm): Promise<void> {
return http.Post(`${USER_BASE_URL}/create`, body)
},
/**
* 修改用户
*
* @param body 用户表单数据
*/
updateUser(body: UserForm): Promise<void> {
return http.Put(`${USER_BASE_URL}/update`, body)
},
/**
* 删除用户
*
* @param ids 用户ID数组
*/
deleteUser(ids: number[]): Promise<void> {
return http.Delete(`${USER_BASE_URL}/delete`, ids)
},
}
export default UserAPI
/* 忘记密码表单(与后端 UserForgetPasswordSchema 一致,confirmPassword 为前端校验字段不提交) */
export interface ForgetPasswordForm {
username: string
new_password: string
}
/* 注册表单 */
export interface RegisterForm {
username: string
password: string
name?: string
}
/* 分页查询表单 */
export interface UserPageQuery extends PageQuery {
username?: string
name?: string
mobile?: string
email?: string
dept_id?: number
status?: number
start_time?: string
end_time?: string
}
/* 搜索选择器数据类型 */
export interface searchSelectDataType {
name?: string
status?: number
}
/* 用户表单 */
export interface UserForm extends BaseFormType {
username?: string
name?: string
dept_id?: number
dept_name?: string
role_ids?: number[]
role_names?: string[]
position_ids?: number[]
position_names?: string[]
password?: string
gender?: number
email?: string
mobile?: string
avatar?: string
is_superuser?: boolean
status?: number
description?: string
}
/* 登录用户信息 */
export interface UserInfo extends BaseType {
username?: string
name?: string
avatar?: string
email?: string
mobile?: string
gender?: string
password?: string
menus?: MenuTable[]
dept?: deptTreeType
dept_id?: deptTreeType['id']
dept_name?: deptTreeType['name']
roles?: roleSelectorType[]
role_names?: roleSelectorType['name'][]
role_ids?: roleSelectorType['id'][]
positions?: positionSelectorType[]
position_names?: positionSelectorType['name'][]
position_ids?: positionSelectorType['id'][]
is_superuser?: boolean
last_login?: string
gitee_login?: string
github_login?: string
wx_login?: string
qq_login?: string
status?: number
description?: string
}
/* 菜单表 */
export interface MenuTable extends BaseType {
name?: string
type?: number
icon?: string
order?: number
permission?: string
route_name?: string
route_path?: string
component_path?: string
redirect?: string
parent_id?: number
parent_name?: string
keep_alive?: boolean
hidden?: boolean
always_show?: boolean
title?: string
params?: { key: string, value: string }[]
affix?: boolean
status?: number
description?: string
children?: MenuTable[]
}
/* 部门树 */
export interface deptTreeType {
id?: number
name?: string
parent_id?: number
children?: deptTreeType[]
}
/* 角色选择器 */
export interface roleSelectorType {
id?: number
name?: string
status?: number
description?: string
}
/* 职位选择器 */
export interface positionSelectorType {
id?: number
name?: string
status?: number
description?: string
}
/* 个人中心用户信息表单 */
export interface UserProfileForm extends BaseFormType {
name?: string
gender?: string
mobile?: string
email?: string
username?: string
dept_name?: string
positions?: positionSelectorType[]
roles?: roleSelectorType[]
avatar?: string
created_time?: string
}
/* 修改密码表单 */
export interface PasswordChangeForm {
old_password: string
new_password: string
confirm_password: string
}
/* 重置密码表单 */
export interface ResetPasswordForm {
id: number
password: string
}
/* 批量设置状态 */
export interface BatchSetStatus {
ids: number[]
status: number
}
+706
View File
@@ -0,0 +1,706 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const CommonUtil: typeof import('./utils/common')['CommonUtil']
const EffectScope: typeof import('vue')['EffectScope']
const Storage: typeof import('./utils/storage')['Storage']
const TEMPLATE_IDS: typeof import('./composables/useSubscribeMessage')['TEMPLATE_IDS']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createRouter: typeof import('@wot-ui/router')['createRouter']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentPath: typeof import('./utils/index')['getCurrentPath']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getSystemTheme: typeof import('./utils/systemTheme')['getSystemTheme']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const initializeThemeOnce: typeof import('./utils/systemTheme')['initializeThemeOnce']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onError: typeof import('@dcloudio/uni-app')['onError']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onHide: typeof import('@dcloudio/uni-app')['onHide']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
const onReady: typeof import('@dcloudio/uni-app')['onReady']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onResize: typeof import('@dcloudio/uni-app')['onResize']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
const onShow: typeof import('@dcloudio/uni-app')['onShow']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistPlugin: typeof import('./store/persist')['persistPlugin']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const subscribeSystemThemeChange: typeof import('./utils/systemTheme')['subscribeSystemThemeChange']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const themeColorOptions: typeof import('./composables/useTheme')['themeColorOptions']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toLoginPage: typeof import('./utils/toLoginPage')['toLoginPage']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAiChat: typeof import('./composables/useAiChat')['useAiChat']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfigStore: typeof import('./store/configStore')['useConfigStore']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDialog: typeof import('@wot-ui/ui')['useDialog']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGlobalDialog: typeof import('./composables/useGlobalDialog')['useGlobalDialog']
const useGlobalLoading: typeof import('./composables/useGlobalLoading')['useGlobalLoading']
const useGlobalMessage: typeof import('./composables/useGlobalMessage')['useGlobalMessage']
const useGlobalToast: typeof import('./composables/useGlobalToast')['useGlobalToast']
const useI18nNavTitle: typeof import('./composables/useI18nNavTitle')['useI18nNavTitle']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useListPage: typeof import('./composables/useListPage')['useListPage']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNotify: typeof import('@wot-ui/ui')['useNotify']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const usePagination: typeof import('alova/client')['usePagination']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useRequest: typeof import('alova/client')['useRequest']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('@wot-ui/router')['useRoute']
const useRouter: typeof import('@wot-ui/router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('./composables/useShare')['useShare']
const useSharePoster: typeof import('./composables/useSharePoster')['useSharePoster']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSubscribeMessage: typeof import('./composables/useSubscribeMessage')['useSubscribeMessage']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTabbar: typeof import('./composables/useTabbar')['useTabbar']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useTheme: typeof import('./composables/useTheme')['useTheme']
const useThemeStore: typeof import('./store/themeStore')['useThemeStore']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('@wot-ui/ui')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useUserStore: typeof import('./store/userStore')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWatermark: typeof import('./composables/useWatermark')['useWatermark']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const useWxLogin: typeof import('./composables/useWxLogin')['useWxLogin']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly CommonUtil: UnwrapRef<typeof import('./utils/common')['CommonUtil']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly Storage: UnwrapRef<typeof import('./utils/storage')['Storage']>
readonly TEMPLATE_IDS: UnwrapRef<typeof import('./composables/useSubscribeMessage')['TEMPLATE_IDS']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createRouter: UnwrapRef<typeof import('@wot-ui/router')['createRouter']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentPath: UnwrapRef<typeof import('./utils/index')['getCurrentPath']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getSystemTheme: UnwrapRef<typeof import('./utils/systemTheme')['getSystemTheme']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly initializeThemeOnce: UnwrapRef<typeof import('./utils/systemTheme')['initializeThemeOnce']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistPlugin: UnwrapRef<typeof import('./store/persist')['persistPlugin']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly subscribeSystemThemeChange: UnwrapRef<typeof import('./utils/systemTheme')['subscribeSystemThemeChange']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly themeColorOptions: UnwrapRef<typeof import('./composables/useTheme')['themeColorOptions']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toLoginPage: UnwrapRef<typeof import('./utils/toLoginPage')['toLoginPage']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAiChat: UnwrapRef<typeof import('./composables/useAiChat')['useAiChat']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfigStore: UnwrapRef<typeof import('./store/configStore')['useConfigStore']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDialog: UnwrapRef<typeof import('@wot-ui/ui')['useDialog']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGlobalDialog: UnwrapRef<typeof import('./composables/useGlobalDialog')['useGlobalDialog']>
readonly useGlobalLoading: UnwrapRef<typeof import('./composables/useGlobalLoading')['useGlobalLoading']>
readonly useGlobalMessage: UnwrapRef<typeof import('./composables/useGlobalMessage')['useGlobalMessage']>
readonly useGlobalToast: UnwrapRef<typeof import('./composables/useGlobalToast')['useGlobalToast']>
readonly useI18nNavTitle: UnwrapRef<typeof import('./composables/useI18nNavTitle')['useI18nNavTitle']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useListPage: UnwrapRef<typeof import('./composables/useListPage')['useListPage']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNotify: UnwrapRef<typeof import('@wot-ui/ui')['useNotify']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly usePagination: UnwrapRef<typeof import('alova/client')['usePagination']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useRequest: UnwrapRef<typeof import('alova/client')['useRequest']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('@wot-ui/router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('@wot-ui/router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('./composables/useShare')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSubscribeMessage: UnwrapRef<typeof import('./composables/useSubscribeMessage')['useSubscribeMessage']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabbar: UnwrapRef<typeof import('./composables/useTabbar')['useTabbar']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useTheme: UnwrapRef<typeof import('./composables/useTheme')['useTheme']>
readonly useThemeStore: UnwrapRef<typeof import('./store/themeStore')['useThemeStore']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('@wot-ui/ui')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useUserStore: UnwrapRef<typeof import('./store/userStore')['useUserStore']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWatermark: UnwrapRef<typeof import('./composables/useWatermark')['useWatermark']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly useWxLogin: UnwrapRef<typeof import('./composables/useWxLogin')['useWxLogin']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
+79
View File
@@ -0,0 +1,79 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by vite-plugin-uni-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
GlobalDialog: typeof import('./components/GlobalDialog.vue')['default']
GlobalLoading: typeof import('./components/GlobalLoading.vue')['default']
GlobalMessage: typeof import('./components/GlobalMessage.vue')['default']
GlobalToast: typeof import('./components/GlobalToast.vue')['default']
PrivacyPopup: typeof import('./components/PrivacyPopup.vue')['default']
SkeletonPage: typeof import('./components/SkeletonPage.vue')['default']
StatusBadge: typeof import('./components/StatusBadge.vue')['default']
UniEcharts: typeof import('uni-echarts')['UniEcharts']
WdActionSheet: typeof import('@wot-ui/ui/components/wd-action-sheet/wd-action-sheet.vue')['WdActionSheet']
WdAvatar: typeof import('@wot-ui/ui/components/wd-avatar/wd-avatar.vue')['WdAvatar']
WdBacktop: typeof import('@wot-ui/ui/components/wd-backtop/wd-backtop.vue')['WdBacktop']
WdBadge: typeof import('@wot-ui/ui/components/wd-badge/wd-badge.vue')['WdBadge']
WdButton: typeof import('@wot-ui/ui/components/wd-button/wd-button.vue')['WdButton']
WdCell: typeof import('@wot-ui/ui/components/wd-cell/wd-cell.vue')['WdCell']
WdCellGroup: typeof import('@wot-ui/ui/components/wd-cell-group/wd-cell-group.vue')['WdCellGroup']
WdCheckbox: typeof import('@wot-ui/ui/components/wd-checkbox/wd-checkbox.vue')['WdCheckbox']
WdCol: typeof import('@wot-ui/ui/components/wd-col/wd-col.vue')['WdCol']
WdCollapse: typeof import('@wot-ui/ui/components/wd-collapse/wd-collapse.vue')['WdCollapse']
WdCollapseItem: typeof import('@wot-ui/ui/components/wd-collapse-item/wd-collapse-item.vue')['WdCollapseItem']
WdConfigProvider: typeof import('@wot-ui/ui/components/wd-config-provider/wd-config-provider.vue')['WdConfigProvider']
WdCountTo: typeof import('@wot-ui/ui/components/wd-count-to/wd-count-to.vue')['WdCountTo']
WdDialog: typeof import('@wot-ui/ui/components/wd-dialog/wd-dialog.vue')['WdDialog']
WdDivider: typeof import('@wot-ui/ui/components/wd-divider/wd-divider.vue')['WdDivider']
WdDropMenu: typeof import('@wot-ui/ui/components/wd-drop-menu/wd-drop-menu.vue')['WdDropMenu']
WdDropMenuItem: typeof import('@wot-ui/ui/components/wd-drop-menu-item/wd-drop-menu-item.vue')['WdDropMenuItem']
WdEmpty: typeof import('@wot-ui/ui/components/wd-empty/wd-empty.vue')['WdEmpty']
WdFab: typeof import('@wot-ui/ui/components/wd-fab/wd-fab.vue')['WdFab']
WdForm: typeof import('@wot-ui/ui/components/wd-form/wd-form.vue')['WdForm']
WdFormItem: typeof import('@wot-ui/ui/components/wd-form-item/wd-form-item.vue')['WdFormItem']
WdGap: typeof import('@wot-ui/ui/components/wd-gap/wd-gap.vue')['WdGap']
WdGrid: typeof import('@wot-ui/ui/components/wd-grid/wd-grid.vue')['WdGrid']
WdGridItem: typeof import('@wot-ui/ui/components/wd-grid-item/wd-grid-item.vue')['WdGridItem']
WdIcon: typeof import('@wot-ui/ui/components/wd-icon/wd-icon.vue')['WdIcon']
WdImagePreview: typeof import('@wot-ui/ui/components/wd-image-preview/wd-image-preview.vue')['WdImagePreview']
WdImg: typeof import('@wot-ui/ui/components/wd-img/wd-img.vue')['WdImg']
WdInput: typeof import('@wot-ui/ui/components/wd-input/wd-input.vue')['WdInput']
WdLoading: typeof import('@wot-ui/ui/components/wd-loading/wd-loading.vue')['WdLoading']
WdNavbar: typeof import('@wot-ui/ui/components/wd-navbar/wd-navbar.vue')['WdNavbar']
WdNoticeBar: typeof import('@wot-ui/ui/components/wd-notice-bar/wd-notice-bar.vue')['WdNoticeBar']
WdNotify: typeof import('@wot-ui/ui/components/wd-notify/wd-notify.vue')['WdNotify']
WdPicker: typeof import('@wot-ui/ui/components/wd-picker/wd-picker.vue')['WdPicker']
WdPopup: typeof import('@wot-ui/ui/components/wd-popup/wd-popup.vue')['WdPopup']
WdRadio: typeof import('@wot-ui/ui/components/wd-radio/wd-radio.vue')['WdRadio']
WdRadioGroup: typeof import('@wot-ui/ui/components/wd-radio-group/wd-radio-group.vue')['WdRadioGroup']
WdRow: typeof import('@wot-ui/ui/components/wd-row/wd-row.vue')['WdRow']
WdSearch: typeof import('@wot-ui/ui/components/wd-search/wd-search.vue')['WdSearch']
WdSkeleton: typeof import('@wot-ui/ui/components/wd-skeleton/wd-skeleton.vue')['WdSkeleton']
WdSlider: typeof import('@wot-ui/ui/components/wd-slider/wd-slider.vue')['WdSlider']
WdSlideVerify: typeof import('@wot-ui/ui/components/wd-slide-verify/wd-slide-verify.vue')['WdSlideVerify']
WdStep: typeof import('@wot-ui/ui/components/wd-step/wd-step.vue')['WdStep']
WdSteps: typeof import('@wot-ui/ui/components/wd-steps/wd-steps.vue')['WdSteps']
WdSwipeAction: typeof import('@wot-ui/ui/components/wd-swipe-action/wd-swipe-action.vue')['WdSwipeAction']
WdSwiper: typeof import('@wot-ui/ui/components/wd-swiper/wd-swiper.vue')['WdSwiper']
WdSwitch: typeof import('@wot-ui/ui/components/wd-switch/wd-switch.vue')['WdSwitch']
WdTab: typeof import('@wot-ui/ui/components/wd-tab/wd-tab.vue')['WdTab']
WdTabbar: typeof import('@wot-ui/ui/components/wd-tabbar/wd-tabbar.vue')['WdTabbar']
WdTabbarItem: typeof import('@wot-ui/ui/components/wd-tabbar-item/wd-tabbar-item.vue')['WdTabbarItem']
WdTabs: typeof import('@wot-ui/ui/components/wd-tabs/wd-tabs.vue')['WdTabs']
WdTag: typeof import('@wot-ui/ui/components/wd-tag/wd-tag.vue')['WdTag']
WdText: typeof import('@wot-ui/ui/components/wd-text/wd-text.vue')['WdText']
WdTextarea: typeof import('@wot-ui/ui/components/wd-textarea/wd-textarea.vue')['WdTextarea']
WdToast: typeof import('@wot-ui/ui/components/wd-toast/wd-toast.vue')['WdToast']
WdUpload: typeof import('@wot-ui/ui/components/wd-upload/wd-upload.vue')['WdUpload']
WdWatermark: typeof import('@wot-ui/ui/components/wd-watermark/wd-watermark.vue')['WdWatermark']
}
}
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import { deepClone, isFunction } from '@wot-ui/ui/common/util'
const { dialogOptions, currentPage } = storeToRefs(useGlobalDialog())
const dialog = useDialog('globalDialog')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => dialogOptions.value, (newVal) => {
if (newVal) {
if (currentPage.value === currentPath) {
const option = deepClone(newVal)
dialog.show(option).then((res) => {
if (isFunction(option.success)) {
option.success(res)
}
}).catch((err) => {
if (isFunction(option.fail)) {
option.fail(err)
}
})
}
}
else {
dialog.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-dialog v-if="hackAlipayVisible" selector="globalDialog" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-dialog selector="globalDialog" />
<!-- #endif -->
</template>
@@ -0,0 +1,46 @@
<script lang="ts" setup>
const { loadingOptions, currentPage } = storeToRefs(useGlobalLoading())
const { close: closeGlobalLoading } = useGlobalLoading()
const loading = useToast('globalLoading')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => loadingOptions.value, (newVal) => {
if (newVal && newVal.show) {
if (currentPage.value === currentPath) {
loading.loading(loadingOptions.value)
}
}
else {
loading.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-toast v-if="hackAlipayVisible" selector="globalLoading" :closed="closeGlobalLoading" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-toast selector="globalLoading" :closed="closeGlobalLoading" />
<!-- #endif -->
</template>
@@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { GlobalMessageOptions } from '@/composables/useGlobalMessage'
import { useI18n } from 'vue-i18n'
import { useGlobalMessage } from '@/composables/useGlobalMessage'
const { t } = useI18n()
const { messageOptions, currentPage } = storeToRefs(useGlobalMessage())
const currentPath = getCurrentPath()
watch(() => messageOptions.value, (newVal) => {
if (newVal && currentPage.value === currentPath) {
const option: GlobalMessageOptions = { ...newVal }
uni.showModal({
title: option.title || '',
content: option.content || '',
showCancel: option.showCancel ?? (option.type === 'confirm'),
confirmText: option.confirmText || t('common.confirm'),
cancelText: option.cancelText || t('common.cancel'),
success: (res) => {
option.success?.({ confirm: res.confirm, cancel: res.cancel })
},
fail: (err) => {
option.fail?.(err)
},
})
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<view />
</template>
@@ -0,0 +1,46 @@
<script lang="ts" setup>
const { toastOptions, currentPage } = storeToRefs(useGlobalToast())
const { close: closeGlobalToast } = useGlobalToast()
const toast = useToast('globalToast')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => toastOptions.value, (newVal) => {
if (newVal && newVal.show) {
if (currentPage.value === currentPath) {
toast.show(toastOptions.value)
}
}
else {
toast.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-toast v-if="hackAlipayVisible" selector="globalToast" :closed="closeGlobalToast" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-toast selector="globalToast" :closed="closeGlobalToast" />
<!-- #endif -->
</template>
@@ -0,0 +1,176 @@
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
interface Props {
title?: string // 标题
desc?: string // 描述
subDesc?: string // 字描述
protocol?: string // 协议名称
}
withDefaults(defineProps<Props>(), {
title: '用户隐私保护提示',
desc: '感谢您使用本应用,您使用本应用的服务之前请仔细阅读并同意',
subDesc: '。当您点击同意并开始时用产品服务时,即表示你已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用相应服务。',
protocol: '《用户隐私保护指引》',
})
const emit = defineEmits(['agree', 'disagree'])
const showPopup = ref<boolean>(false) // 是否展示popup
const privacyResolves = ref(new Set()) // onNeedPrivacyAuthorization的reslove
function privacyHandler(resolve: any) {
showPopup.value = true
privacyResolves.value.add(resolve)
}
onBeforeMount(() => {
// 注册监听
if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization((resolve: any) => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}
})
/**
* 同意隐私协议
*/
function handleAgree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'agree',
buttonId: 'agree-btn',
})
})
privacyResolves.value.clear()
emit('agree')
}
/**
* 拒绝隐私协议
*/
function handleDisagree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'disagree',
})
})
privacyResolves.value.clear()
}
/**
* 打开隐私协议
*/
function openPrivacyContract() {
wx.openPrivacyContract({})
}
/**
* 弹出框关闭时清空
*/
function handleClose() {
privacyResolves.value.clear()
}
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<view>
<wd-popup v-model="showPopup" :close-on-click-modal="false" custom-class="wd-privacy-popup" @close="handleClose">
<view class="wd-privacy-popup__header">
<!-- 标题 -->
<view class="wd-picker__title">
{{ title }}
</view>
</view>
<view class="wd-privacy-popup__container">
<wd-text :text="desc" />
<wd-text class="wd-privacy-popup__container-protocol" :text="protocol" @click="openPrivacyContract" />
<wd-text :text="subDesc" />
</view>
<view class="wd-privacy-popup__footer">
<button id="disagree-btn" class="is-block is-round is-medium is-plain wd-privacy-popup__footer-disagree wd-button" @click="handleDisagree">
拒绝
</button>
<button
id="agree-btn"
class="wd-button is-block is-round is-medium is-primary wd-privacy-popup__footer-agree"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>
同意
</button>
</view>
</wd-popup>
</view>
</template>
<style lang="scss">
// 非 scoped:wd-popup 通过 custom-class 传入根类名,弹窗挂载层级不受组件作用域约束,
// :deep() 在小程序端对 virtualHost 组件不可靠,改用全局类名直接命中(H5/MP 均生效)
.wd-privacy-popup {
width: 600rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 32rpx;
overflow: hidden;
}
</style>
<style lang="scss" scoped>
.wd-privacy-popup {
&__header {
width: 100%;
height: 128rpx;
line-height: 128rpx;
@apply wot-text-text-main;
font-size: 30rpx;
padding: 0 12rpx;
box-sizing: border-box;
}
&__container {
width: 100%;
box-sizing: border-box;
padding: 0 12rpx;
margin-bottom: 32rpx;
font-size: 28rpx;
line-height: 1.8;
@apply wot-text-text-secondary;
text-align: left;
font-weight: 550;
&-protocol {
@apply wot-text-primary-6;
}
}
&__footer {
display: flex;
justify-content: space-between;
padding-bottom: 36rpx;
/* 组件 wxss 不允许标签选择器(微信小程序限制),改用类选择器 */
&-disagree,
&-agree {
border: none;
outline: none;
}
}
}
</style>
@@ -0,0 +1,44 @@
<script setup lang="ts">
/**
* 页面骨架屏组件
* 在数据加载时展示占位内容,提升感知性能
* 基于 wd-skeleton 封装
*/
const props = withDefaults(defineProps<{
/** 显示行数(列表行) */
rows?: number
/** 是否显示搜索栏骨架 */
search?: boolean
/** 是否显示操作栏骨架 */
action?: boolean
}>(), {
rows: 5,
})
/** 搜索栏占位行 */
const searchRowCol = [{ type: 'rect' as const, height: '64rpx', borderRadius: '12rpx' }]
/** 列表行占位:圆形头像 + 文本块 + 右侧徽章 */
const listRowCol = [
[
{ type: 'circle' as const, size: '64rpx' },
{ type: 'rect' as const, width: '200rpx', height: '28rpx', marginLeft: '24rpx' },
{ type: 'rect' as const, width: '80rpx', height: '40rpx', borderRadius: '999rpx' },
],
]
</script>
<template>
<view class="p-sm">
<!-- 搜索栏骨架 -->
<view v-if="search" class="admin-card mb-md p-md">
<wd-skeleton :row-col="searchRowCol" :loading="true" animation="gradient" />
</view>
<!-- 多行列表骨架 -->
<view class="admin-card p-sm">
<view v-for="i in props.rows" :key="i" class="gap-md p-md">
<wd-skeleton :row-col="listRowCol" :loading="true" animation="gradient" />
</view>
</view>
</view>
</template>
+133
View File
@@ -0,0 +1,133 @@
<script setup lang="ts">
/**
* 状态徽标组件
* 用于统一展示启用/禁用/草稿/发布等状态
*/
import { useI18n } from 'vue-i18n'
const props = withDefaults(defineProps<{
status?: string | boolean | number
/** 自定义映射: { '0': 'enabled', '1': 'disabled', true: 'enabled', false: 'disabled' } */
map?: Record<string, string>
/** 自定义标签文本 */
label?: string
/** 仅显示圆点 */
dot?: boolean
}>(), {
status: '',
label: '',
dot: false,
})
const { t } = useI18n()
const statusMap: Record<string, { labelKey: string, cls: string }> = {
enabled: { labelKey: 'common.status.enabled', cls: 'status-badge--enabled' },
disabled: { labelKey: 'common.status.disabled', cls: 'status-badge--disabled' },
draft: { labelKey: 'common.status.draft', cls: 'status-badge--draft' },
published: { labelKey: 'common.status.published', cls: 'status-badge--primary' },
archived: { labelKey: 'common.status.archived', cls: 'status-badge--disabled' },
active: { labelKey: 'common.status.active', cls: 'status-badge--primary' },
success: { labelKey: 'common.status.success', cls: 'status-badge--enabled' },
failed: { labelKey: 'common.status.failed', cls: 'status-badge--danger' },
pending: { labelKey: 'common.status.pending', cls: 'status-badge--draft' },
processing: { labelKey: 'common.status.processing', cls: 'status-badge--primary' },
completed: { labelKey: 'common.status.completed', cls: 'status-badge--enabled' },
closed: { labelKey: 'common.status.closed', cls: 'status-badge--disabled' },
deprecated: { labelKey: 'common.status.deprecated', cls: 'status-badge--danger' },
expired: { labelKey: 'common.status.expired', cls: 'status-badge--danger' },
}
function resolve(input: string | boolean | number): { label: string, cls: string } {
// 1. 自定义 map 优先(业务状态:如 notices 0=草稿/1=已发布、tickets 0=待处理/1=处理中)
const mappedKey = props.map?.[String(input)]
if (mappedKey && statusMap[mappedKey])
return { label: t(statusMap[mappedKey].labelKey), cls: statusMap[mappedKey].cls }
// 2. 后端通用规范: 0=启用, 1=禁用
if (input === true || input === 'true' || input === '1' || input === 1 || input === '0' || input === 0 || input === false || input === 'false') {
const isEnabled = input === 0 || input === '0' || input === true || input === 'true'
return isEnabled
? { label: t('common.status.enabled'), cls: 'status-badge--enabled' }
: { label: t('common.status.disabled'), cls: 'status-badge--disabled' }
}
// 3. 内置状态映射兜底
const entry = statusMap[String(input)]
return entry ? { label: t(entry.labelKey), cls: entry.cls } : { label: String(input), cls: 'status-badge--disabled' }
}
</script>
<template>
<text v-if="dot" class="status-badge--dot" :class="resolve(status).cls" />
<text v-else class="status-badge" :class="resolve(status).cls">
<text class="status-badge--dot" :class="resolve(status).cls" />
{{ label || resolve(status).label }}
</text>
</template>
<style lang="scss">
/* 状态徽标全局样式(非 scoped,供页面复用类名)
* 颜色使用 wot-ui 语义变量,自动适配亮/暗主题 */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 14rpx;
border-radius: 999rpx;
font-size: 20rpx;
line-height: 1.4;
}
.status-badge--dot {
display: inline-block;
width: 10rpx;
height: 10rpx;
border-radius: 50%;
flex-shrink: 0;
background: currentColor;
}
/* 各状态仅定义文字色,背景色由带外层 .status-badge 的组合规则提供,
* 使纯圆点模式只显示纯色圆点 */
.status-badge--enabled {
@apply wot-text-success-main;
}
.status-badge.status-badge--enabled {
@apply wot-bg-success-surface;
}
.status-badge--disabled {
@apply wot-text-text-auxiliary;
}
.status-badge.status-badge--disabled {
@apply wot-bg-filled-content;
}
.status-badge--primary {
@apply wot-text-primary-6;
}
.status-badge.status-badge--primary {
@apply wot-bg-primary-1;
}
.status-badge--danger {
@apply wot-text-danger-main;
}
.status-badge.status-badge--danger {
@apply wot-bg-danger-surface;
}
/* failed 为 danger 的别名(menus 页菜单类型使用) */
.status-badge--failed {
@apply wot-text-danger-main;
}
.status-badge.status-badge--failed {
@apply wot-bg-danger-surface;
}
.status-badge--draft {
@apply wot-text-text-secondary;
}
.status-badge.status-badge--draft {
@apply wot-bg-filled-content;
}
</style>
+161
View File
@@ -0,0 +1,161 @@
/*
* @Author: weisheng
* @Date: 2025-09-02 09:42:36
* @LastEditTime: 2026-04-10 11:01:37
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/src/composables/types/theme.ts
* 记得注释
*/
import type { ConfigProviderThemeVars } from '@wot-ui/ui'
export type PrimaryShadeKey
= | 'primary1'
| 'primary2'
| 'primary3'
| 'primary4'
| 'primary5'
| 'primary6'
| 'primary7'
| 'primary8'
| 'primary9'
| 'primary10'
export type ThemePrimaryShades = Record<PrimaryShadeKey, string>
/**
* 主题色选项接口
*/
export interface ThemeColorOption {
/** i18n key(如 theme.colors.blue) */
labelKey: string
value: string
// 主色 6,用于列表和当前主题色圆点展示
primary: string
// 完整主色阶,用于注入 ConfigProvider 主题变量
primaryShades: ThemePrimaryShades
}
/**
* 主题类型
*/
export type ThemeMode = 'light' | 'dark'
/**
* 主题状态接口
*/
export interface ThemeState {
theme: ThemeMode
followSystem: boolean
hasUserSet: boolean
currentThemeColor: ThemeColorOption
themeVars: ConfigProviderThemeVars
}
/**
* 预定义的主题色选项
*/
export const themeColorOptions: ThemeColorOption[] = [
{
labelKey: 'theme.colors.blue',
value: 'blue',
primary: '#1C64FD',
primaryShades: {
primary1: '#F5F8FF',
primary2: '#E5EDFF',
primary3: '#B8CFFF',
primary4: '#7CA4FF',
primary5: '#4480FF',
primary6: '#1C64FD',
primary7: '#164ED1',
primary8: '#1341AD',
primary9: '#0F3285',
primary10: '#0A235C',
},
},
{
labelKey: 'theme.colors.orange',
value: 'orange',
primary: '#FF7D00',
primaryShades: {
primary1: '#FFF7F0',
primary2: '#FFEAD6',
primary3: '#FFD0A8',
primary4: '#FFB06E',
primary5: '#FF9338',
primary6: '#FF7D00',
primary7: '#D96800',
primary8: '#B35600',
primary9: '#8A4200',
primary10: '#5F2D00',
},
},
{
labelKey: 'theme.colors.green',
value: 'green',
primary: '#07C160',
primaryShades: {
primary1: '#F1FCF6',
primary2: '#DCF7E8',
primary3: '#B4EFD0',
primary4: '#7FE2AF',
primary5: '#42D28A',
primary6: '#07C160',
primary7: '#049F4F',
primary8: '#028241',
primary9: '#016532',
primary10: '#014825',
},
},
{
labelKey: 'theme.colors.pink',
value: 'pink',
primary: '#FF69B4',
primaryShades: {
primary1: '#FFF2F9',
primary2: '#FFE2F1',
primary3: '#FFC2E3',
primary4: '#FF99D1',
primary5: '#FF7FC2',
primary6: '#FF69B4',
primary7: '#E84E9F',
primary8: '#CC3F8A',
primary9: '#A9336F',
primary10: '#7A2450',
},
},
{
labelKey: 'theme.colors.purple',
value: 'purple',
primary: '#8A2BE2',
primaryShades: {
primary1: '#F7F2FF',
primary2: '#EEE2FF',
primary3: '#D9BDFF',
primary4: '#BC8DFF',
primary5: '#A05EFF',
primary6: '#8A2BE2',
primary7: '#7423BF',
primary8: '#5E1D9C',
primary9: '#491679',
primary10: '#321056',
},
},
{
labelKey: 'theme.colors.red',
value: 'red',
primary: '#FF4757',
primaryShades: {
primary1: '#FFF3F4',
primary2: '#FFE3E5',
primary3: '#FFC0C6',
primary4: '#FF909A',
primary5: '#FF6672',
primary6: '#FF4757',
primary7: '#DB3445',
primary8: '#B7293A',
primary9: '#931E2E',
primary10: '#6F1421',
},
},
]
+118
View File
@@ -0,0 +1,118 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/userStore'
export interface AiChatStreamHandlers {
/** 收到内容分片(追加到当前 AI 消息) */
onChunk?: (text: string) => void
/** 生成结束([DONE] / [STOPPED]) */
onDone?: () => void
/** 连接或生成错误 */
onError?: (message: string) => void
}
/** 构建 AI 对话 WebSocket 地址:优先 VITE_APP_WS_ENDPOINT,否则从 API 域名推导(http→ws) */
function buildChatWsUrl(): string {
const userStore = useUserStore()
const token = userStore.getAccessToken() || ''
let wsBase = import.meta.env.VITE_APP_WS_ENDPOINT || ''
if (!wsBase) {
const apiBase = import.meta.env.VITE_API_BASE_URL || ''
wsBase = apiBase.replace(/^http/, 'ws')
}
const apiPrefix = import.meta.env.VITE_APP_BASE_API || '/api/v1'
// token 走 query(小程序 WebSocket 不支持自定义 subprotocol,后端已兼容 ?token=)
return `${wsBase}${apiPrefix}/ai/chat/ws?token=${encodeURIComponent(token)}`
}
/**
* AI 对话流式输出(WebSocket)
* 协议(对应后端 /ai/chat/ws):
* - 发送:{"message": "...", "session_id": "..."} | {"action": "stop"}
* - 接收:内容分片(纯文本)→ [DONE] 结束 / [STOPPED] 停止确认
*/
export function useAiChat() {
const { t } = useI18n()
const isStreaming = ref(false)
let socketTask: ReturnType<typeof uni.connectSocket> | null = null
let pendingOpen: Promise<void> | null = null
let resolveOpen: (() => void) | null = null
let closedByUser = false
let handlers: AiChatStreamHandlers = {}
function ensureConnected(): Promise<void> {
if (socketTask)
return Promise.resolve()
closedByUser = false
pendingOpen = new Promise<void>((resolve) => {
resolveOpen = resolve
})
const task = uni.connectSocket({ url: buildChatWsUrl(), complete: () => {} })
socketTask = task
task.onOpen(() => resolveOpen?.())
task.onMessage((res) => {
const text = typeof res.data === 'string' ? res.data : ''
if (text === '[DONE]' || text === '[STOPPED]') {
isStreaming.value = false
handlers.onDone?.()
}
else if (text) {
handlers.onChunk?.(text)
}
})
task.onError((err) => {
resolveOpen?.()
isStreaming.value = false
handlers.onError?.(err?.errMsg || t('chat.wsConnectFailed'))
})
task.onClose(() => {
socketTask = null
if (!closedByUser) {
isStreaming.value = false
handlers.onError?.(t('chat.wsDisconnected'))
}
})
return pendingOpen
}
/** 发送一条流式对话消息(自动建立/复用连接) */
async function sendMessage(
payload: { message: string, session_id?: string | null },
handler: AiChatStreamHandlers,
): Promise<void> {
handlers = handler
isStreaming.value = true
try {
await ensureConnected()
socketTask?.send({
data: JSON.stringify({ message: payload.message, session_id: payload.session_id || undefined }),
})
}
catch {
isStreaming.value = false
handlers.onError?.(t('chat.wsSendFailed'))
}
}
/** 停止当前生成(后端停止后返回 [STOPPED]) */
function stop() {
if (!isStreaming.value)
return
try {
socketTask?.send({ data: JSON.stringify({ action: 'stop' }) })
}
catch { /* 忽略发送失败 */ }
}
/** 关闭连接(页面卸载时调用) */
function close() {
closedByUser = true
isStreaming.value = false
if (socketTask) {
socketTask.close({ code: 1000, reason: 'page unload' })
socketTask = null
}
}
return { isStreaming, sendMessage, stop, close }
}
@@ -0,0 +1,106 @@
import type { DialogOptions, DialogResult } from '@wot-ui/ui/components/wd-dialog/types'
import { defineStore } from 'pinia'
export type GlobalDialogOptions = DialogOptions & {
success?: (res: DialogResult) => void
fail?: (res: DialogResult) => void
}
interface GlobalDialog {
dialogOptions: GlobalDialogOptions | null
currentPage: string
}
type DialogType = NonNullable<DialogOptions['type']>
function isButtonPropsObject(value: unknown): value is Record<string, any> {
return value !== null && CommonUtil.isObj(value)
}
function normalizeButtonProps(props: unknown, text?: string) {
if (props === null) {
return null
}
if (isButtonPropsObject(props)) {
return {
...props,
...(text ? { text } : {}),
}
}
if (CommonUtil.isString(props) || text) {
return {
text: text || props,
}
}
if (props === undefined) {
return {}
}
return props
}
function withDefaultTypeOptions(option: GlobalDialogOptions, type?: DialogType): GlobalDialogOptions {
const next: GlobalDialogOptions = {
...option,
...(type ? { type } : {}),
}
if (next.showCancelButton === undefined) {
if (next.type === 'alert') {
next.showCancelButton = false
}
else if (next.type === 'confirm' || next.type === 'prompt') {
next.showCancelButton = true
}
}
return next
}
function normalizeDialogOptions(option: GlobalDialogOptions, type?: DialogType): GlobalDialogOptions {
const next = withDefaultTypeOptions(option, type)
next.confirmButtonProps = normalizeButtonProps(next.confirmButtonProps, next.confirmButtonText) as DialogOptions['confirmButtonProps']
if (next.showCancelButton === false) {
next.cancelButtonProps = null
}
else if (next.showCancelButton === true || next.cancelButtonProps !== undefined || next.cancelButtonText) {
next.cancelButtonProps = normalizeButtonProps(next.cancelButtonProps, next.cancelButtonText) as DialogOptions['cancelButtonProps']
}
return next
}
function normalizeOption(option: GlobalDialogOptions | string, type?: DialogType): GlobalDialogOptions {
return normalizeDialogOptions(CommonUtil.isString(option) ? { title: option } : option, type)
}
export const useGlobalDialog = defineStore('global-Dialog', {
state: (): GlobalDialog => ({
dialogOptions: null,
currentPage: '',
}),
actions: {
show(option: GlobalDialogOptions | string, type?: DialogType) {
this.currentPage = getCurrentPath()
this.dialogOptions = normalizeOption(option, type)
},
alert(option: GlobalDialogOptions | string) {
this.show(option, 'alert')
},
confirm(option: GlobalDialogOptions | string) {
this.show(option, 'confirm')
},
prompt(option: GlobalDialogOptions | string) {
this.show(option, 'prompt')
},
close() {
this.dialogOptions = null
this.currentPage = ''
},
},
})
@@ -0,0 +1,36 @@
import type { ToastOptions } from '@wot-ui/ui/components/wd-toast/types'
import { defineStore } from 'pinia'
interface GlobalLoading {
loadingOptions: ToastOptions
currentPage: string
}
const defaultOptions: ToastOptions = {
show: false,
}
export const useGlobalLoading = defineStore('global-loading', {
state: (): GlobalLoading => ({
loadingOptions: defaultOptions,
currentPage: '',
}),
getters: {},
actions: {
// 加载提示
loading(option: ToastOptions | string) {
this.currentPage = getCurrentPath()
this.loadingOptions = CommonUtil.deepMerge({
iconName: 'loading',
duration: 0,
cover: true,
position: 'middle',
show: true,
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions
},
// 关闭Toast
close() {
this.loadingOptions = defaultOptions
this.currentPage = ''
},
},
})
@@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
export interface GlobalMessageOptions {
title?: string
content?: string
type?: 'alert' | 'confirm'
showCancel?: boolean
confirmText?: string
cancelText?: string
success?: (res: { confirm: boolean, cancel: boolean }) => void
fail?: (err: unknown) => void
}
/**
* 全局消息 store:GlobalMessage 组件监听 messageOptions,
* 仅当触发页面与 currentPage 一致时展示,避免跨页误弹
*/
export const useGlobalMessage = defineStore('global-message', {
state: () => ({
messageOptions: null as GlobalMessageOptions | null,
currentPage: '',
}),
actions: {
show(options: GlobalMessageOptions) {
this.currentPage = getCurrentPath()
this.messageOptions = options
},
close() {
this.messageOptions = null
},
},
})
@@ -0,0 +1,65 @@
import type { ToastOptions } from '@wot-ui/ui/components/wd-toast/types'
import { defineStore } from 'pinia'
interface GlobalToast {
toastOptions: ToastOptions
currentPage: string
}
const defaultOptions: ToastOptions = {
duration: 2000,
show: false,
}
export const useGlobalToast = defineStore('global-toast', {
state: (): GlobalToast => ({
toastOptions: defaultOptions,
currentPage: '',
}),
getters: {},
actions: {
// 打开Toast
show(option: ToastOptions | string) {
this.currentPage = getCurrentPath()
const options = CommonUtil.deepMerge(defaultOptions, typeof option === 'string' ? { msg: option } : option) as ToastOptions
this.toastOptions = CommonUtil.deepMerge(options, {
show: true,
position: options.position || 'middle',
}) as ToastOptions
},
// 成功提示
success(option: ToastOptions | string) {
this.show(CommonUtil.deepMerge({
iconName: 'success',
iconColor: '#07c160',
duration: 1500,
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
},
// 关闭提示
error(option: ToastOptions | string) {
this.show(CommonUtil.deepMerge({
iconName: 'error',
iconColor: '#fa5151',
direction: 'vertical',
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
},
// 常规提示
info(option: ToastOptions | string) {
this.show(CommonUtil.deepMerge({
iconName: 'info',
iconColor: '#10aeff',
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
},
// 警告提示
warning(option: ToastOptions | string) {
this.show(CommonUtil.deepMerge({
iconName: 'warning',
iconColor: '#ff9f0a',
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
},
// 关闭Toast
close() {
this.toastOptions = defaultOptions
this.currentPage = ''
},
},
})
@@ -0,0 +1,17 @@
import { onShow } from '@dcloudio/uni-app'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
/**
* 页面导航栏标题国际化
* definePage 的 navigationBarTitleText 是编译期静态文案,
* 这里在 onShow 时按当前语言动态覆盖标题,语言切换时也能即时刷新。
*/
export function useI18nNavTitle(key: string) {
const { t, locale } = useI18n()
function apply() {
uni.setNavigationBarTitle({ title: t(key) })
}
onShow(apply)
watch(locale, apply)
}
+101
View File
@@ -0,0 +1,101 @@
import type { AlovaGenerics, Method } from 'alova'
import { usePagination } from 'alova/client'
import { computed, ref } from 'vue'
export interface ListPageParams {
page_no: number
page_size: number
}
export interface ListPageOptions<AG extends AlovaGenerics = AlovaGenerics> {
fetcher: (params: ListPageParams) => Method<AG>
pageSize?: number
onError?: (error: unknown) => void
}
/**
* 通用列表分页逻辑(基于 alova usePagination 封装)
* 底层使用 alova 状态管理,统一暴露 list/total/loading/error 三态与翻页动作
* 说明:fetcher 需返回 alova Method(http 层 get/post 均为 Method 实例),
* 请求发送与错误处理完全交给 alova hook 管理
*/
export function useListPage<T, AG extends AlovaGenerics = AlovaGenerics>(options: ListPageOptions<AG>) {
const { fetcher, pageSize = 10, onError } = options
const pageParams = ref<ListPageParams>({ page_no: 1, page_size: pageSize })
const {
data,
total,
loading,
error,
send,
onError: onPageError,
} = usePagination<AG, T[], any[]>(
(pageNo: number, pageSizeNo: number) => fetcher({ page_no: pageNo, page_size: pageSizeNo }),
{
initialPage: 1,
initialPageSize: pageSize,
// 由页面手动触发(onLoad/搜索/翻页),禁用自动监听与预加载
immediate: false,
watchingStates: [],
preloadNextPage: false,
preloadPreviousPage: false,
// 列表页总是请求最新数据,禁用 alova 响应缓存
force: true,
// 响应已由 http 层 responded 解包为业务结构 { list, total }
data: res => (res as PageResult<T>).list ?? [],
total: res => (res as PageResult<T>).total ?? 0,
},
)
onPageError(({ error: e }) => {
onError?.(e)
})
/** 加载当前页数据 */
async function loadData() {
try {
await send(pageParams.value.page_no, pageParams.value.page_size)
}
catch {
// 错误已由 onPageError → onError 统一处理,避免未捕获 Promise 告警
}
finally {
// 收起下拉刷新指示器(onPullDownRefresh → loadData 场景;非刷新场景调用无害)
uni.stopPullDownRefresh()
}
}
/** 上一页 */
async function loadPrev() {
if (pageParams.value.page_no <= 1)
return
pageParams.value.page_no -= 1
await loadData()
}
/** 下一页 */
async function loadNext() {
pageParams.value.page_no += 1
await loadData()
}
/** 跳回第一页(搜索/重置时使用) */
async function toFirst() {
pageParams.value.page_no = 1
await loadData()
}
return {
list: computed<T[]>(() => data.value ?? []),
total: computed<number>(() => total.value ?? 0),
loading,
error,
pageParams,
loadData,
loadPrev,
loadNext,
toFirst,
}
}
+62
View File
@@ -0,0 +1,62 @@
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
/** 分享配置 */
interface ShareConfig {
/** 分享标题 */
title: string
/** 分享路径(带 / 开头,例如 /pages/index/index) */
path?: string
/** 分享图片 URL(可选,不传则使用页面截图) */
imageUrl?: string
}
/** 默认分享路径(全局兜底) */
const DEFAULT_SHARE_PATH = '/pages/index/index'
/**
* 全局分享 composable
*
* 在页面 setup 中调用即可注册 onShareAppMessage + onShareTimeline。
* 微信小程序中,页面必须调用此 composable 才能开启右上角"分享"按钮。
*
* @example
* ```ts
* // 静态分享
* useShare({ title: '工作台', path: '/pages/work/index' })
*
* // 动态分享(标题随数据变化)
* const shareTitle = computed(() => `${userInfo.value?.name || '管理员'} 邀请你使用`)
* useShare(() => ({ title: shareTitle.value, path: '/pages/index/index' }))
* ```
*/
export function useShare(configOrGetter?: ShareConfig | (() => ShareConfig)) {
const { t } = useI18n()
const getConfig = (): ShareConfig => {
// 未指定分享标题时使用全局默认(随当前语言)
const defaults: ShareConfig = { title: t('common.shareDefaultTitle'), path: DEFAULT_SHARE_PATH }
if (typeof configOrGetter === 'function')
return { ...defaults, ...configOrGetter() }
if (configOrGetter)
return { ...defaults, ...configOrGetter }
return defaults
}
onShareAppMessage(() => {
const cfg = getConfig()
return {
title: cfg.title,
path: cfg.path || DEFAULT_SHARE_PATH,
imageUrl: cfg.imageUrl,
}
})
onShareTimeline(() => {
const cfg = getConfig()
return {
title: cfg.title,
query: '', // 朋友圈分享不支持 path,用 query 传参
imageUrl: cfg.imageUrl,
}
})
}
@@ -0,0 +1,144 @@
import { useI18n } from 'vue-i18n'
/**
* 微信订阅消息 composable
*
* 封装 wx.requestSubscribeMessage,在用户操作时请求订阅消息授权。
* 常见场景:工单状态变更、通知公告发布、系统到期提醒等。
*
* 使用前提:
* 1. 在微信公众平台配置订阅消息模板
* 2. 将模板 ID 填入此处常量
* 3. 仅微信小程序端有效
*
* @example
* ```ts
* const { subscribe } = useSubscribeMessage()
*
* // 提交工单时订阅"工单状态变更"通知
* async function submitTicket() {
* await subscribe([TEMPLATE_IDS.TICKET_STATUS])
* // ...提交工单
* }
* ```
*/
/** 订阅消息模板 ID(在微信公众平台 → 订阅消息中配置) */
export const TEMPLATE_IDS = {
/** 工单状态变更通知 */
TICKET_STATUS: '',
/** 通知公告发布提醒 */
NOTICE_PUBLISH: '',
/** 系统到期提醒 */
SYSTEM_EXPIRY: '',
/** 工单回复通知 */
TICKET_REPLY: '',
} as const
/** 订阅消息模板 ID 类型 */
export type TemplateId = keyof typeof TEMPLATE_IDS
/** 订阅结果 */
interface SubscribeResult {
/** 模板 ID */
templateId: string
/** 订阅状态:accept=同意, reject=拒绝, ban=被禁止 */
status: 'accept' | 'reject' | 'ban' | 'filter'
}
/**
* 订阅消息 composable
*/
export function useSubscribeMessage() {
const { t } = useI18n()
/**
* 请求订阅消息授权
*
* @param templateIds 模板 ID 列表(支持传 key 名称或完整 ID)
* @returns 订阅结果数组,每项包含模板 ID 和用户选择状态
*/
async function subscribe(templateIds: Array<TemplateId | string>): Promise<SubscribeResult[]> {
// #ifdef MP-WEIXIN
// 将 key 名称解析为实际模板 ID
const ids = templateIds.map((id) => {
if (id in TEMPLATE_IDS)
return TEMPLATE_IDS[id as TemplateId]
return id
}).filter(Boolean)
if (ids.length === 0) {
console.warn('[useSubscribeMessage] 未配置有效的模板 ID')
return []
}
try {
const res = await uni.requestSubscribeMessage({
tmplIds: ids,
}) as unknown as Record<string, string>
// 微信返回格式: { [templateId]: 'accept' | 'reject' | 'ban' | 'filter' }
const results: SubscribeResult[] = ids.map(templateId => ({
templateId,
status: res[templateId] as SubscribeResult['status'],
}))
// 记录用户选择,可用于后续引导
const accepted = results.filter(r => r.status === 'accept')
if (accepted.length > 0)
console.log(`[useSubscribeMessage] 用户已订阅 ${accepted.length} 个模板`)
return results
}
catch (error) {
console.error('[useSubscribeMessage] 订阅失败', error)
return []
}
// #endif
// #ifndef MP-WEIXIN
return []
// #endif
}
/**
* 检查订阅消息设置状态
* 用户可能在设置中关闭了订阅消息,此方法检测并引导用户开启
*
* @returns 是否可以发送订阅消息
*/
async function checkSubscriptionStatus(): Promise<boolean> {
// #ifdef MP-WEIXIN
try {
const settings = await uni.getSetting({})
const subscription = (settings as any).authSetting?.subscribeMessage
// 如果用户曾经拒绝过,引导去设置页开启
if (subscription === false) {
uni.showModal({
title: t('common.subscribeTitle'),
content: t('common.subscribeContent'),
confirmText: t('common.subscribeGoSetting'),
success: (res) => {
if (res.confirm)
uni.openSetting({})
},
})
return false
}
return true
}
catch {
return true
}
// #endif
// #ifndef MP-WEIXIN
return false
// #endif
}
return {
subscribe,
checkSubscriptionStatus,
TEMPLATE_IDS,
}
}
+53
View File
@@ -0,0 +1,53 @@
export interface TabbarItem {
name: string
value?: number
active: boolean
titleKey: string
icon: string
}
const tabbarItems = ref<TabbarItem[]>([
{ name: 'home', active: true, titleKey: 'common.tab.home', icon: 'home' },
{ name: 'work', active: false, titleKey: 'common.tab.work', icon: 'apps' },
{ name: 'mine', active: false, titleKey: 'common.tab.mine', icon: 'user' },
])
export function useTabbar() {
const tabbarList = computed(() => tabbarItems.value)
const activeTabbar = computed(() => {
const item = tabbarItems.value.find(item => item.active)
return item || tabbarItems.value[0]
})
const getTabbarItemValue = (name: string) => {
const item = tabbarItems.value.find(item => item.name === name)
return item?.value
}
const setTabbarItem = (name: string, value: number) => {
const tabbarItem = tabbarItems.value.find(item => item.name === name)
if (tabbarItem) {
tabbarItem.value = value
}
}
const setTabbarItemActive = (name: string) => {
tabbarItems.value.forEach((item) => {
if (item.name === name) {
item.active = true
}
else {
item.active = false
}
})
}
return {
tabbarList,
activeTabbar,
getTabbarItemValue,
setTabbarItem,
setTabbarItemActive,
}
}
@@ -0,0 +1,16 @@
import { onShow } from '@dcloudio/uni-app'
/**
* tab 页 onShow 时同步底部导航高亮。
* 通过 getCurrentPages 校验当前路由,避免从子页面返回时误触发
* (子页面返回的中间态路由是子页面自身,不会 emit 到错误 tab)。
*/
export function useTabbarActive(route: string, tabKey: string, onActive?: () => void) {
onShow(() => {
const pages = getCurrentPages()
if (pages.length > 0 && pages[pages.length - 1].route === route) {
uni.$emit('updateTabbar', tabKey)
onActive?.()
}
})
}
+128
View File
@@ -0,0 +1,128 @@
import type { ThemeColorOption, ThemeMode } from '@/composables/types/theme'
import { themeColorOptions } from '@/composables/types/theme'
import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemTheme'
/**
* 主题管理组合式API(统一入口)
*
* 功能特性:
* - 支持手动切换暗黑模式
* - 支持主题色选择
* - 支持跟随系统主题
* - 自动同步导航栏颜色
* - 持久化用户设置
*
* @example
* ```vue
* <script setup>
* const {
* theme,
* isDark,
* toggleTheme,
* openThemeColorPicker,
* currentThemeColor,
* themeVars
* } = useTheme()
* </script>
*
* <template>
* <wd-config-provider :theme="theme" :theme-vars="themeVars">
* <view :class="{ 'dark-mode': isDark }">
* <wd-button @click="toggleTheme">切换主题</wd-button>
* <wd-button @click="openThemeColorPicker">选择主题色</wd-button>
* </view>
* </wd-config-provider>
* </template>
* ```
*/
export function useTheme() {
const store = useThemeStore()
const showThemeColorSheet = ref(false)
let stopThemeChangeListener: (() => void) | undefined
/**
* 切换暗黑模式
* @param mode 指定主题模式,不传则自动切换
* @param isFollowSystem 是否跟随系统
*/
function toggleTheme(mode?: ThemeMode, isFollowSystem: boolean = false) {
store.toggleTheme(mode, isFollowSystem)
}
/**
* 打开主题色选择器
*/
function openThemeColorPicker() {
showThemeColorSheet.value = true
}
/**
* 关闭主题色选择器
*/
function closeThemeColorPicker() {
showThemeColorSheet.value = false
}
/**
* 选择主题色
* @param option 主题色选项
*/
function selectThemeColor(option: ThemeColorOption) {
store.setCurrentThemeColor(option)
closeThemeColorPicker()
}
/**
* 初始化主题
*/
function initTheme() {
store.initTheme()
}
// 组件挂载前初始化主题
onBeforeMount(() => {
initializeThemeOnce(store, initTheme)
stopThemeChangeListener = subscribeSystemThemeChange(store, (res) => {
if (store.followSystem) {
store.toggleTheme(res.theme, true)
}
})
})
// 页面显示时更新导航栏颜色,确保每次切换页面时导航栏颜色都是正确的
onShow(() => {
store.setNavigationBarColor()
})
// 组件卸载时清理监听
onUnmounted(() => {
stopThemeChangeListener?.()
stopThemeChangeListener = undefined
})
return {
// 状态
theme: computed(() => store.theme),
isDark: computed(() => store.isDark),
followSystem: computed(() => store.followSystem),
hasUserSet: computed(() => store.hasUserSet),
currentThemeColor: computed(() => store.currentThemeColor),
themeVars: computed(() => store.themeVars),
showThemeColorSheet,
// 常量
themeColorOptions,
// 方法
initTheme,
toggleTheme,
setFollowSystem: store.setFollowSystem,
openThemeColorPicker,
closeThemeColorPicker,
selectThemeColor,
}
}
// 导出类型和常量供外部使用
export type { ThemeColorOption, ThemeMode }
export { themeColorOptions }
@@ -0,0 +1,23 @@
import { ref } from 'vue'
import { TicketAPI } from '@/api/module_system/ticket'
/**
* 工单统计(后端聚合接口 + alova 30 秒缓存),work/mine 两页共用
*/
export function useTicketStats() {
const pendingTickets = ref<number | null>(null)
const processingTickets = ref<number | null>(null)
const doneTickets = ref<number | null>(null)
async function loadTicketStats() {
try {
const stats = await TicketAPI.getStats()
pendingTickets.value = stats.pending
processingTickets.value = stats.processing
doneTickets.value = stats.done
}
catch { /* silent */ }
}
return { pendingTickets, processingTickets, doneTickets, loadTicketStats }
}
@@ -0,0 +1,34 @@
import { computed, onUnmounted, ref } from 'vue'
import { WATERMARK_KEY } from '@/constants'
import { useUserStore } from '@/store/userStore'
import { Storage } from '@/utils/storage'
/**
* 小程序水印逻辑(配合 wd-watermark 使用)
* - 显示条件:本地偏好开启(默认开,由设置页"水印"开关控制)&& 已登录
* - 水印内容为当前登录用户名(username 优先,回退 name)
* - 响应式:登录用户 / 设置页开关变更后自动更新(layout 常驻不重挂载,经 uni.$on 同步)
*/
export function useWatermark() {
const userStore = useUserStore()
const themeStore = useThemeStore()
// 兼容历史脏对象 { value },统一兜底为布尔(wd-switch change 事件参数为对象)
const stored = Storage.get<boolean | { value: boolean }>(WATERMARK_KEY)
const localSwitch = ref(typeof stored === 'object' && stored ? Boolean(stored.value) : (stored ?? true))
const content = computed(() => userStore.userInfo?.username || userStore.userInfo?.name || '')
const enabled = computed(() => localSwitch.value && !!content.value)
// 水印文字使用当前主题色,主题色切换后经 wd-watermark 的 props deep watch 自动重绘
const color = computed(() => themeStore.currentThemeColor.primary)
// 设置页切换水印开关时同步本地状态(uni.$on 在 H5 / 小程序均可用)
const onSwitchChange = (value: boolean) => {
localSwitch.value = value
}
uni.$on('updateWatermarkSwitch', onSwitchChange)
onUnmounted(() => {
uni.$off('updateWatermarkSwitch', onSwitchChange)
})
return { enabled, content, color }
}
@@ -0,0 +1,76 @@
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/userStore'
/**
* 微信小程序登录 composable
*
* 封装 uni.login → 后端换 token 的完整流程。
* 仅在微信小程序端生效(#ifdef MP-WEIXIN)。
*
* @example
* ```ts
* const { wxLogin, wxPhoneLogin } = useWxLogin()
*
* // 微信一键登录
* async function handleWxLogin() {
* await wxLogin()
* uni.reLaunch({ url: '/pages/index/index' })
* }
*
* // 手机号登录(配合 <button open-type="getPhoneNumber">)
* async function handlePhoneLogin(e) {
* if (e.detail.errMsg !== 'getPhoneNumber:ok') return
* await wxPhoneLogin(e.detail.code)
* uni.reLaunch({ url: '/pages/index/index' })
* }
* ```
*/
export function useWxLogin() {
const { t } = useI18n()
const userStore = useUserStore()
/**
* 微信一键登录
* 1. uni.login 获取 code
* 2. 可选获取用户头像昵称(需用户授权)
* 3. 发送 code 给后端换取 JWT
*/
async function wxLogin(): Promise<void> {
// #ifdef MP-WEIXIN
const { code } = await uni.login({ provider: 'weixin' })
if (!code)
throw new Error(t('login.wxAuthFailed'))
await userStore.wxLogin({ code })
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: t('login.wxOnly'), icon: 'none' })
// #endif
}
/**
* 微信手机号快速登录
* 配合 <button open-type="getPhoneNumber"> 使用
* 使用 2023+ 新方案:仅需 getPhoneNumber 回调返回的 code,无需 encryptedData/iv
*
* @param phoneCode getPhoneNumber 回调返回的动态令牌(e.detail.code)
*/
async function wxPhoneLogin(phoneCode: string): Promise<void> {
// #ifdef MP-WEIXIN
if (!phoneCode)
throw new Error(t('login.phoneCodeFailed'))
await userStore.wxPhoneLogin({ code: phoneCode })
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: t('login.wxOnly'), icon: 'none' })
// #endif
}
return {
wxLogin,
wxPhoneLogin,
}
}
+6
View File
@@ -0,0 +1,6 @@
/**
* 常量统一导出
*/
// 存储相关常量
export * from './storage.constant'
@@ -0,0 +1,21 @@
/**
* markdown 渲染公共配置(配合 mp-html 组件使用)
* 中性 rgba 色可自适应亮暗主题;使用 wot 语义色可随品牌主题联动
*/
export const MARKDOWN_TAG_STYLE: Record<string, string> = {
h1: 'font-size:17px;font-weight:700;margin:10px 0 6px;line-height:1.5;',
h2: 'font-size:16px;font-weight:700;margin:9px 0 5px;line-height:1.5;',
h3: 'font-size:15px;font-weight:700;margin:8px 0 4px;line-height:1.5;',
p: 'margin:6px 0;line-height:1.7;',
ul: 'margin:6px 0;padding-left:18px;',
ol: 'margin:6px 0;padding-left:18px;',
li: 'margin:3px 0;line-height:1.7;',
code: 'font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:85%;background:rgba(127,127,127,.14);padding:2px 5px;border-radius:4px;',
pre: 'background:rgba(127,127,127,.10);padding:10px 12px;border-radius:8px;overflow-x:auto;margin:8px 0;',
blockquote: 'margin:8px 0;padding:2px 12px;border-left:3px solid rgba(127,127,127,.35);color:rgba(127,127,127,.9);',
table: 'width:100%;border-collapse:collapse;margin:8px 0;',
th: 'border:1px solid rgba(127,127,127,.25);padding:6px 10px;text-align:left;font-weight:600;',
td: 'border:1px solid rgba(127,127,127,.25);padding:6px 10px;text-align:left;',
a: 'color:#4F8CFF;',
img: 'max-width:100%;border-radius:8px;',
}
@@ -0,0 +1,55 @@
/**
* 存储键名常量
*
* @description
* 统一管理所有 uni.storage 的键名,与 web 端保持一致
* 命名规则:{分类}_{具体名称}(snake_case)
*/
// 🔐 用户认证相关
export const ACCESS_TOKEN_KEY = 'access_token'
export const REFRESH_TOKEN_KEY = 'refresh_token'
export const REMEMBER_ME_KEY = 'remember_me'
// 📊 数据缓存相关
export const DICT_CACHE_KEY = 'dict_cache'
// 🎨 系统设置相关
export const THEME_KEY = 'theme'
export const THEME_COLOR_KEY = 'themeColor'
// 🛡️ 功能开关相关
export const WATERMARK_KEY = 'watermark_switch'
// 🌍 国际化相关
export const LANG_KEY = 'lang'
// 🎯 角色常量
export const ROLE_ROOT = 'ADMIN'
// 📌 认证相关键集合
export const AUTH_KEYS = {
ACCESS_TOKEN: ACCESS_TOKEN_KEY,
REFRESH_TOKEN: REFRESH_TOKEN_KEY,
REMEMBER_ME: REMEMBER_ME_KEY,
} as const
// 📦 缓存相关键集合
export const CACHE_KEYS = {
DICT_CACHE: DICT_CACHE_KEY,
} as const
// 🎨 设置相关键集合
export const SETTINGS_KEYS = {
THEME: THEME_KEY,
THEME_COLOR: THEME_COLOR_KEY,
WATERMARK: WATERMARK_KEY,
LANG: LANG_KEY,
} as const
// 📦 所有存储键的统一集合
export const ALL_STORAGE_KEYS = {
...AUTH_KEYS,
...CACHE_KEYS,
...SETTINGS_KEYS,
} as const
@@ -0,0 +1 @@
<view></view>
@@ -0,0 +1,5 @@
// eslint-disable-next-line no-undef
Component({
data: {},
methods: {},
})
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+205
View File
@@ -0,0 +1,205 @@
import type { Method } from 'alova'
import type { UniappNormalResponse } from '../types'
import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import VueHook from 'alova/vue'
import AuthAPI from '@/api/module_system/auth'
import { useUserStore } from '@/store/userStore'
import { toLoginPage } from '@/utils/toLoginPage'
import { ContentTypeEnum, HttpStatus, ResultEnum, ShowMessage } from '../tools/enum'
// 配置动态Tag
export const API_DOMAINS = {
DEFAULT: import.meta.env.VITE_API_BASE_URL || '',
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY || '',
}
/** 从响应中提取错误消息:优先后端 msg,其次按状态码映射 */
function getErrorMessage(statusCode: number, rawData: any): string {
return rawData?.msg || rawData?.message || rawData?.error || ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
}
/** 登录态失效:清空凭据与用户信息后回登录页(不调用后端注销接口,避免 token 失效后再触发 401) */
function handleAuthExpired(userStore: ReturnType<typeof useUserStore>) {
userStore.clearAll()
toLoginPage({ mode: 'reLaunch' })
}
/** 未登录重定向防抖标记:并发请求只跳一次登录页 */
let redirectingToLogin = false
// ===== 401 处理(仿 web 端 utils/http/index.ts):刷新 token + 并发队列重放 =====
/** token 刷新进行中标记,避免并发 401 触发多次 refresh 请求 */
let isRefreshing = false
let pendingRequests: Array<{
method: Method
resolve: (value: unknown) => void
reject: (reason?: unknown) => void
}> = []
/** 刷新成功:重放等待队列(重放时 beforeRequest 会自动注入新 token) */
function onRefreshed() {
const list = pendingRequests
pendingRequests = []
list.forEach(({ method, resolve }) => resolve(method.send()))
}
/** 刷新失败:拒绝等待队列 */
function onRefreshFailed() {
pendingRequests.forEach(({ reject }) => reject(new Error('[认证失效]:登录已过期')))
pendingRequests = []
}
/**
* 401 统一处理:
* - refresh 请求自身 401:抛错交给刷新发起方(避免死循环)
* - login/visitor 公开请求 401:按普通错误提示
* - 业务请求 401:首次刷新,并发入队;刷新成功重放,失败清凭据跳登录
*/
async function handleUnauthorized(method: Method, rawData: any): Promise<unknown> {
const { config } = method
const userStore = useUserStore()
const role = config.meta?.authRole
const message = getErrorMessage(HttpStatus.UNAUTHORIZED, rawData)
if (role === 'refreshToken')
throw new Error(message)
if (role === 'login' || role === 'visitor')
return throwApiError(message, config)
if (method.url?.includes('/auth/logout')) {
handleAuthExpired(userStore)
throw new Error(message)
}
if (!isRefreshing) {
isRefreshing = true
try {
const refreshToken = userStore.getRefreshToken()
if (!refreshToken)
throw new Error('[认证失效]:请重新登录')
const res = await AuthAPI.refreshToken({ refresh_token: refreshToken })
if (!res?.access_token)
throw new Error('[认证失效]:登录已过期')
userStore.setAccessToken(res.access_token)
if (res.refresh_token)
userStore.setRefreshToken(res.refresh_token)
isRefreshing = false
onRefreshed()
return method.send()
}
catch (error) {
isRefreshing = false
onRefreshFailed()
handleAuthExpired(userStore)
throw error
}
}
return new Promise((resolve, reject) => {
pendingRequests.push({ method, resolve, reject })
})
}
/**
* alova 请求实例
*/
const alovaInstance = createAlova({
baseURL: `${API_DOMAINS.DEFAULT}${import.meta.env.VITE_APP_BASE_API || ''}`,
...AdapterUniapp(),
timeout: 10000,
statesHook: VueHook,
beforeRequest(method) {
// 设置默认 Content-Type
method.config.headers = {
ContentType: ContentTypeEnum.JSON,
Accept: 'application/json, text/plain, */*',
...method.config.headers,
}
const { config } = method
// 处理动态域名
if (config.meta?.domain)
method.baseURL = config.meta.domain
// 免认证请求(验证码/登录/刷新令牌等)通过 meta.ignoreAuth 标记,统一跳过鉴权
if (config.meta?.ignoreAuth)
return
// 需要认证的请求:校验 token 并注入 Authorization 头
const userStore = useUserStore()
const token = userStore.getAccessToken()
if (!token) {
// 未登录:清空残留状态并引导到登录页(防抖,避免并发请求重复重定向),并终止请求
if (!redirectingToLogin) {
redirectingToLogin = true
handleAuthExpired(userStore)
setTimeout(() => {
redirectingToLogin = false
}, 1500)
}
throw new Error('[请求错误]:未登录')
}
method.config.headers.Authorization = `Bearer ${token}`
},
responded: {
// 成功响应拦截:统一处理 HTTP 状态码与业务码,错误统一提示并抛出
onSuccess: async (response, method) => {
const { config } = method
if (config.requestType === 'upload' || config.requestType === 'download')
return response
// 普通请求:uniapp adapter 响应结构为 { statusCode, data }(上传/下载已提前返回,此处必为普通请求)
const { statusCode, data: rawData } = response as unknown as UniappNormalResponse
// 401:uniapp adapter 对 401 也走成功回调,需在此手动刷新 + 重放
if (statusCode === HttpStatus.UNAUTHORIZED)
return handleUnauthorized(method, rawData)
if (statusCode !== 200)
return throwApiError(getErrorMessage(statusCode, rawData), config)
const { code, message, msg, data } = rawData
if (code !== ResultEnum.SUCCESS) {
return throwApiError(msg || message || '请求错误', config)
}
// 处理成功响应,返回业务数据
return data
},
// 错误响应拦截:覆盖网络/超时等未在 onSuccess 提示过的错误
onError: (error, method) => {
const { config } = method
if (!(error as Error & { __toasted?: boolean })?.__toasted && !config.meta?.silent) {
if (error.name === 'NetworkError')
notifyError('网络错误,请检查您的网络连接')
else if (error.name === 'TimeoutError')
notifyError('请求超时,请重试')
else
notifyError((error as Error)?.message || '请求错误')
}
throw error
},
},
})
/** 统一错误提示:非组件上下文使用全局 toast(wot-ui wd-toast,挂载于 App.ku.vue) */
function notifyError(message: string) {
useGlobalToast().error(message || '请求错误')
}
/**
* 抛出接口错误并统一全局提示
* 页面无需再单独提取/展示错误信息;meta.silent 可用于内联处理场景(如聊天内联展示)
*/
function throwApiError(message: string, config: { meta?: Record<string, any> }) {
const err = new Error(message) as Error & { __toasted?: boolean }
if (!config.meta?.silent) {
err.__toasted = true
notifyError(message)
}
throw err
}
export const http = alovaInstance
+13
View File
@@ -0,0 +1,13 @@
// 导出类型
import type { CustomRequestOptions, IResponse } from './types'
// 导出请求适配器
import { http as alovaHttp } from './adapters/alova'
// 导出默认请求实例
export const http = alovaHttp
// 导出所有类型
export type { CustomRequestOptions, IResponse }
// 导出请求适配器
export { alovaHttp }
+91
View File
@@ -0,0 +1,91 @@
/**
* API 结果枚举 - 与后端 ResultEnum 对应
*
* 仅包含业务状态码,HTTP 状态码请使用 {@link HttpStatus}
*/
export enum ResultEnum {
/** 成功 */
SUCCESS = 0,
/** 错误 */
ERROR = 1,
/** 异常 */
EXCEPTION = -1,
/** 未授权访问 */
UNAUTHORIZED = 10403,
/** 令牌已过期 */
TOKEN_EXPIRED = 10401,
}
/**
* HTTP 状态码枚举(用于 error.response.status 判断)
*/
export enum HttpStatus {
SUCCESS = 200,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
REQUEST_TIMEOUT = 408,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
}
/**
* 内容类型枚举
*/
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',
}
/**
* 根据状态码,生成对应的错误信息
* @param status 状态码
* @returns 错误信息
*/
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},请检查网络或联系管理员!`
}
@@ -0,0 +1,29 @@
/**
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
* 支持基本类型值和数组,不支持嵌套对象
* @param obj 要序列化的对象
* @returns 序列化后的查询字符串
*/
export function stringifyQuery(obj: Record<string, any>): 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('&')
}
+43
View File
@@ -0,0 +1,43 @@
/**
* 在 uniapp 的 RequestOptions 和 UploadFileOption 基础上,添加自定义参数
*/
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & UniApp.UploadFileOption // 添加uni.uploadFile参数类型
/** 主要提供给 openapi-ts-request 生成的代码使用 */
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
export interface HttpRequestResult<T> {
promise: Promise<T>
requestTask: UniApp.RequestTask
}
// 通用响应格式(兼容 msg + message 字段)
export interface IResponse<T = any> {
code: number
data: T
msg: string
message?: string
status_code: number
success: boolean
[key: string]: any // 允许额外属性
}
// 分页请求参数
export interface PageParams {
page: number
pageSize: number
[key: string]: any
}
/**
* uniapp adapter 普通请求(非上传/下载)的响应结构
* 对应 UniNamespace.RequestSuccessCallbackResult
*/
export interface UniappNormalResponse {
statusCode: number
data: IResponse
}
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts" setup>
const { enabled: watermarkEnabled, content: watermarkContent, color: watermarkColor } = useWatermark()
</script>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<slot />
<wd-watermark
v-if="watermarkEnabled"
:content="watermarkContent"
:color="watermarkColor"
:width="130"
:height="140"
:opacity="0.4"
layout="grid"
/>
</template>
+66
View File
@@ -0,0 +1,66 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar()
const { enabled: watermarkEnabled, content: watermarkContent, color: watermarkColor } = useWatermark()
function handleTabbarChange({ value }: { value: string }) {
setTabbarItemActive(value)
router.pushTab({ name: value })
}
onMounted(() => {
// #ifdef APP
uni.hideTabBar()
// #endif
nextTick(() => {
if (route.name && route.name !== activeTabbar.value.name) {
setTabbarItemActive(route.name)
}
})
})
</script>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<slot />
<wd-gap
safe-area-bottom
height="var(--wot-tabbar-height, 50px)"
custom-class="tabbar-gap"
/>
<wd-tabbar
:model-value="activeTabbar.name" bordered safe-area-inset-bottom fixed
@change="handleTabbarChange"
>
<wd-tabbar-item
v-for="(item, index) in tabbarList" :key="index" :name="item.name"
:value="getTabbarItemValue(item.name)" :title="t(item.titleKey)" :icon="item.icon"
/>
</wd-tabbar>
<wd-watermark
v-if="watermarkEnabled"
:content="watermarkContent"
:color="watermarkColor"
:width="130"
:height="140"
:opacity="0.4"
layout="grid"
/>
</template>
+37
View File
@@ -0,0 +1,37 @@
import { createI18n } from 'vue-i18n'
import { LANG_KEY } from '@/constants'
import { Storage } from '@/utils/storage'
import enUS from './langs/en.json'
import zhCN from './langs/zh.json'
/** 支持的语言列表 */
export const SUPPORT_LOCALES = ['zh-CN', 'en-US'] as const
export type SupportLocale = typeof SUPPORT_LOCALES[number]
/** 默认语言 */
export const DEFAULT_LOCALE: SupportLocale = 'zh-CN'
/** 从本地存储读取语言偏好(非法值回退默认语言) */
function resolveLocale(): SupportLocale {
const saved = Storage.get<string>(LANG_KEY)
return (SUPPORT_LOCALES as readonly string[]).includes(saved || '') ? saved as SupportLocale : DEFAULT_LOCALE
}
/**
* vue-i18n 实例
* - legacy: false 使用 Composition API 模式
* - globalInjection: true 全局注入 $t/$d 等,模板可直接使用 $t(否则会报 Property "$t" is not defined)
* - 语言偏好持久化于 LANG_KEY,切换后由设置页写入
*/
const i18n = createI18n({
locale: resolveLocale(),
legacy: false,
globalInjection: true,
fallbackLocale: DEFAULT_LOCALE,
messages: {
'zh-CN': zhCN,
'en-US': enUS,
},
})
export default i18n
+377
View File
@@ -0,0 +1,377 @@
{
"common": {
"title": "Tip",
"cancel": "Cancel",
"save": "Save",
"send": "Send",
"delete": "Delete",
"new": "New",
"confirm": "Confirm",
"loadFailed": "Failed to load",
"operationFailed": "Operation failed",
"deleteSuccess": "Deleted",
"deleteFailed": "Delete failed",
"deleteConfirm": "Confirm delete?",
"updateSuccess": "Updated",
"createSuccess": "Created",
"saveSuccess": "Saved",
"noMore": "No more",
"placeholder": "Please enter",
"selectPlaceholder": "Please select",
"all": "All",
"admin": "Admin",
"docs": "Documentation",
"github": "GitHub Repository",
"aboutUs": "About Us",
"settings": "Settings",
"profile": "Profile",
"field": {
"status": "Status",
"remark": "Remark"
},
"form": {
"usernamePlaceholder": "Enter username",
"usernameRequired": "Please enter a username",
"usernameFormat": "Username must start with a letter, 3-32 characters, only letters, digits, _ . -",
"passwordPlaceholder": "Enter password",
"passwordRequired": "Please enter a password",
"passwordLength": "Password must be at least 6 characters",
"newPasswordPlaceholder": "Enter new password",
"newPasswordRequired": "Please enter a new password",
"confirmPlaceholder": "Confirm password",
"confirmRequired": "Please confirm the password",
"confirmNewPlaceholder": "Confirm new password",
"confirmNewRequired": "Please confirm the new password",
"mismatch": "The two passwords do not match",
"nicknamePlaceholder": "Please enter a nickname",
"nicknameRequired": "Please enter a nickname"
},
"shareDefaultTitle": "FastapiAdmin - Efficient enterprise management mobile platform",
"subscribeTitle": "Enable Message Notifications",
"subscribeContent": "You have disabled message notifications. Go to settings to enable?",
"subscribeGoSetting": "Go to Settings",
"tab": { "home": "Home", "work": "Workbench", "mine": "Me" },
"nav": {
"notices": "Notices",
"tickets": "Tickets",
"aiChat": "AI Chat",
"aiModels": "AI Models"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"draft": "Draft",
"published": "Published",
"archived": "Archived",
"active": "Active",
"success": "Success",
"failed": "Failed",
"pending": "Pending",
"processing": "Processing",
"completed": "Completed",
"closed": "Closed",
"deprecated": "Deprecated",
"expired": "Expired",
"unknown": "Unknown"
},
"type": {
"suggestion": "Suggestion",
"bug": "Bug",
"optimize": "Optimization",
"other": "Other"
}
},
"login": {
"navTitle": "Login",
"cardTitle": "Account Login",
"remember": "Remember me",
"forgot": "Forgot password?",
"submit": "Sign In",
"submitting": "Signing in...",
"thirdParty": "Third-party Login",
"wechat": "WeChat",
"github": "GitHub",
"gitee": "Gitee",
"qq": "QQ",
"noAccount": "Don't have an account?",
"toRegister": "Register",
"sliderText": "Slide to verify",
"sliderSuccessText": "Verified",
"sliderRequired": "Please complete the slider verification first",
"captchaExpired": "Captcha expired, please refresh",
"sliderFailed": "Verification failed, please retry",
"usernameLength": "Username must be 3-20 characters",
"passwordLength": "Password must be 6-20 characters",
"oauthCopyTip": "Link copied, please open in browser",
"oauthFailed": "OAuth login failed",
"loginFailed": "Login failed, please check your credentials",
"wxLogin": "WeChat One-click Login",
"phoneLogin": "Quick Phone Login",
"orAccount": "or login with account",
"wxLoginFailed": "WeChat login failed, please retry",
"phoneAuthCancel": "Phone number authorization cancelled",
"phoneLoginFailed": "Phone number login failed, please retry",
"brandSubtitle": "Efficient enterprise management mobile platform",
"wxOnly": "This feature is only available in the WeChat Mini Program",
"wxAuthFailed": "WeChat login authorization failed",
"phoneCodeFailed": "Failed to get phone number authorization code"
},
"register": {
"navTitle": "Register",
"title": "Create Account",
"submit": "Register",
"submitting": "Registering...",
"hasAccount": "Already have an account?",
"toLogin": "Login",
"agree": "I have read and agree to the",
"userAgreement": "User Agreement",
"agreeRequired": "Please read and agree to the user agreement first",
"success": "Registration successful"
},
"forget": {
"navTitle": "Forgot Password",
"title": "Forgot Password",
"submit": "Reset Password",
"submitting": "Submitting...",
"toLogin": "Back to Login",
"success": "Password reset, please log in again"
},
"setting": {
"navTitle": "Settings",
"basic": "Basic",
"darkMode": "Dark Mode",
"followSystem": "Follow System",
"themeColor": "Theme Color",
"watermark": "Watermark",
"links": "Links",
"general": "General",
"version": "Version",
"clearCache": "Clear Cache",
"clearCacheMsg": "Clear local cache? You will be logged out.",
"clearCacheConfirm": "Clear",
"language": "Language",
"copied": "{url} copied to clipboard",
"brandDesc": "Providing secure, efficient and scalable management solutions",
"langZh": "简体中文",
"langEn": "English"
},
"about": {
"navTitle": "About",
"currentVersion": "Version v{version}",
"moreInfo": "More Info",
"copied": "Link copied to clipboard"
},
"mine": {
"navTitle": "Me",
"changePassword": "Change Password",
"logout": "Logout",
"logoutMsg": "Are you sure you want to log out?",
"logoutConfirm": "Logout",
"pendingTickets": "Pending",
"processingTickets": "Processing",
"doneTickets": "Done",
"shareTitle": "{name} invites you to join FastapiAdmin",
"roleFallback": "Super Admin · Tech Dept"
},
"profile": {
"navTitle": "Profile",
"avatar": "Avatar",
"nickname": "Nickname",
"gender": "Gender",
"male": "Male",
"female": "Female",
"unknown": "Unknown",
"username": "Username",
"mobile": "Mobile",
"email": "Email",
"dept": "Department",
"roles": "Roles",
"editNickname": "Edit Nickname",
"selectGender": "Select Gender",
"notSet": "Not set",
"notBound": "Not bound",
"uploadFailed": "Avatar upload failed",
"nicknameRequired": "Please enter a nickname"
},
"account": {
"password": "Account Password",
"passwordTip": "Changing your password regularly helps protect your account",
"modify": "Change",
"oldPassword": "Current Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"oldPlaceholder": "Enter current password",
"submit": "Submit",
"success": "Password changed",
"oldRequired": "Please enter current password",
"newLength": "New password must be at least 6 characters"
},
"tickets": {
"title": "Ticket Management",
"searchPlaceholder": "Search ticket title",
"typeFilter": "Type",
"allType": "All Types",
"emptyWithFilter": "No matching tickets",
"empty": "No tickets",
"titleRequired": "Please enter a ticket title",
"formTitle": "Ticket Title",
"content": "Content",
"type": "Type",
"create": "New"
},
"notices": {
"title": "Notices",
"searchPlaceholder": "Search notice title",
"count": "{count} in total",
"emptyWithFilter": "No matching notices",
"empty": "No notices",
"createTitle": "New Notice",
"editTitle": "Edit Notice",
"detailFailed": "Failed to load detail",
"formTitle": "Notice Title",
"content": "Notice Content",
"contentPlaceholder": "Please enter content",
"type": "Notice Type",
"notice": "Notice",
"announce": "Announcement"
},
"chat": {
"title": "AI Assistant",
"shareTitle": "FastapiAdmin AI Assistant - Smart Chat, Anytime",
"sessionList": "Sessions",
"new": "New",
"newSession": "New Chat",
"newSessionSuccess": "New session created",
"createFailed": "Failed to create session",
"loadSessionsFailed": "Failed to load sessions",
"loadMessagesFailed": "Failed to load messages",
"deleteConfirm": "Delete this session?",
"deleted": "Deleted",
"deleteFailed": "Delete failed",
"sessionId": "Session {id}",
"noReply": "(No reply)",
"requestFailed": "Request failed, please retry later.",
"welcome": "Hi! I'm the FastapiAdmin AI Assistant. How can I help you?",
"empty": "No sessions, tap + to create one",
"loadMore": "Load earlier messages ({count})",
"inputPlaceholder": "Type a message...",
"stop": "Stop",
"wsConnectFailed": "WebSocket connection failed",
"wsDisconnected": "Connection lost, please retry",
"wsSendFailed": "Connection failed, please retry later"
},
"aiModels": {
"title": "AI Model Config",
"createTitle": "New Model Config",
"editTitle": "Edit Model Config",
"loadFailed": "Failed to load models",
"nameRequired": "Please enter a config name",
"baseUrlRequired": "Please enter the API base URL",
"modelIdRequired": "Please enter a model ID",
"saveFailed": "Save failed",
"deleteConfirm": "Delete this model config?",
"activateConfirm": "Switch to this model config?",
"activated": "Model activated",
"activateFailed": "Switch failed",
"empty": "No model configs",
"groupTitle": "Model Config",
"count": "{count} in total",
"unnamed": "Unnamed",
"temperature": "Temp",
"inUse": "In Use",
"activate": "Activate",
"basic": "Basic",
"name": "Config Name",
"namePlaceholder": "e.g. Production GPT-4",
"modelId": "Model ID",
"modelIdPlaceholder": "e.g. gpt-4o / deepseek-chat",
"baseUrl": "API Base URL",
"advanced": "Security & Params",
"temperatureLabel": "Temperature (0-2)",
"update": "Update",
"create": "Create"
},
"ticketDetail": {
"title": "Ticket Detail",
"shareTitle": "Ticket: {title}",
"loadFailed": "Failed to load ticket detail",
"commentRequired": "Please enter a comment",
"commentSuccess": "Commented",
"commentFailed": "Comment failed",
"unnamed": "Untitled Ticket",
"progress": "Progress",
"createdAt": "Created At",
"handler": "Handler",
"unassigned": "Unassigned",
"updatedAt": "Updated At",
"content": "Ticket Content",
"attachments": "Attachments",
"replies": "Replies",
"comments": "Comments ({count})",
"emptyComments": "No comments yet",
"anonymous": "Anonymous",
"loadMore": "Load more",
"notFound": "Ticket not found or deleted",
"commentPlaceholder": "Write a comment..."
},
"index": {
"navTitle": "Workbench",
"shareTitle": "{greeting}! {name} invites you to use FastapiAdmin",
"welcomeDefault": "Welcome to FastapiAdmin",
"today": "Today",
"welcomeBack": "Welcome back, have a productive day",
"overview": "Overview",
"todo": "To-do",
"pendingCount": "{count} pending tickets",
"allDone": "All tickets handled",
"todoSubtitle": "Tap to handle now to keep business on track",
"doneSubtitle": "Great job, keep it up",
"goHandle": "Handle",
"viewTickets": "View Tickets",
"ticketDesc": "Tap to view ticket list",
"data": "Data",
"dataTitle": "Overview",
"dataSubtitle": "System status & trend analysis",
"dataDesc": "Users {total} · Online {online} · Logins today {today}",
"viewDetail": "View Details",
"loginCount": "Logins",
"visitor": "Unique Visitors",
"newUser": "New Users",
"night": "It's late",
"morning": "Good morning",
"forenoon": "Good morning",
"noon": "Good noon",
"afternoon": "Good afternoon",
"evening": "Good evening",
"opsOverview": "Operation Overview",
"registered": "Registered Users",
"weekNew": "{count} this week",
"online": "Online Users",
"currentOnline": "Currently online",
"todayLogin": "Logins Today",
"independentUser": "{count} unique users",
"sysMonitor": "System Monitor",
"loginTrend": "Login Trend (7 days)",
"latestNotice": "Latest Notices"
},
"work": {
"navTitle": "Workbench",
"shareTitle": "FastapiAdmin Workbench - Notices / Tickets / AI",
"businessCenter": "Business Center",
"devTools": "Developer Tools",
"nameFallback": "FastapiAdmin",
"roleFallback": "Super Admin",
"searchPlaceholder": "Search modules",
"emptyTip": "No matching module, try another keyword"
},
"theme": {
"colors": {
"blue": "Default Blue",
"orange": "Vibrant Orange",
"green": "Mint Green",
"pink": "Sakura Pink",
"purple": "Violet",
"red": "Vermilion Red"
}
}
}
+377
View File
@@ -0,0 +1,377 @@
{
"common": {
"title": "提示",
"cancel": "取消",
"save": "保存",
"send": "发送",
"delete": "删除",
"new": "新建",
"confirm": "确定",
"loadFailed": "加载失败",
"operationFailed": "操作失败",
"deleteSuccess": "删除成功",
"deleteFailed": "删除失败",
"deleteConfirm": "确认删除?",
"updateSuccess": "更新成功",
"createSuccess": "创建成功",
"saveSuccess": "保存成功",
"noMore": "已经到底啦",
"placeholder": "请输入",
"selectPlaceholder": "请选择",
"all": "全部",
"admin": "管理员",
"docs": "使用文档",
"github": "GitHub 仓库",
"aboutUs": "关于我们",
"settings": "设置",
"profile": "个人资料",
"field": {
"status": "状态",
"remark": "备注"
},
"form": {
"usernamePlaceholder": "请输入用户名",
"usernameRequired": "请输入用户名",
"usernameFormat": "账号需以字母开头,3-32 位,仅允许字母、数字、_ . -",
"passwordPlaceholder": "请输入密码",
"passwordRequired": "请输入密码",
"passwordLength": "密码长度不能少于 6 位",
"newPasswordPlaceholder": "请输入新密码",
"newPasswordRequired": "请输入新密码",
"confirmPlaceholder": "请确认密码",
"confirmRequired": "请确认密码",
"confirmNewPlaceholder": "请确认新密码",
"confirmNewRequired": "请确认新密码",
"mismatch": "两次输入的密码不一致",
"nicknamePlaceholder": "请输入昵称",
"nicknameRequired": "请输入昵称"
},
"shareDefaultTitle": "FastapiAdmin - 高效的企业管理移动平台",
"subscribeTitle": "开启消息通知",
"subscribeContent": "您已关闭消息通知,是否前往设置开启?",
"subscribeGoSetting": "去设置",
"tab": { "home": "首页", "work": "工作台", "mine": "我的" },
"nav": {
"notices": "通知公告",
"tickets": "工单管理",
"aiChat": "AI 助手",
"aiModels": "AI 模型"
},
"status": {
"enabled": "启用",
"disabled": "禁用",
"draft": "草稿",
"published": "已发布",
"archived": "已归档",
"active": "活跃",
"success": "成功",
"failed": "失败",
"pending": "待处理",
"processing": "处理中",
"completed": "已完成",
"closed": "已关闭",
"deprecated": "已废弃",
"expired": "过期",
"unknown": "未知"
},
"type": {
"suggestion": "建议",
"bug": "缺陷",
"optimize": "优化",
"other": "其他"
}
},
"login": {
"navTitle": "登录",
"cardTitle": "账号登录",
"remember": "记住密码",
"forgot": "忘记密码?",
"submit": "登 录",
"submitting": "登录中...",
"thirdParty": "第三方登录",
"wechat": "微信",
"github": "GitHub",
"gitee": "Gitee",
"qq": "QQ",
"noAccount": "没有账号?",
"toRegister": "立即注册",
"sliderText": "向右滑动滑块验证",
"sliderSuccessText": "验证成功",
"sliderRequired": "请先完成滑块验证",
"captchaExpired": "验证码已过期,请刷新",
"sliderFailed": "验证失败,请重试",
"usernameLength": "用户名长度为 3-20 个字符",
"passwordLength": "密码长度为 6-20 个字符",
"oauthCopyTip": "链接已复制,请在浏览器中打开",
"oauthFailed": "OAuth登录失败",
"loginFailed": "登录失败,请检查账号密码",
"wxLogin": "微信一键登录",
"phoneLogin": "手机号快速登录",
"orAccount": "或账号密码登录",
"wxLoginFailed": "微信登录失败,请重试",
"phoneAuthCancel": "已取消手机号授权",
"phoneLoginFailed": "手机号登录失败,请重试",
"brandSubtitle": "高效的企业管理移动平台",
"wxOnly": "请在微信小程序中使用此功能",
"wxAuthFailed": "微信登录授权失败",
"phoneCodeFailed": "手机号授权码获取失败"
},
"register": {
"navTitle": "注册",
"title": "注册账号",
"submit": "注 册",
"submitting": "注册中...",
"hasAccount": "已有账号?",
"toLogin": "去登录",
"agree": "我已阅读并同意",
"userAgreement": "《用户协议》",
"agreeRequired": "请先阅读并同意用户协议",
"success": "注册成功"
},
"forget": {
"navTitle": "忘记密码",
"title": "忘记密码",
"submit": "重置密码",
"submitting": "提交中...",
"toLogin": "返回登录",
"success": "密码重置成功,请重新登录"
},
"setting": {
"navTitle": "设置",
"basic": "基础设置",
"darkMode": "暗黑模式",
"followSystem": "跟随系统",
"themeColor": "选择主题色",
"watermark": "水印",
"links": "相关链接",
"general": "通用",
"version": "版本信息",
"clearCache": "清除缓存",
"clearCacheMsg": "确认清除本地缓存吗?清除后将退出登录状态",
"clearCacheConfirm": "清除",
"language": "语言",
"copied": "{url} 已复制到剪贴板",
"brandDesc": "提供安全、高效、可扩展的管理解决方案",
"langZh": "简体中文",
"langEn": "English"
},
"about": {
"navTitle": "关于",
"currentVersion": "当前版本 v{version}",
"moreInfo": "更多信息",
"copied": "链接已复制到剪贴板"
},
"mine": {
"navTitle": "我的",
"changePassword": "修改密码",
"logout": "退出登录",
"logoutMsg": "确认退出登录吗?",
"logoutConfirm": "退出",
"pendingTickets": "待处理工单",
"processingTickets": "处理中工单",
"doneTickets": "已完成工单",
"shareTitle": "{name} 邀请你加入 FastapiAdmin",
"roleFallback": "超级管理员 · 技术部"
},
"profile": {
"navTitle": "个人资料",
"avatar": "头像",
"nickname": "昵称",
"gender": "性别",
"male": "男",
"female": "女",
"unknown": "未知",
"username": "用户名",
"mobile": "手机号",
"email": "邮箱",
"dept": "部门",
"roles": "角色",
"editNickname": "编辑昵称",
"selectGender": "选择性别",
"notSet": "未设置",
"notBound": "未绑定",
"uploadFailed": "头像上传失败",
"nicknameRequired": "请填写昵称"
},
"account": {
"password": "账户密码",
"passwordTip": "定期修改密码有助于保护账户安全",
"modify": "修改",
"oldPassword": "原密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"oldPlaceholder": "请输入原密码",
"submit": "提交",
"success": "密码修改成功",
"oldRequired": "请输入原密码",
"newLength": "新密码至少 6 位"
},
"tickets": {
"title": "工单管理",
"searchPlaceholder": "搜索工单标题",
"typeFilter": "类型",
"allType": "全部类型",
"emptyWithFilter": "未找到匹配工单",
"empty": "暂无工单",
"titleRequired": "请输入工单标题",
"formTitle": "工单标题",
"content": "内容",
"type": "类型",
"create": "新增"
},
"notices": {
"title": "通知公告",
"searchPlaceholder": "搜索公告标题",
"count": "共 {count} 条",
"emptyWithFilter": "未找到匹配公告",
"empty": "暂无公告",
"createTitle": "新增公告",
"editTitle": "编辑公告",
"detailFailed": "获取详情失败",
"formTitle": "公告标题",
"content": "公告内容",
"contentPlaceholder": "请输入内容",
"type": "公告类型",
"notice": "通知",
"announce": "公告"
},
"chat": {
"title": "AI 助手",
"shareTitle": "FastapiAdmin AI 助手 - 智能对话,随时答疑",
"sessionList": "会话列表",
"new": "新建",
"newSession": "新对话",
"newSessionSuccess": "新会话已创建",
"createFailed": "创建会话失败",
"loadSessionsFailed": "加载会话列表失败",
"loadMessagesFailed": "加载会话消息失败",
"deleteConfirm": "确认删除该会话?",
"deleted": "已删除",
"deleteFailed": "删除失败",
"sessionId": "会话 {id}",
"noReply": "(无回复内容)",
"requestFailed": "请求失败,请稍后重试。",
"welcome": "你好!我是 FastapiAdmin AI 助手,有什么可以帮助你的?",
"empty": "暂无会话,点击右上角新建",
"loadMore": "加载更早消息({count} 条)",
"inputPlaceholder": "输入消息...",
"stop": "停止",
"wsConnectFailed": "WebSocket 连接失败",
"wsDisconnected": "连接已断开,请重试",
"wsSendFailed": "连接失败,请稍后重试"
},
"aiModels": {
"title": "AI 模型配置",
"createTitle": "新建模型配置",
"editTitle": "编辑模型配置",
"loadFailed": "加载模型列表失败",
"nameRequired": "请输入配置名称",
"baseUrlRequired": "请输入 API 地址",
"modelIdRequired": "请输入模型 ID",
"saveFailed": "保存失败",
"deleteConfirm": "确认删除此模型配置?",
"activateConfirm": "确认切换到此模型配置?",
"activated": "已切换激活模型",
"activateFailed": "切换失败",
"empty": "暂无模型配置",
"groupTitle": "模型配置",
"count": "共 {count} 条",
"unnamed": "未命名",
"temperature": "温度",
"inUse": "使用中",
"activate": "激活",
"basic": "基础配置",
"name": "配置名称",
"namePlaceholder": "如:生产环境 GPT-4",
"modelId": "模型 ID",
"modelIdPlaceholder": "如:gpt-4o / deepseek-chat",
"baseUrl": "API 地址",
"advanced": "安全与参数",
"temperatureLabel": "温度 (0-2)",
"update": "更新",
"create": "创建"
},
"ticketDetail": {
"title": "工单详情",
"shareTitle": "工单:{title}",
"loadFailed": "加载工单详情失败",
"commentRequired": "请输入评论内容",
"commentSuccess": "评论成功",
"commentFailed": "评论失败",
"unnamed": "未命名工单",
"progress": "处理进度",
"createdAt": "创建时间",
"handler": "处理人",
"unassigned": "未分配",
"updatedAt": "更新时间",
"content": "工单内容",
"attachments": "附件图片",
"replies": "处理回复",
"comments": "评论 ({count})",
"emptyComments": "暂无评论,快来抢沙发",
"anonymous": "匿名用户",
"loadMore": "加载更多",
"notFound": "工单不存在或已删除",
"commentPlaceholder": "写下你的评论..."
},
"index": {
"navTitle": "工作台概览",
"shareTitle": "{greeting}!{name} 邀你使用 FastapiAdmin",
"welcomeDefault": "欢迎使用 FastapiAdmin",
"today": "今日",
"welcomeBack": "欢迎回来,开启高效的一天",
"overview": "数据概览",
"todo": "待办",
"pendingCount": "{count} 个待处理工单",
"allDone": "工单已全部处理",
"todoSubtitle": "点击立即处理,避免影响业务",
"doneSubtitle": "处理及时,继续保持",
"goHandle": "去处理",
"viewTickets": "查看工单",
"ticketDesc": "点击进入工单列表",
"data": "数据",
"dataTitle": "数据概览",
"dataSubtitle": "系统运行状态与趋势分析",
"dataDesc": "总用户 {total} · 在线 {online} · 今日登录 {today}",
"viewDetail": "查看详情",
"loginCount": "登录次数",
"visitor": "独立访客",
"newUser": "新增用户",
"night": "夜深了",
"morning": "早上好",
"forenoon": "上午好",
"noon": "中午好",
"afternoon": "下午好",
"evening": "晚上好",
"opsOverview": "运营概览",
"registered": "注册用户",
"weekNew": "本周新增 {count}",
"online": "在线用户",
"currentOnline": "当前在线",
"todayLogin": "今日登录",
"independentUser": "{count} 独立用户",
"sysMonitor": "系统监控",
"loginTrend": "登录趋势 (近7天)",
"latestNotice": "最新公告"
},
"work": {
"navTitle": "工作台",
"shareTitle": "FastapiAdmin 工作台 - 通知公告 / 工单管理 / AI 助手",
"businessCenter": "业务中心",
"devTools": "开发者工具",
"nameFallback": "FastapiAdmin",
"roleFallback": "超级管理员",
"searchPlaceholder": "搜索模块功能",
"emptyTip": "未找到相关模块,换个关键词试试"
},
"theme": {
"colors": {
"blue": "默认蓝",
"orange": "活力橙",
"green": "薄荷绿",
"pink": "樱花粉",
"purple": "紫罗兰",
"red": "朱砂红"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import i18n from './locales'
import router from './router/index.js'
import 'uno.css'
const pinia = createPinia()
pinia.use(persistPlugin)
export function createApp() {
const app = createSSRApp(App)
app.use(router)
app.use(pinia)
app.use(i18n)
return {
app,
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"name": "wot-starter",
"appid": "__UNI__1208592",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"app-harmony": {
"distribute": {}
},
"mp-harmony": {
"distribute": {}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxf306aa91619d2e1c",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"optimization": {
"subPackages": true
},
"darkmode": true,
"themeLocation": "theme.json"
},
"mp-alipay": {
"usingComponents": true,
"compileOptions": {
"globalObjectMode": "enable",
"treeShaking": true
}
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"format": 2,
"compileOptions": {
"globalObjectMode": "enable"
}
}
+199
View File
@@ -0,0 +1,199 @@
{
"globalStyle": {
"navigationBarBackgroundColor": "@navBgColor",
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "FastapiAdmin",
"backgroundColor": "@bgColor",
"backgroundTextStyle": "@bgTxtStyle",
"backgroundColorTop": "@bgColorTop",
"backgroundColorBottom": "@bgColorBottom",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,
"animationType": "pop-in",
"animationDuration": 300
},
"pages": [
// #ifdef APP
{
"path": "pages/index/index",
"type": "home",
"name": "home",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "首页"
}
},
// #endif
// #ifdef H5 || MP-WEIXIN
{
"path": "pages/index/index",
"type": "home",
"name": "home",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "工作台概览"
}
},
// #endif
// #ifdef APP
{
"path": "pages/about/index",
"type": "page",
"name": "about",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "关于"
}
},
// #endif
{
"path": "pages/login/index",
"type": "page",
"name": "login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/mine/index",
"type": "page",
"name": "mine",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/work/index",
"type": "page",
"name": "work",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/login/forget/index",
"type": "page",
"name": "forget",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/login/register/index",
"type": "page",
"name": "register",
"style": {
"navigationBarTitleText": "注册"
}
}
],
"subPackages": [
{
"root": "subPages",
"pages": [
// #ifdef H5 || MP-WEIXIN
{
"path": "about/index",
"type": "page",
"name": "about",
"style": {
"navigationBarTitleText": "关于"
}
},
// #endif
// #ifdef H5 || MP-WEIXIN
{
"path": "setting/index",
"type": "page",
"name": "setting",
"layout": "default",
"style": {
"navigationBarTitleText": "设置"
}
},
// #endif
// #ifdef H5 || MP-WEIXIN
{
"path": "module_ai/chat/index",
"type": "page",
"name": "work-chat",
"style": {
"navigationBarTitleText": "AI 助手"
}
},
// #endif
// #ifdef H5 || MP-WEIXIN
{
"path": "module_system/profile/index",
"type": "page",
"name": "profile",
"layout": "default",
"style": {
"navigationBarTitleText": "个人资料"
}
},
// #endif
{
"path": "module_ai/ai-models/index",
"type": "page",
"name": "work-ai-models",
"style": {
"navigationBarTitleText": "AI 模型配置"
}
},
{
"path": "module_system/notices/index",
"type": "page",
"name": "work-notices",
"style": {
"navigationBarTitleText": "通知公告"
}
},
{
"path": "module_system/ticket-detail/index",
"type": "page",
"name": "work-ticket-detail",
"style": {
"navigationBarTitleText": "工单详情"
}
},
{
"path": "module_system/tickets/index",
"type": "page",
"name": "work-tickets",
"style": {
"navigationBarTitleText": "工单管理"
}
}
]
}
],
"tabBar": {
"custom": true,
"height": "0",
"color": "@tabColor",
"selectedColor": "@tabSelectedColor",
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"list": [
// #ifdef APP || H5 || MP-WEIXIN
{
"pagePath": "pages/index/index"
},
// #endif
// #ifdef APP
{
"pagePath": "pages/about/index"
},
// #endif
{
"pagePath": "pages/work/index"
},
{
"pagePath": "pages/mine/index"
}
]
}
}

Some files were not shown because too many files have changed in this diff Show More