完善生产查询与登录权限配置

This commit is contained in:
2026-09-03 19:56:19 +08:00
parent 793f140a45
commit 02c8fb7021
438 changed files with 1595 additions and 573 deletions
@@ -0,0 +1,9 @@
import { reactive } from 'vue';
import { lightPalette } from './theme-tokens';
import type { ThemeColor } from '../../types/global';
// 使用 reactive 包装颜色对象,使其在运行时可被响应式读取与更新
// 这个对象会被 configProvider 在主题切换时更新
export const color = reactive<ThemeColor>({ ...lightPalette });
export default color;
@@ -0,0 +1,59 @@
/**
* 组件库配置项类型定义
*/
import { reactive } from 'vue';
import { version } from '../../package.json';
import type { DarkMode, DebugMode } from '../../types/global';
export type AppConfig = {
/** 版本号 */
v: string;
/** 版本号(冗余字段) */
version: string;
/** 主题名称列表 */
type: string[];
/** 默认主题名称 */
defaultTheme: string;
/** 默认暗黑主题 */
defaultDarkMode: DarkMode;
/** 默认语言 */
defaultLocale: string;
/** 是否开启调试模式 */
debugMode?: boolean | DebugMode | DebugMode[];
};
export const config = reactive<AppConfig>({
v: version,
version: version,
// 主题名称
type: ['primary', 'success', 'info', 'error', 'warning'],
// 默认为官方主题名称
defaultTheme: 'uviewpro',
// 默认为亮色模式
defaultDarkMode: 'light',
// 默认为中文
defaultLocale: 'zh-CN',
// 默认不开启调试模式
debugMode: false
});
export const isDebugMode = (debug: DebugMode = 'log'): boolean => {
if (config.debugMode === false) return false;
if (config.debugMode === true) return true;
if (Array.isArray(config.debugMode)) return config.debugMode.includes(debug);
return config.debugMode === debug;
};
export const setConfig = (options: Partial<AppConfig>) => {
try {
Object.keys(options).forEach(key => {
if (key in config) {
(config as any)[key] = options[key as keyof AppConfig];
}
});
} catch (e) {
if (isDebugMode('error')) console.error('Failed to set config:', e);
}
};
export default config;
@@ -0,0 +1,102 @@
import type { Theme, ThemeColor } from '../../types/global';
import config from './config';
export const lightPalette: ThemeColor = {
primary: '#2979ff',
primaryDark: '#2b85e4',
primaryDisabled: '#a0cfff',
primaryLight: '#ecf5ff',
success: '#19be6b',
successDark: '#18b566',
successDisabled: '#71d5a1',
successLight: '#dbf1e1',
warning: '#ff9900',
warningDark: '#f29100',
warningDisabled: '#fcbd71',
warningLight: '#fdf6ec',
error: '#fa3534',
errorDark: '#dd6161',
errorDisabled: '#fab6b6',
errorLight: '#fef0f0',
info: '#909399',
infoDark: '#82848a',
infoDisabled: '#c8c9cc',
infoLight: '#f4f4f5',
whiteColor: '#ffffff',
blackColor: '#000000',
mainColor: '#303133',
contentColor: '#606266',
tipsColor: '#909399',
lightColor: '#c0c4cc',
borderColor: '#dcdfe6',
dividerColor: '#e4e7ed',
maskColor: 'rgba(0, 0, 0, 0.4)',
shadowColor: 'rgba(0, 0, 0, 0.1)',
bgColor: '#f3f4f6',
bgWhite: '#ffffff',
bgGrayLight: '#f1f1f1',
bgGrayDark: '#2f343c',
bgBlack: '#000000'
};
export const darkPalette: ThemeColor = {
primary: '#8ab4ff',
primaryDark: '#5f8dff',
primaryDisabled: '#3d4f74',
primaryLight: '#1d273f',
success: '#4ade80',
successDark: '#1f9d57',
successDisabled: '#2f4d3d',
successLight: '#10291f',
warning: '#fbbf24',
warningDark: '#c88f00',
warningDisabled: '#4a3b17',
warningLight: '#2b1f05',
error: '#ff6b6b',
errorDark: '#d83a3a',
errorDisabled: '#4f2323',
errorLight: '#2d1414',
info: '#a0a7b8',
infoDark: '#7c8394',
infoDisabled: '#3b3f4c',
infoLight: '#1d2029',
whiteColor: '#f5f6f7',
blackColor: '#f5f6f7',
mainColor: '#f5f6f7',
contentColor: '#cfd3dc',
tipsColor: '#9aa1af',
lightColor: '#6b7082',
borderColor: '#3a4251',
dividerColor: '#3a4251',
maskColor: 'rgba(0, 0, 0, 0.6)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
bgColor: '#111827',
bgWhite: '#000000',
bgGrayLight: '#1a1a1a',
bgGrayDark: '#f5f7fa',
bgBlack: '#ffffff'
};
const lightCss: Record<string, string> = {
'--u-background': '#ffffff',
'--u-surface': '#f7f8fa',
'--u-text': '#303133'
};
const darkCss: Record<string, string> = {
'--u-background': '#0f1115',
'--u-surface': '#1c2233',
'--u-text': '#f5f6f7'
};
export const defaultThemes: Theme[] = [
{
name: config.defaultTheme,
label: '默认蓝',
description: 'uView Pro 默认主题,支持亮色与暗黑模式',
color: lightPalette,
darkColor: darkPalette,
css: lightCss,
darkCss: darkCss
}
];
@@ -0,0 +1,32 @@
/**
* 组件库 z-index 配置项类型定义
*/
export interface ZIndexConfig {
toast: number;
noNetwork: number;
/** popup包含popup,actionSheet,keyboard,picker的值 */
popup: number;
mask: number;
navbar: number;
topTips: number;
sticky: number;
indexListSticky: number;
tabbar: number;
dropdown: number;
}
const zIndex: ZIndexConfig = {
toast: 10090,
noNetwork: 10080,
// popup包含popup,actionSheet,keyboard,picker的值
popup: 10075,
mask: 10070,
navbar: 980,
topTips: 975,
sticky: 970,
indexListSticky: 965,
tabbar: 998,
dropdown: 985
};
export default zIndex;
@@ -0,0 +1,155 @@
.u-type-primary-light {
color: $u-type-primary-light;
}
.u-type-warning-light {
color: $u-type-warning-light;
}
.u-type-success-light {
color: $u-type-success-light;
}
.u-type-error-light {
color: $u-type-error-light;
}
.u-type-info-light {
color: $u-type-info-light;
}
.u-type-primary-light-bg {
background-color: $u-type-primary-light;
}
.u-type-warning-light-bg {
background-color: $u-type-warning-light;
}
.u-type-success-light-bg {
background-color: $u-type-success-light;
}
.u-type-error-light-bg {
background-color: $u-type-error-light;
}
.u-type-info-light-bg {
background-color: $u-type-info-light;
}
.u-type-primary-dark {
color: $u-type-primary-dark;
}
.u-type-warning-dark {
color: $u-type-warning-dark;
}
.u-type-success-dark {
color: $u-type-success-dark;
}
.u-type-error-dark {
color: $u-type-error-dark;
}
.u-type-info-dark {
color: $u-type-info-dark;
}
.u-type-primary-dark-bg {
background-color: $u-type-primary-dark;
}
.u-type-warning-dark-bg {
background-color: $u-type-warning-dark;
}
.u-type-success-dark-bg {
background-color: $u-type-success-dark;
}
.u-type-error-dark-bg {
background-color: $u-type-error-dark;
}
.u-type-info-dark-bg {
background-color: $u-type-info-dark;
}
.u-type-primary-disabled {
color: $u-type-primary-disabled;
}
.u-type-warning-disabled {
color: $u-type-warning-disabled;
}
.u-type-success-disabled {
color: $u-type-success-disabled;
}
.u-type-error-disabled {
color: $u-type-error-disabled;
}
.u-type-info-disabled {
color: $u-type-info-disabled;
}
.u-type-primary {
color: $u-type-primary;
}
.u-type-warning {
color: $u-type-warning;
}
.u-type-success {
color: $u-type-success;
}
.u-type-error {
color: $u-type-error;
}
.u-type-info {
color: $u-type-info;
}
.u-type-primary-bg {
background-color: $u-type-primary;
}
.u-type-warning-bg {
background-color: $u-type-warning;
}
.u-type-success-bg {
background-color: $u-type-success;
}
.u-type-error-bg {
background-color: $u-type-error;
}
.u-type-info-bg {
background-color: $u-type-info;
}
.u-main-color {
color: $u-main-color;
}
.u-content-color {
color: $u-content-color;
}
.u-tips-color {
color: $u-tips-color;
}
.u-light-color {
color: $u-light-color;
}
@@ -0,0 +1,183 @@
.u-relative,
.u-rela {
position: relative;
}
.u-absolute,
.u-abso {
position: absolute;
}
// nvue不能用标签命名样式,不能放在微信组件中,否则微信开发工具会报警告,无法使用标签名当做选择器
/* #ifndef APP-NVUE */
image {
display: inline-block;
}
// 在weex,也即nvue中,所有元素默认为border-box
view,
text {
box-sizing: border-box;
}
/* #endif */
.u-font-xs {
font-size: 22rpx;
}
.u-font-sm {
font-size: 26rpx;
}
.u-font-md {
font-size: 28rpx;
}
.u-font-lg {
font-size: 30rpx;
}
.u-font-xl {
font-size: 34rpx;
}
.u-flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
}
.u-flex-wrap {
flex-wrap: wrap;
}
.u-flex-nowrap {
flex-wrap: nowrap;
}
.u-col-center {
align-items: center;
}
.u-col-top {
align-items: flex-start;
}
.u-col-bottom {
align-items: flex-end;
}
.u-row-center {
justify-content: center;
}
.u-row-left {
justify-content: flex-start;
}
.u-row-right {
justify-content: flex-end;
}
.u-row-between {
justify-content: space-between;
}
.u-row-around {
justify-content: space-around;
}
.u-text-left {
text-align: left;
}
.u-text-center {
text-align: center;
}
.u-text-right {
text-align: right;
}
.u-flex-col {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.u-hidden {
opacity: 0;
visibility: hidden;
}
// 定义flex等分
@for $i from 0 through 12 {
.u-flex-#{$i} {
flex: $i;
}
}
// 定义字体(px)单位,小于20都为px单位字体
@for $i from 9 to 20 {
.u-font-#{$i} {
font-size: $i + px;
}
}
// 定义字体(rpx)单位,大于或等于20的都为rpx单位字体
@for $i from 20 through 40 {
.u-font-#{$i} {
font-size: $i + rpx;
}
}
// 定义内外边距,历遍1-80
@for $i from 0 through 80 {
// 只要双数和能被5除尽的数
@if $i % 2 == 0 or $i % 5 == 0 {
// 得出:u-margin-30或者u-m-30
.u-margin-#{$i},
.u-m-#{$i} {
margin: $i + rpx !important;
}
// 得出:u-padding-30或者u-p-30
.u-padding-#{$i},
.u-p-#{$i} {
padding: $i + rpx !important;
}
@each $short, $long in l left, t top, r right, b bottom {
// 缩写版,结果如: u-m-l-30
// 定义外边距
.u-m-#{$short}-#{$i} {
margin-#{$long}: $i + rpx !important;
}
// 定义内边距
.u-p-#{$short}-#{$i} {
padding-#{$long}: $i + rpx !important;
}
// 完整版,结果如:u-margin-left-30
// 定义外边距
.u-margin-#{$long}-#{$i} {
margin-#{$long}: $i + rpx !important;
}
// 定义内边距
.u-padding-#{$long}-#{$i} {
padding-#{$long}: $i + rpx !important;
}
}
}
}
// 重置nvue的默认关于flex的样式
.u-reset-nvue {
flex-direction: row;
align-items: center;
}
@@ -0,0 +1,16 @@
// 定义混入指令,用于在非nvue环境下的flex定义,因为nvue没有display属性,会报错
@mixin vue-flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
flex-direction: $direction;
/* #endif */
}
// 通过scss的mixin功能,把原来需要写4行的css,变成一行
// 目的是保持代码干净整洁,不至于在nvue下,到处都要写display:flex的条件编译
@mixin flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: $direction;
}
@@ -0,0 +1,8 @@
/* H5的时候,隐藏滚动条 */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
@@ -0,0 +1,68 @@
/* start--微信小程序编译后页面有组件名的元素,特别处理--start */
/* #ifdef MP-WEIXIN || MP-QQ */
u-td,
u-th {
flex: 1;
align-self: stretch;
}
u-icon {
display: inline-flex;
align-items: center;
}
// 各家小程序宫格组件外层设置为100%,避免受到父元素display: flex;的影响
u-grid {
width: 100%;
flex: 0 0 100%;
}
// 避免小程序线条组件因为父组件display: flex;而失效
u-line {
flex: 1;
}
u-switch {
display: inline-flex;
align-items: center;
}
u-dropdown {
flex: 1;
}
/* #endif */
/* end-微信小程序编译后页面有组件名的元素,特别处理--end */
/* #ifdef MP-QQ || MP-TOUTIAO */
// 需要做这一切额外的兼容,都是因为TX的无能
u-icon {
line-height: 0;
}
/* #endif */
/* start--头条小程序编译后页面有组件名的元素,特别处理--start */
// 由于头条小程序不支持直接组件名形式写样式,目前只能在写组件的时候给组件加上对应的类名
/* #ifdef MP-TOUTIAO */
.u-td,
.u-th,
.u-tr {
flex: 1;
align-self: stretch;
}
.u-row,
.u-col {
flex: 1;
align-self: stretch;
}
// 避免小程序线条组件因为父组件display: flex;而失效
.u-line {
flex: 1;
}
.u-dropdown {
flex: 1;
}
/* #endif */
/* end-头条小程序编译后页面有组件名的元素,特别处理--end */
@@ -0,0 +1,15 @@
.nvue {
font-size: 24rpx;
}
view,
scroll-view,
swiper-item {
display: flex;
flex-direction: column;
flex-shrink: 0;
flex-grow: 0;
flex-basis: auto;
align-items: stretch;
align-content: flex-start;
}
@@ -0,0 +1,56 @@
/* #ifdef H5 */
:root {
color-scheme: light;
}
:root.u-theme-dark,
:root[data-u-theme-mode='dark'] {
color-scheme: dark;
}
/* #endif */
page {
color: $u-main-color;
background-color: $u-bg-color;
font-size: 28rpx;
}
page.u-theme-dark,
:root.u-theme-dark page,
:root[data-u-theme-mode='dark'] page {
background-color: $u-bg-color;
color: $u-main-color;
}
.u-config-provider {
color: $u-main-color;
background-color: var(--u-bg-white, #ffffff);
}
.u-config-provider.u-theme-dark {
background-color: var(--u-bg-white, #0f1115);
color: $u-main-color;
}
// picker-view遮罩层背景色
.u-picker-view-mask,
.uni-picker-view-mask {
background-image:
linear-gradient(180deg, rgba(var(--u-bg-white-rgb), 0.95), rgba(var(--u-bg-white-rgb), 0.6)),
linear-gradient(0deg, rgba(var(--u-bg-white-rgb), 0.95), rgba(var(--u-bg-white-rgb), 0.6)) !important;
}
.u-picker-view-indicator {
border: rgba(var(--u-bg-white-rgb), 0.6) !important;
}
// picker-view指示器边框色
.u-picker-view-indicator::before,
.uni-picker-view-indicator::before {
border-top: 1px solid var(--u-border-color) !important;
}
.u-picker-view-indicator::after,
.uni-picker-view-indicator::after {
border-bottom: 1px solid var(--u-border-color) !important;
}
@@ -0,0 +1,188 @@
page {
color: $u-main-color;
font-size: 28rpx;
}
/* start--去除webkit的默认样式--start */
.u-fix-ios-appearance {
-webkit-appearance: none;
}
/* end--去除webkit的默认样式--end */
/* start--icon图标外层套一个view,让其达到更好的垂直居中的效果--start */
.u-icon-wrap {
display: flex;
align-items: center;
}
/* end-icon图标外层套一个view,让其达到更好的垂直居中的效果--end */
/* start--iPhoneX底部安全区定义--start */
@each $d in top, right, bottom, left {
.safe-area-inset-#{$d} {
padding-#{$d}: 0;
padding-#{$d}: constant(safe-area-inset-#{$d});
padding-#{$d}: env(safe-area-inset-#{$d});
}
}
/* end-iPhoneX底部安全区定义--end */
/* start--各种hover点击反馈相关的类名-start */
.u-hover-class {
// background-color: #f7f8f9!important;
opacity: 0.6;
}
.u-cell-hover {
background-color: $u-bg-gray-light !important;
}
/* end--各种hover点击反馈相关的类名--end */
/* start--文本行数限制--start */
// 超出行数,自动显示行尾省略号,最多5行
// 来自uView的温馨提示:当您在控制台看到此报错,说明需要在App.vue的style标签加上【lang="scss"】
@for $i from 1 through 5 {
.u-line-#{$i} {
/* #ifdef APP-NVUE */
// nvue下,可以直接使用lines属性,这是weex特有样式
lines: $i;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
// vue下,单行和多行显示省略号需要单独处理
@if $i == 1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} @else {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: $i;
-webkit-box-orient: vertical;
}
/* #endif */
}
}
.u-line-force {
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
}
/* end--文本行数限制--end */
/* start--Retina 屏幕下的 1px 边框--start */
.u-border,
.u-border-bottom,
.u-border-left,
.u-border-right,
.u-border-top,
.u-border-top-bottom {
position: relative;
}
.u-border-bottom:after,
.u-border-left:after,
.u-border-right:after,
.u-border-top-bottom:after,
.u-border-top:after,
.u-border:after {
/* #ifndef APP-NVUE */
content: ' ';
/* #endif */
position: absolute;
left: 0;
top: 0;
pointer-events: none;
box-sizing: border-box;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
// 多加0.1%,能解决有时候边框缺失的问题
width: 199.8%;
height: 199.7%;
transform: scale(0.5, 0.5);
border: 0 solid $u-border-color;
z-index: 2;
}
.u-border-top:after {
border-top-width: 1px;
}
.u-border-left:after {
border-left-width: 1px;
}
.u-border-right:after {
border-right-width: 1px;
}
.u-border-bottom:after {
border-bottom-width: 1px;
}
.u-border-top-bottom:after {
border-width: 1px 0;
}
.u-border:after {
border-width: 1px;
}
/* end--Retina 屏幕下的 1px 边框--end */
/* start--clearfix--start */
.u-clearfix:after,
.clearfix:after {
/* #ifndef APP-NVUE */
content: '';
/* #endif */
display: table;
clear: both;
}
/* end--clearfix--end */
/* start--高斯模糊tabbar底部处理--start */
.u-blur-effect-inset {
width: 750rpx;
height: var(--window-bottom);
background-color: #ffffff;
}
/* end--高斯模糊tabbar底部处理--end */
/* start--提升H5端uni.toast()的层级,避免被uView的modal等遮盖--start */
/* #ifdef H5 */
uni-toast {
z-index: 10090;
}
uni-toast .uni-toast {
z-index: 10090;
}
/* #endif */
/* end--提升H5端uni.toast()的层级,避免被uView的modal等遮盖--end */
/* start--去除button的所有默认样式--start */
// 去除button的所有默认样式,让其表现跟普通的view、text元素一样
.u-reset-button {
padding: 0;
margin: 0;
background-color: transparent;
/* #ifndef APP-PLUS */
font-size: inherit;
line-height: inherit;
color: inherit;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
/* #ifndef APP-NVUE */
.u-reset-button::after {
border: none;
}
/* #endif */
/* end--去除button的所有默认样式--end */
@@ -0,0 +1,24 @@
// 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
// this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
// 这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name
// 值(默认为undefined),就是查找最顶层的$parent
import { type ComponentInternalInstance, getCurrentInstance } from 'vue';
export default function $parent(
componentName?: string,
_instance: ComponentInternalInstance | null | undefined = null
) {
const instance: ComponentInternalInstance | null | undefined = _instance || getCurrentInstance();
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
if (!componentName) return parent;
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
return parent;
}
parent = parent.parent;
}
return null;
}
@@ -0,0 +1,50 @@
import validation from './test';
/**
* 添加单位,支持以下场景:
* 1. 单值:如果有rpx、%、px等单位结尾或者值为auto,直接返回,否则加上rpx单位结尾
* 2. 多值(空格分隔):分别处理每个值,如 "10 20" => "10rpx 20rpx"
*
* @example
* ```ts
* addUnit(10) => "10rpx"
* addUnit('10px') => "10px"
* addUnit('auto') => "auto"
* addUnit('10 20') => "10rpx 20rpx"
* addUnit('10rpx 20') => "10rpx 20rpx"
* ```
*
* @param value 输入值,可以为字符串或数字,默认'auto'。支持空格分隔的多值(用于 padding、margin 等)
* @param unit 单位,默认'rpx'
* @returns 添加单位后的字符串
*/
export default function addUnit(value: string | number = 'auto', unit: string = 'rpx'): string {
// 若传入 number 类型,转为 string
const strValue = String(value);
// 如果是空值,直接返回
if (!strValue) return '';
// auto 直接返回
if (strValue === 'auto') return strValue;
// 检查是否包含空格(多值场景)
if (strValue.includes(' ')) {
// 分割每个值并单独处理
return strValue
.split(' ')
.map(s => {
// 如果当前值已有单位或为 auto,直接返回
if (s === 'auto' || /^-?\d*\.?\d+(%|px|rpx|em|rem|vh|vw)$/.test(s)) return s;
// 数字类型的值添加单位
return validation.number(s) ? `${s}${unit}` : s;
})
.join(' ');
}
// 单值场景:如果已有单位或为 auto,直接返回
if (/^-?\d*\.?\d+(%|px|rpx|em|rem|vh|vw)$/.test(strValue)) return strValue;
// 用 uView 内置验证规则判断是否为数值,是则加上单位
return validation.number(strValue) ? `${strValue}${unit}` : strValue;
}
@@ -0,0 +1,73 @@
function H5Copy(text: string, config: TClipboardOptions) {
const success = (result: string) => {
if (config.showToast) {
uni.showToast({
title: '复制成功',
icon: 'none'
});
}
config.success?.(result);
config.complete?.(result);
};
const fail = (err: string) => {
if (config.showToast) {
uni.showToast({
title: '复制失败',
icon: 'none'
});
}
config.fail?.(err);
config.complete?.(err);
};
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.readOnly = true;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, text.length);
try {
const result = document.execCommand('copy');
if (result) {
success('复制成功');
} else {
// console.error(`复制失败,可能不是用户主动触发点击的方式调用,因web安全性,不能js直接调用!`);
fail('复制失败,可能不是用户主动触发点击的方式调用,因browser安全性,不能js直接调用!');
}
} catch (err) {
// console.error('【Clipboard Error】:', err);
fail(String(err));
} finally {
document.body.removeChild(textarea);
}
}
function UniCopy(text: string, config: TClipboardOptions) {
const opt = Object.assign({ data: text }, config);
uni.setClipboardData(opt);
}
type TClipboardOptions = Omit<UniNamespace.SetClipboardDataOptions, 'data'>;
export function clipboard(content: string, options?: TClipboardOptions) {
const text = String(content);
const defaultOpt = {
showToast: true,
success: () => {},
fail: () => {},
complete: () => {}
};
const config = Object.assign(defaultOpt, options);
// #ifdef H5
H5Copy(text, config);
// #endif
// #ifndef H5
UniCopy(text, config);
// #endif
}
@@ -0,0 +1,28 @@
import type { ColorType, ThemeColor } from '../../types/global';
import { color } from '../config/color';
import { lightPalette } from '../config/theme-tokens';
import configProvider from '../util/config-provider';
/**
* 获取颜色值(响应式)
* 优先从 configProvider 获取当前主题颜色,否则返回默认值
* @param name 颜色名称
* @returns 颜色值
*/
export function getColor(name: ColorType): string {
// 从响应式 color 对象获取(会被 configProvider 更新)
if (color[name]) {
return color[name] as string;
}
// 兜底返回默认值
return lightPalette[name] || '';
}
export function setColor(theme: Partial<ThemeColor> | undefined) {
if (theme) {
configProvider.setThemeColor(theme);
}
}
export default color;
@@ -0,0 +1,139 @@
/**
* 求两个颜色之间的渐变值
* @param startColor 开始的颜色
* @param endColor 结束的颜色
* @param step 颜色等分的份额
* @returns 渐变色数组
*/
function colorGradient(
startColor: string = 'rgb(0, 0, 0)',
endColor: string = 'rgb(255, 255, 255)',
step: number = 10
): string[] {
const startRGB = hexToRgb(startColor, false) as [number, number, number]; // 转换为rgb数组模式
const [startR, startG, startB] = startRGB;
const endRGB = hexToRgb(endColor, false) as [number, number, number];
const [endR, endG, endB] = endRGB;
const sR = (endR - startR) / step; // 总差值
const sG = (endG - startG) / step;
const sB = (endB - startB) / step;
const colorArr: string[] = [];
for (let i = 0; i < step; i++) {
// 计算每一步的hex值
const hex = rgbToHex(
`rgb(${Math.round(sR * i + startR)},${Math.round(sG * i + startG)},${Math.round(sB * i + startB)})`
);
colorArr.push(hex as string);
}
return colorArr;
}
/**
* 将hex表示方式转换为rgb表示方式(返回rgb数组或字符串)
* @param sColor hex或rgb字符串
* @param str 是否返回字符串
* @returns rgb数组或字符串
*/
function hexToRgb(sColor: string, str: boolean = true): [number, number, number] | string {
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
sColor = sColor.toLowerCase();
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
let sColorNew = '#';
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
// 处理六位的颜色值
const sColorChange: [number, number, number] = [
parseInt('0x' + sColor.slice(1, 3)),
parseInt('0x' + sColor.slice(3, 5)),
parseInt('0x' + sColor.slice(5, 7))
];
if (!str) {
return sColorChange;
} else {
return `rgb(${sColorChange[0]},${sColorChange[1]},${sColorChange[2]})`;
}
} else if (/^(rgb|RGB)/.test(sColor)) {
const arr = sColor.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',');
return arr.map(val => Number(val)) as [number, number, number];
} else {
return sColor;
}
}
/**
* rgb转hex
* @param rgb rgb字符串或hex字符串
* @returns hex字符串
*/
function rgbToHex(rgb: string): string | undefined {
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
if (/^(rgb|RGB)/.test(rgb)) {
const aColor = rgb.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',');
let strHex = '#';
for (let i = 0; i < aColor.length; i++) {
let hex = Number(aColor[i]).toString(16);
hex = hex.length == 1 ? '0' + hex : hex; // 保证每个rgb的值为2位
strHex += hex;
}
if (strHex.length !== 7) {
strHex = rgb;
}
return strHex;
} else if (reg.test(rgb)) {
const aNum = rgb.replace(/#/, '').split('');
if (aNum.length === 6) {
return rgb;
} else if (aNum.length === 3) {
let numHex = '#';
for (let i = 0; i < aNum.length; i += 1) {
numHex += aNum[i] + aNum[i];
}
return numHex;
}
} else {
return rgb;
}
// 默认返回原始值
return rgb;
}
/**
* JS颜色十六进制转换为rgb或rgba,返回的格式为 rgba(255,255,255,0.5)字符串
* @param color 十六进制色值或rgb字符串
* @param alpha 透明度
* @returns rgba字符串
*/
function colorToRgba(color: string, alpha: number = 0.3): string {
color = rgbToHex(color) as string; // 确保是hex格式
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
let sColor = color.toLowerCase();
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
let sColorNew = '#';
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
const sColorChange: [number, number, number] = [
parseInt('0x' + sColor.slice(1, 3)),
parseInt('0x' + sColor.slice(3, 5)),
parseInt('0x' + sColor.slice(5, 7))
];
return `rgba(${sColorChange.join(',')},${alpha})`;
} else {
return sColor;
}
}
export default {
colorGradient,
hexToRgb,
rgbToHex,
colorToRgba
};
@@ -0,0 +1,28 @@
let timeout: ReturnType<typeof setTimeout> | null = null;
/**
* 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数
* @param func 要执行的回调函数
* @param wait 延时的时间,单位ms,默认500
* @param immediate 是否立即执行,默认false
* @returns void
*/
function debounce(func: () => void, wait: number = 500, immediate: boolean = false): void {
// 清除定时器
if (timeout !== null) clearTimeout(timeout);
// 立即执行,此类情况一般用不到
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) typeof func === 'function' && func();
} else {
// 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法
timeout = setTimeout(() => {
typeof func === 'function' && func();
}, wait);
}
}
export default debounce;
@@ -0,0 +1,39 @@
// 判断arr是否为一个数组,返回一个bool值
function isArray(arr: any): arr is any[] {
return Object.prototype.toString.call(arr) === '[object Array]';
}
/**
* 深度克隆
* @param obj 需要克隆的对象
* @param cache 用于处理循环引用的 WeakMap
* @returns 克隆后的对象
*/
function deepClone<T>(obj: T, cache: WeakMap<any, any> = new WeakMap()): T {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
let clone: any;
if (obj instanceof Date) {
clone = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
clone = new RegExp(obj);
} else if (obj instanceof Map) {
clone = new Map(Array.from(obj, ([key, value]) => [key, deepClone(value, cache)]));
} else if (obj instanceof Set) {
clone = new Set(Array.from(obj, value => deepClone(value, cache)));
} else if (Array.isArray(obj)) {
clone = obj.map(value => deepClone(value, cache));
} else if (Object.prototype.toString.call(obj) === '[object Object]') {
clone = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, clone);
for (const [key, value] of Object.entries(obj)) {
clone[key] = deepClone(value, cache);
}
} else {
clone = Object.assign({}, obj);
}
cache.set(obj, clone);
return clone;
}
export default deepClone;
@@ -0,0 +1,35 @@
import deepClone from './deepClone';
/**
* JS对象深度合并
* @param target 目标对象
* @param source 源对象
* @returns 合并后的对象
*/
function deepMerge<T extends object, S extends object>(target: T = {} as T, source: S = {} as S): T & S {
target = deepClone(target);
if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null)
return target as T & S;
const merged: any = Array.isArray(target) ? target.slice() : Object.assign({}, target);
for (const prop in source) {
if (!Object.prototype.hasOwnProperty.call(source, prop)) continue;
const sourceValue = (source as any)[prop];
const targetValue = merged[prop];
if (sourceValue instanceof Date) {
merged[prop] = new Date(sourceValue);
} else if (sourceValue instanceof RegExp) {
merged[prop] = new RegExp(sourceValue);
} else if (sourceValue instanceof Map) {
merged[prop] = new Map(sourceValue);
} else if (sourceValue instanceof Set) {
merged[prop] = new Set(sourceValue);
} else if (typeof sourceValue === 'object' && sourceValue !== null) {
merged[prop] = deepMerge(targetValue, sourceValue);
} else {
merged[prop] = sourceValue;
}
}
return merged as T & S;
}
export default deepMerge;
@@ -0,0 +1,63 @@
// 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
// this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
interface VueInstance {
$parent?: VueInstance;
$options?: { name?: string };
[key: string]: any;
}
/**
* 获取父组件参数
* @param this 当前组件实例
* @param name 父组件name
* @param keys 需要获取的参数名数组或对象
* @returns 父组件参数对象
*/
export default function getParent(
this: VueInstance,
name: string,
keys: string[] | Record<string, any>
): Record<string, any> {
let parent = this.$parent;
// 通过while历遍,这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options?.name !== name) {
// 如果组件的name不相等,继续上一级寻找
parent = parent.$parent;
} else {
const data: Record<string, any> = {};
// 判断keys是否数组,如果传过来的是一个数组,那么直接使用数组元素值当做键值去父组件寻找
if (Array.isArray(keys)) {
keys.forEach(val => {
data[val] = parent?.[val] ? parent[val] : '';
});
} else {
// 历遍传过来的对象参数
for (const i in keys) {
// 如果子组件有此值则用,无此值则用父组件的值
// 判断是否空数组,如果是,则用父组件的值,否则用子组件的值
if (Array.isArray(keys[i])) {
if (keys[i].length) {
data[i] = keys[i];
} else {
data[i] = parent[i];
}
} else if (keys[i] && keys[i].constructor === Object) {
// 判断是否对象,如果是对象,且有属性,那么使用子组件的值,否则使用父组件的值
if (Object.keys(keys[i]).length) {
data[i] = keys[i];
} else {
data[i] = parent[i];
}
} else {
// 只要子组件有传值,即使是false值,也是“传值”了,也需要覆盖父组件的同名参数
data[i] = keys[i] || keys[i] === false ? keys[i] : parent[i];
}
}
}
return data;
}
}
return {};
}
@@ -0,0 +1,26 @@
/**
* 获取元素的位置信息
* @param {any} selector 选择器
* @param {boolean} all 是否获取所有匹配元素
* @returns {Promise<any>} 返回一个 Promise,解析为元素的位置信息
*/
import { getCurrentInstance } from 'vue';
export default function (selector: any, _instance: any = null, all: boolean = false): Promise<any> {
const instance = _instance || getCurrentInstance();
return new Promise(resolve => {
uni.createSelectorQuery()
.in(instance?.proxy)
[all ? 'selectAll' : 'select'](selector)
.boundingClientRect((rect: any) => {
if (all && Array.isArray(rect) && rect.length) {
resolve(rect);
}
if (!all && rect) {
resolve(rect);
}
})
.exec();
});
}
@@ -0,0 +1,42 @@
/**
* 本算法来源于简书开源代码,详见:https://www.jianshu.com/p/fdbf293d0a85
* 全局唯一标识符(uuid,Globally Unique Identifier),也称作 uuid(Universally Unique IDentifier)
* 一般用于多个组件之间,给它一个唯一的标识符,或者v-for循环的时候,如果使用数组的index可能会导致更新列表出现问题
* 最可能的情况是左滑删除item或者对某条信息流"不喜欢"并去掉它的时候,会导致组件内的数据可能出现错乱
* v-for的时候,推荐使用后端返回的id而不是循环的index
* @param len uuid的长度,默认32
* @param firstU 将返回的首字母置为"u",默认true
* @param radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
* @returns 生成的uuid字符串
*/
function guid(len: number = 32, firstU: boolean = true, radix?: number): string {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid: string[] = [];
const base = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * base)];
} else {
let r: number;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guid不能用作id或者class
if (firstU) {
uuid.shift();
return 'u' + uuid.join('');
} else {
return uuid.join('');
}
}
export default guid;
@@ -0,0 +1,403 @@
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
let hexcase: number = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
let b64pad: string = ''; /* base-64 pad character. "=" for strict RFC compliance */
/**
* 这些是通常需要调用的函数
* @param s 输入字符串
* @returns 十六进制/BASE64/任意编码的MD5字符串
*/
function hex_md5(s: string): string {
return rstr2hex(rstr_md5(str2rstr_utf8(s)));
}
function b64_md5(s: string): string {
return rstr2b64(rstr_md5(str2rstr_utf8(s)));
}
function any_md5(s: string, e: string): string {
return rstr2any(rstr_md5(str2rstr_utf8(s)), e);
}
function hex_hmac_md5(k: string, d: string): string {
return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
}
function b64_hmac_md5(k: string, d: string): string {
return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
}
function any_hmac_md5(k: string, d: string, e: string): string {
return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e);
}
/**
* 执行简单自测,判断 VM 是否正常
* @returns 是否通过测试
*/
function md5_vm_test(): boolean {
return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72';
}
/**
* 计算原始字符串的 MD5
* @param s 原始字符串
* @returns MD5 结果字符串
*/
function rstr_md5(s: string): string {
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
}
/**
* 计算 HMAC-MD5
* @param key 密钥
* @param data 数据
* @returns HMAC-MD5 结果字符串
*/
function rstr_hmac_md5(key: string, data: string): string {
let bkey: number[] = rstr2binl(key);
if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
const ipad: number[] = Array(16),
opad: number[] = Array(16);
for (let i = 0; i < 16; i++) {
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5c5c5c5c;
}
const hash: number[] = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
}
/**
* 原始字符串转十六进制字符串
* @param input 输入字符串
* @returns 十六进制字符串
*/
function rstr2hex(input: string): string {
try {
hexcase;
} catch (e) {
hexcase = 0;
}
const hex_tab: string = hexcase ? '0123456789ABCDEF' : '0123456789abcdef';
let output = '';
let x: number;
for (let i = 0; i < input.length; i++) {
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f);
}
return output;
}
/**
* 原始字符串转 BASE64 字符串
* @param input 输入字符串
* @returns BASE64 字符串
*/
function rstr2b64(input: string): string {
try {
b64pad;
} catch (e) {
b64pad = '';
}
const tab: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let output = '';
const len: number = input.length;
for (let i = 0; i < len; i += 3) {
const triplet: number =
(input.charCodeAt(i) << 16) |
(i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) |
(i + 2 < len ? input.charCodeAt(i + 2) : 0);
for (let j = 0; j < 4; j++) {
if (i * 8 + j * 6 > input.length * 8) output += b64pad;
else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f);
}
}
return output;
}
/**
* 原始字符串转任意编码字符串
* @param input 输入字符串
* @param encoding 编码表
* @returns 编码后的字符串
*/
function rstr2any(input: string, encoding: string): string {
const divisor: number = encoding.length;
let i: number, j: number, q: number, x: number, quotient: number[];
/* Convert to an array of 16-bit big-endian values, forming the dividend */
let dividend: number[] = Array(Math.ceil(input.length / 2));
for (i = 0; i < dividend.length; i++) {
dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
}
/*
* Repeatedly perform a long division. The binary array forms the dividend,
* the length of the encoding is the divisor. Once computed, the quotient
* forms the dividend for the next step. All remainders are stored for later
* use.
*/
const full_length: number = Math.ceil((input.length * 8) / (Math.log(encoding.length) / Math.log(2)));
const remainders: number[] = Array(full_length);
for (j = 0; j < full_length; j++) {
quotient = [];
x = 0;
for (i = 0; i < dividend.length; i++) {
x = (x << 16) + dividend[i];
q = Math.floor(x / divisor);
x -= q * divisor;
if (quotient.length > 0 || q > 0) quotient[quotient.length] = q;
}
remainders[j] = x;
dividend = quotient;
}
/* Convert the remainders to the output string */
let output = '';
for (i = remainders.length - 1; i >= 0; i--) output += encoding.charAt(remainders[i]);
return output;
}
/**
* 字符串转 UTF-8 编码
* @param input 输入字符串
* @returns UTF-8 编码字符串
*/
function str2rstr_utf8(input: string): string {
let output = '';
let i = -1;
let x: number, y: number;
while (++i < input.length) {
/* Decode utf-16 surrogate pairs */
x = input.charCodeAt(i);
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) {
x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff);
i++;
}
/* Encode output as utf-8 */
if (x <= 0x7f) output += String.fromCharCode(x);
else if (x <= 0x7ff) output += String.fromCharCode(0xc0 | ((x >>> 6) & 0x1f), 0x80 | (x & 0x3f));
else if (x <= 0xffff)
output += String.fromCharCode(0xe0 | ((x >>> 12) & 0x0f), 0x80 | ((x >>> 6) & 0x3f), 0x80 | (x & 0x3f));
else if (x <= 0x1fffff)
output += String.fromCharCode(
0xf0 | ((x >>> 18) & 0x07),
0x80 | ((x >>> 12) & 0x3f),
0x80 | ((x >>> 6) & 0x3f),
0x80 | (x & 0x3f)
);
}
return output;
}
/**
* 字符串转 UTF-16LE 编码
* @param input 输入字符串
* @returns UTF-16LE 编码字符串
*/
function str2rstr_utf16le(input: string): string {
let output = '';
for (let i = 0; i < input.length; i++)
output += String.fromCharCode(input.charCodeAt(i) & 0xff, (input.charCodeAt(i) >>> 8) & 0xff);
return output;
}
/**
* 字符串转 UTF-16BE 编码
* @param input 输入字符串
* @returns UTF-16BE 编码字符串
*/
function str2rstr_utf16be(input: string): string {
let output = '';
for (let i = 0; i < input.length; i++)
output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xff, input.charCodeAt(i) & 0xff);
return output;
}
/**
* 原始字符串转小端字数组
* @param input 输入字符串
* @returns 小端字数组
*/
function rstr2binl(input: string): number[] {
const output: number[] = Array(input.length >> 2);
for (let i = 0; i < output.length; i++) output[i] = 0;
for (let i = 0; i < input.length * 8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
return output;
}
/**
* 小端字数组转字符串
* @param input 小端字数组
* @returns 字符串
*/
function binl2rstr(input: number[]): string {
let output = '';
for (let i = 0; i < input.length * 32; i += 8) output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
return output;
}
/**
* 计算小端字数组的 MD5
* @param x 小端字数组
* @param len 位长度
* @returns MD5 结果数组
*/
function binl_md5(x: number[], len: number): number[] {
/* append padding */
x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len;
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (let i = 0; i < x.length; i += 16) {
const olda = a;
const oldb = b;
const oldc = c;
const oldd = d;
a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return [a, b, c, d];
}
/**
* 四种基本操作
*/
function md5_cmn(q: number, a: number, b: number, x: number, s: number, t: number): number {
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
}
function md5_ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
}
function md5_gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function md5_hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
}
/**
* 安全整数加法,防止溢出
* @param x 整数
* @param y 整数
* @returns 加法结果
*/
function safe_add(x: number, y: number): number {
const lsw: number = (x & 0xffff) + (y & 0xffff);
const msw: number = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
/**
* 左移位操作
* @param num 数字
* @param cnt 位数
* @returns 结果
*/
function bit_rol(num: number, cnt: number): number {
return (num << cnt) | (num >>> (32 - cnt));
}
/**
* 计算字符串的 MD5 值
* @param str 输入字符串
* @returns MD5 十六进制字符串
*/
function md5(str: string): string {
return hex_md5(str);
}
export default {
md5
};
@@ -0,0 +1,21 @@
import { type ComponentInternalInstance, getCurrentInstance } from 'vue';
export function parent(componentName?: string, _instance: ComponentInternalInstance | null | undefined = null) {
const instance = _instance || getCurrentInstance();
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
return parent;
}
parent = parent.parent;
}
return null;
}
export function parentData(componentName?: string, _instance: ComponentInternalInstance | null | undefined = null) {
const instance = _instance || getCurrentInstance();
const findParent = parent(componentName, instance);
return findParent ? findParent.exposed : null;
}
@@ -0,0 +1,64 @@
/**
* 对象转url参数
* @param data 对象参数
* @param isPrefix 是否自动加上"?",默认true
* @param arrayFormat 数组参数格式,indices/brackets/repeat/comma,默认brackets
* @returns url参数字符串
*/
function queryParams(
data: Record<string, any> = {},
isPrefix: boolean = true,
arrayFormat: 'indices' | 'brackets' | 'repeat' | 'comma' = 'brackets'
): string {
const prefix = isPrefix ? '?' : '';
const _result: string[] = [];
if (!['indices', 'brackets', 'repeat', 'comma'].includes(arrayFormat)) arrayFormat = 'brackets';
for (const key in data) {
const value = data[key];
// 去掉为空的参数
if (['', undefined, null].includes(value)) {
continue;
}
// 如果值为数组,另行处理
if (Array.isArray(value)) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(`${key}[${i}]=${value[i]}`);
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach((_value: any) => {
_result.push(`${key}[]=${_value}`);
});
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach((_value: any) => {
_result.push(`${key}=${_value}`);
});
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = '';
value.forEach((_value: any) => {
commaStr += (commaStr ? ',' : '') + _value;
});
_result.push(`${key}=${commaStr}`);
break;
default:
value.forEach((_value: any) => {
_result.push(`${key}[]=${_value}`);
});
}
} else {
_result.push(`${key}=${value}`);
}
}
return _result.length ? prefix + _result.join('&') : '';
}
export default queryParams;
@@ -0,0 +1,16 @@
/**
* 生成指定范围的随机整数
* @param min 最小值(包含)
* @param max 最大值(包含)
* @returns 随机整数
*/
function random(min: number, max: number): number {
if (min >= 0 && max > 0 && max >= min) {
const gab = max - min + 1;
return Math.floor(Math.random() * gab + min);
} else {
return 0;
}
}
export default random;
@@ -0,0 +1,11 @@
/**
* 打乱数组顺序
* @param array 需要打乱的数组
* @returns 打乱后的新数组
*/
function randomArray<T>(array: T[] = []): T[] {
// 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.5大于或者小于0
return array.sort(() => Math.random() - 0.5);
}
export default randomArray;
@@ -0,0 +1,118 @@
/**
* 路由跳转方法,该方法相对于直接使用uni.xxx的好处是使用更加简单快捷
* 并且带有路由拦截功能
*/
interface RouterConfig {
type?: string;
url?: string;
delta?: number;
params?: Record<string, any>;
animationType?: string;
animationDuration?: number;
intercept?: boolean;
}
declare const uni: any; // 声明uni对象,避免ts报错
class Router {
config: RouterConfig;
// route: (options?: string | RouterConfig, params?: Record<string, any>) => Promise<void>;
constructor() {
// 原始属性定义
this.config = {
type: 'navigateTo',
url: '',
delta: 1, // navigateBack页面后退时,回退的层数
params: {}, // 传递的参数
animationType: 'pop-in', // 窗口动画,只在APP有效
animationDuration: 300, // 窗口动画持续时间,单位毫秒,只在APP有效
intercept: false // 是否需要拦截
};
// 因为route方法是需要对外赋值给另外的对象使用,同时route内部有使用this,会导致route失去上下文
// 这里在构造函数中进行this绑定
this.route = this.route.bind(this);
}
// 判断url前面是否有"/",如果没有则加上,否则无法跳转
addRootPath(url: string): string {
return url[0] === '/' ? url : `/${url}`;
}
// 整合路由参数
mixinParam(url: string, params: Record<string, any>): string {
url = url && this.addRootPath(url);
// 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
// 如果有url中有get参数,转换后无需带上"?"
let query = '';
if (/.*\/.*\?.*=.*/.test(url)) {
// object对象转为get类型的参数
query = uni.$u.queryParams(params, false);
// 因为已有get参数,所以后面拼接的参数需要带上"&"隔开
return url + '&' + query;
} else {
// 直接拼接参数,因为此处url中没有后面的query参数,也就没有"?/&"之类的符号
query = uni.$u.queryParams(params);
return url + query;
}
}
/**
* 路由跳转主方法
* @param options 跳转配置或url字符串
* @param params 跳转参数
*/
async route(options: string | RouterConfig = {}, params: Record<string, any> = {}): Promise<void> {
let mergeConfig: RouterConfig = {};
if (typeof options === 'string') {
// 如果options为字符串,则为route(url, params)的形式
mergeConfig.url = this.mixinParam(options, params);
mergeConfig.type = 'navigateTo';
} else {
mergeConfig = uni.$u.deepMerge(this.config, options);
// 否则正常使用mergeConfig中的url和params进行拼接
mergeConfig.url = this.mixinParam(options.url || '', options.params || {});
}
if (params.intercept) {
this.config.intercept = params.intercept;
}
// params参数也带给拦截器
mergeConfig.params = params;
// 合并内外部参数
mergeConfig = uni.$u.deepMerge(this.config, mergeConfig);
// 判断用户是否定义了拦截器
if (uni.$u.routeIntercept && typeof uni.$u.routeIntercept === 'function') {
// 定一个promise,根据用户执行resolve(true)或者resolve(false)来决定是否进行路由跳转
const isNext = await new Promise<boolean>(resolve => {
uni.$u.routeIntercept(mergeConfig, resolve);
});
// 如果isNext为true,则执行路由跳转
isNext && this.openPage(mergeConfig);
} else {
this.openPage(mergeConfig);
}
}
// 执行路由跳转
openPage(config: RouterConfig): void {
const { url = '', type = '', delta = 1, animationDuration = 300 } = config;
if (type == 'navigateTo' || type == 'to') {
uni.navigateTo({ url, animationDuration });
}
if (type == 'redirectTo' || type == 'redirect') {
uni.redirectTo({ url });
}
if (type == 'switchTab' || type == 'tab') {
uni.switchTab({ url });
}
if (type == 'reLaunch' || type == 'launch') {
uni.reLaunch({ url });
}
if (type == 'navigateBack' || type == 'back') {
uni.navigateBack({ delta });
}
}
}
export default new Router().route;
@@ -0,0 +1,83 @@
import type { CSSProperties } from 'vue';
import trim from './trim';
import test from './test';
function isValidValue(value: any): boolean {
if (test.isEmpty(value)) return false;
if (typeof value === 'string') {
const trimmed = trim(value).toLowerCase();
return trimmed !== 'null' && trimmed !== 'undefined' && trimmed !== '';
}
return true;
}
/**
* 将 CSS 字符串解析为对象
*/
function cssStrToObj(str: string): object {
const result = {};
if (!isValidValue(str)) return result;
const styleStr = trim(String(str));
if (!isValidValue(styleStr)) return result;
const declarations = styleStr.split(';');
declarations.forEach(decl => {
const [prop, ...values] = decl.split(':').map(s => s.trim());
const value = values.join(':');
if (prop && value) {
const camelProp = prop.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
(result as Record<string, string>)[camelProp] = value;
}
});
return result;
}
/**
* 将 CSS 对象转换为 CSS 字符串
*/
function cssObjToStr(obj: object): string {
if (!isValidValue(obj) || typeof obj !== 'object') return '';
return (
Object.entries(obj)
.map(([key, value]) => {
if (!isValidValue(value)) return '';
// 驼峰转短横线
const kebab = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${kebab}: ${value}`;
})
.filter(Boolean)
.join('; ') + ';'
);
}
/**
* 合并多个 CSS 输入(对象或字符串),返回一个 CSS 对象
*/
export function mergeStyles(...args: (object | string)[]): CSSProperties {
const result = {} as Record<string, any>;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!isValidValue(arg)) return result as CSSProperties;
if (typeof arg === 'string') {
Object.assign(result, cssStrToObj(arg));
} else if (test.object(arg)) {
const cleanedObj: Record<string, any> = {};
Object.keys(arg).forEach(key => {
const value = (arg as Record<string, any>)[key];
if (isValidValue(value)) {
// 有效
cleanedObj[key] = value;
}
});
Object.assign(result, cleanedObj);
}
}
return result;
}
/**
* 合并为 CSS 字符串
*/
export function mergeCssStrings(...args: (object | string)[]): string {
const obj = mergeStyles(...args);
return cssObjToStr(obj);
}
@@ -0,0 +1,15 @@
/**
* 获取当前操作系统平台
* @returns 平台字符串,如 'ios'、'android'、'windows' 等
*/
export function os(): string {
return uni.getSystemInfoSync().platform;
}
/**
* 获取系统信息
* @returns uniapp 系统信息对象
*/
export function sys(): UniApp.GetSystemInfoResult {
return uni.getSystemInfoSync();
}
@@ -0,0 +1,289 @@
/**
* 验证电子邮箱格式
*/
function email(value: string): boolean {
return /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/.test(
value
);
}
/**
* 验证手机格式
*/
function mobile(value: string): boolean {
return /^1[3-9]\d{9}$/.test(value);
}
/**
* 验证URL格式
*/
function url(value: string): boolean {
return /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w-.\/?%&=]*)?/.test(value);
}
/**
* 验证日期格式
*/
function date(value: string): boolean {
return !/Invalid|NaN/.test(new Date(value).toString());
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value: string): boolean {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value);
}
/**
* 验证十进制数字
*/
function number(value: string): boolean {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value);
}
/**
* 验证整数
*/
function digits(value: string): boolean {
return /^\d+$/.test(value);
}
/**
* 验证身份证号码
*/
function idCard(value: string): boolean {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value);
}
/**
* 是否车牌号
*/
function carNo(value: string): boolean {
// 新能源车牌
const xreg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
// 旧车牌
const creg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
if (value.length === 7) {
return creg.test(value);
} else if (value.length === 8) {
return xreg.test(value);
} else {
return false;
}
}
/**
* 金额,只允许2位小数
*/
function amount(value: string): boolean {
//金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
}
/**
* 中文
*/
function chinese(value: string): boolean {
let reg = /^[\u4e00-\u9fa5]+$/gi;
return reg.test(value);
}
/**
* 只能输入字母
*/
function letter(value: string): boolean {
return /^[a-zA-Z]*$/.test(value);
}
/**
* 只能是字母或者数字
*/
function enOrNum(value: string): boolean {
//英文或者数字
let reg = /^[0-9a-zA-Z]*$/g;
return reg.test(value);
}
/**
* 验证是否包含某个值
*/
function contains(value: string, param: string): boolean {
return value.indexOf(param) >= 0;
}
/**
* 验证一个值范围[min, max]
*/
function range(value: number, param: [number, number]): boolean {
return value >= param[0] && value <= param[1];
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value: string, param: [number, number]): boolean {
return value.length >= param[0] && value.length <= param[1];
}
/**
* 是否固定电话
*/
function landline(value: string): boolean {
let reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
return reg.test(value);
}
/**
* 判断是否为空
*/
function empty(value: any): boolean {
switch (typeof value) {
case 'undefined':
return true;
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!value) return true;
break;
case 'number':
if (0 === value || isNaN(value)) return true;
break;
case 'object':
if (null === value || value.length === 0) return true;
for (var i in value) {
return false;
}
return true;
}
return false;
}
/**
* 是否json字符串
*/
function jsonString(value: string): boolean {
if (typeof value == 'string') {
try {
var obj = JSON.parse(value);
if (typeof obj == 'object' && obj) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
return false;
}
/**
* 是否数组
*/
function array(value: any): boolean {
if (typeof Array.isArray === 'function') {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === '[object Array]';
}
}
/**
* 是否对象
*/
function object(value: any): boolean {
return Object.prototype.toString.call(value) === '[object Object]';
}
/**
* 是否短信验证码
*/
function code(value: string, len: number = 6): boolean {
return new RegExp(`^\\d{${len}}$`).test(value);
}
/**
* 是否函数方法
* @param {Object} value
*/
function func(value: any) {
return typeof value === 'function';
}
/**
* 是否promise对象
* @param {Object} value
*/
function promise(value: any) {
return object(value) && func(value.then) && func(value.catch);
}
/** 是否图片格式
* @param {Object} value
*/
function image(value: any) {
const newValue = value.split('?')[0];
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i;
return IMAGE_REGEXP.test(newValue);
}
/**
* 是否视频格式
* @param {Object} value
*/
function video(value: any) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i;
return VIDEO_REGEXP.test(value);
}
/**
* 是否为正则对象
* @param {Object}
* @return {Boolean}
*/
function regExp(o: any) {
return o && Object.prototype.toString.call(o) === '[object RegExp]';
}
/**
* 验证字符串
*/
function string(value: any) {
return typeof value === 'string';
}
export default {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
isEmpty: empty,
jsonString,
landline,
object,
array,
code,
func,
promise,
video,
image,
regExp,
string
};
@@ -0,0 +1,31 @@
let timer: ReturnType<typeof setTimeout> | undefined;
let flag: boolean | undefined;
/**
* 节流原理:在一定时间内,只能触发一次
* @param func 要执行的回调函数
* @param wait 延时的时间,单位ms,默认500
* @param immediate 是否立即执行,默认true
* @returns void
*/
function throttle(func: () => void, wait: number = 500, immediate: boolean = true): void {
if (immediate) {
if (!flag) {
flag = true;
// 如果是立即执行,则在wait毫秒内开始时执行
typeof func === 'function' && func();
timer = setTimeout(() => {
flag = false;
}, wait);
}
} else {
if (!flag) {
flag = true;
// 如果是非立即执行,则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false;
typeof func === 'function' && func();
}, wait);
}
}
}
export default throttle;
@@ -0,0 +1,55 @@
// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序
// 所以这里做一个兼容polyfill的兼容处理
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (this: string, maxLength: number, fillString: string = ' '): string {
if (Object.prototype.toString.call(fillString) !== '[object String]')
throw new TypeError('fillString must be String');
let str = this;
if (str.length >= maxLength) return String(str);
let fillLength = maxLength - str.length,
times = Math.ceil(fillLength / fillString.length);
while ((times >>= 1)) {
fillString += fillString;
if (times === 1) {
fillString += fillString;
}
}
return fillString.slice(0, fillLength) + str;
};
}
/**
* 时间格式化
* @param dateTime 时间戳、Date对象或null,默认当前时间
* @param fmt 格式化字符串,默认 'yyyy-mm-dd'
* @returns 格式化后的时间字符串
*/
function timeFormat(dateTime: number | string | Date | null = null, fmt: string = 'yyyy-mm-dd'): string {
// 如果为null,则格式化当前时间
if (!dateTime) dateTime = Number(new Date());
// 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式
if (typeof dateTime === 'number' || typeof dateTime === 'string') {
if (dateTime.toString().length == 10) dateTime = Number(dateTime) * 1000;
}
const date = new Date(dateTime);
let ret: RegExpExecArray | null;
const opt: Record<string, string> = {
'y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'h+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString() // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
}
}
return fmt;
}
export default timeFormat;
@@ -0,0 +1,48 @@
import timeFormat from './timeFormat';
/**
* 时间戳转为多久之前
* @param dateTime 时间戳、Date对象或null,默认当前时间
* @param format 时间格式字符串或false,超出范围时返回指定格式,否则返回多久以前
* @returns 格式化后的时间字符串
*/
function timeFrom(dateTime: number | string | Date | null = null, format: string | false = 'yyyy-mm-dd'): string {
// 如果为null,则格式化当前时间
if (!dateTime) dateTime = Number(new Date());
// 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式
if (typeof dateTime === 'number' || typeof dateTime === 'string') {
if (dateTime.toString().length == 10) dateTime = Number(dateTime) * 1000;
}
const timestamp = +new Date(Number(dateTime));
const timer = (Number(new Date()) - timestamp) / 1000;
// 如果小于5分钟,则返回"刚刚",其他以此类推
let tips = '';
switch (true) {
case timer < 300:
tips = '刚刚';
break;
case timer >= 300 && timer < 3600:
tips = parseInt(String(timer / 60)) + '分钟前';
break;
case timer >= 3600 && timer < 86400:
tips = parseInt(String(timer / 3600)) + '小时前';
break;
case timer >= 86400 && timer < 2592000:
tips = parseInt(String(timer / 86400)) + '天前';
break;
default:
// 如果format为false,则无论什么时间戳,都显示xx之前
if (format === false) {
if (timer >= 2592000 && timer < 365 * 86400) {
tips = parseInt(String(timer / (86400 * 30))) + '个月前';
} else {
tips = parseInt(String(timer / (86400 * 365))) + '年前';
}
} else {
tips = timeFormat(timestamp, format as string);
}
}
return tips;
}
export default timeFrom;
@@ -0,0 +1,14 @@
/**
* 显示无图标的 Toast 提示
* @param title 提示文本
* @param option 显示时长(毫秒)默认1500 /显示图标,默认为none,
*/
function toast(title: string, option: number | string | Record<string, any> = 1500): void {
uni.showToast({
title: title,
icon: typeof option === 'string' ? option : typeof option === 'object' ? option.icon || 'none' : 'none',
duration: typeof option === 'number' ? option : typeof option === 'object' ? option.duration || '1500' : 1500
});
}
export default toast;
@@ -0,0 +1,21 @@
/**
* 去除字符串空格
* @param str 输入字符串
* @param pos 去除位置,'both' | 'left' | 'right' | 'all',默认'both'
* @returns 去除空格后的字符串
*/
function trim(str: string, pos: 'both' | 'left' | 'right' | 'all' = 'both'): string {
if (pos === 'both') {
return str.replace(/^\s+|\s+$/g, '');
} else if (pos === 'left') {
return str.replace(/^\s*/, '');
} else if (pos === 'right') {
return str.replace(/(\s*$)/g, '');
} else if (pos === 'all') {
return str.replace(/\s+/g, '');
} else {
return str;
}
}
export default trim;
@@ -0,0 +1,38 @@
import type { ThemeType } from '../../types/global';
/**
* 根据主题type值,获取对应的图标
* @param type 主题名称, primary|info|error|warning|success,默认success
* @param fill 是否使用fill填充实体的图标,默认false
* @returns 图标名称字符串
*/
function type2icon(type: ThemeType = 'success', fill: boolean = false): string {
// 如果非预置值,默认为success
if (!['primary', 'info', 'error', 'warning', 'success'].includes(type)) type = 'success';
let iconName = '';
// 目前(2019-12-12),info和primary使用同一个图标
switch (type) {
case 'primary':
iconName = 'info-circle';
break;
case 'info':
iconName = 'info-circle';
break;
case 'error':
iconName = 'close-circle';
break;
case 'warning':
iconName = 'error-circle';
break;
case 'success':
iconName = 'checkmark-circle';
break;
default:
iconName = 'checkmark-circle';
}
// 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的
if (fill) iconName += '-fill';
return iconName;
}
export default type2icon;
@@ -0,0 +1,10 @@
export * from './useEmitter';
export * from './useRect';
export * from './useCompRelation';
export * from './useTheme';
export * from './useColor';
export * from './useLocale';
export * from './useDebounce';
export * from './useThrottle';
export * from './useToast';
export * from './useModal';
@@ -0,0 +1,61 @@
/**
* 响应式颜色管理 composable
* 提供响应式的颜色访问,支持主题切换和暗黑模式
*
* 使用方式:
* const { color, getColor } = useColor()
* // color 是响应式的,可以直接在模板中使用
* // getColor('primary') 返回响应式的颜色值
*/
import { computed, type ComputedRef } from 'vue';
import type { ColorType } from '../../types/global';
import configProvider from '../util/config-provider';
/**
* 响应式颜色 composable
* 返回响应式的颜色对象和获取颜色的方法
*/
export function useColor() {
// 从 configProvider 获取当前主题的响应式引用
const currentTheme = configProvider.currentThemeRef;
const darkModeRef = configProvider.darkModeRef;
/**
* 获取当前激活模式下的颜色对象(响应式)
*/
const color = computed(() => {
const theme = currentTheme.value;
if (!theme) return {} as Record<ColorType, string>;
const isDark = configProvider.isInDarkMode();
const palette =
isDark && theme.darkColor && Object.keys(theme.darkColor).length ? theme.darkColor : theme.color || {};
// 合并默认值,确保所有颜色都有值
const defaultPalette = isDark
? (configProvider as any).baseDarkColorTokens || {}
: (configProvider as any).baseColorTokens || {};
return {
...defaultPalette,
...palette
} as Record<ColorType, string>;
});
/**
* 获取指定颜色(响应式)
* @param name 颜色名称
* @returns 颜色值
*/
const getColor = (name: ColorType): ComputedRef<string> => {
return computed(() => {
return color.value[name] || '';
});
};
return {
color,
getColor
};
}
@@ -0,0 +1,424 @@
import { ref, reactive, getCurrentInstance, onUnmounted, nextTick, computed, onMounted } from 'vue';
import { isDebugMode } from '../config/config';
// 类型定义
interface ParentContext {
name: string;
addChild: (child: ChildContext) => void;
removeChild: (childId: string) => void;
broadcast: (event: string, data?: any, childIds?: string | string[]) => void;
broadcastToChildren: (componentName: string, event: string, data?: any) => void;
getChildren: () => ChildContext[];
getExposed: () => Record<string, any>;
getChildExposed: (childId: string) => Record<string, any>;
getChildrenExposed: () => Array<{ id: string; name: string; exposed: Record<string, any> }>;
getInstance: () => any;
}
interface ChildContext {
id: string;
name: string;
getChildIndex: () => number;
emitToParent: (event: string, data?: any) => void;
getParentExposed: () => Record<string, any>;
getInstance: () => any;
getExposed: () => Record<string, any>;
}
// 符号定义
const PARENT_CONTEXT_SYMBOL = Symbol('parent_context');
// 每个组件实例的 child id 缓存,避免同个组件中多次调用 useChildren 时生成不同 id
const CHILD_ID_SYMBOL = Symbol('child_id');
/**
* 生成实例唯一ID
*/
function generateInstanceId(componentName: string): string {
return `${componentName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 查找父组件实例
*/
function findParentInstance(name: string, instance: any): any {
if (!instance) return null;
let parent = instance.parent;
while (parent) {
const parentName = parent.type?.name || parent.type?.__name;
if (parentName === name) {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* 获取父组件上下文
*/
function getParentContext(name: string, instance: any): ParentContext | null {
const parentInstance = findParentInstance(name, instance);
return parentInstance?.proxy?.[PARENT_CONTEXT_SYMBOL] || null;
}
/**
* 递归查找所有指定名称的子组件
*/
function findAllChildComponents(componentName: string, instance: any): any[] {
const components: any[] = [];
function traverse(currentInstance: any) {
if (!currentInstance?.subTree) return;
const subTree = currentInstance.subTree?.children || [];
const children = Array.isArray(subTree) ? subTree : [subTree];
children.forEach((vnode: any) => {
const child = vnode.component;
if (!child) return;
const name = child.type?.name || child.type?.__name;
if (name === componentName) {
components.push(child);
}
traverse(child);
});
}
traverse(instance);
if (isDebugMode()) console.log(`Found ${components.length} ${componentName} components`);
return components;
}
/**
* 父组件 Hook
*/
export function useParent(componentName?: string) {
const instance = getCurrentInstance();
if (!instance) {
throw new Error('useParent must be called within setup function');
}
const name = componentName || instance.type.name || instance.type.__name;
if (!name) {
throw new Error('Component name is required for useParent');
}
const children = reactive<ChildContext[]>([]);
const childrenMap = new Map<string, ChildContext>();
const broadcast = (event: string, data?: any, childIds?: string | string[]) => {
const targetChildren = childIds
? ((Array.isArray(childIds) ? childIds : [childIds])
.map(id => childrenMap.get(id))
.filter(Boolean) as ChildContext[])
: Array.from(childrenMap.values());
if (isDebugMode())
console.log(`Parent ${name} broadcasting event: ${event} to ${targetChildren.length} children`);
targetChildren.forEach(child => {
const exposed = child.getExposed();
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data);
} catch (error) {
if (isDebugMode('error')) console.error(`Error calling child method ${event}:`, error);
}
}
});
};
const broadcastToChildren = (componentName: string, event: string, data?: any) => {
if (isDebugMode())
console.log(`Parent ${name} broadcasting event: ${event} to all ${componentName} components`);
const childComponents = findAllChildComponents(componentName, instance);
let successCount = 0;
childComponents.forEach(childComponent => {
const exposed = childComponent.exposed || childComponent.proxy;
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data);
successCount++;
} catch (error) {
if (isDebugMode('error')) console.error(`Error calling ${componentName} method ${event}:`, error);
}
}
});
if (isDebugMode())
console.log(
`Parent ${name} successfully called ${successCount} of ${childComponents.length} ${componentName} components`
);
};
const parentContext: ParentContext = {
name,
addChild(child: ChildContext) {
if (!childrenMap.has(child.id)) {
childrenMap.set(child.id, child);
children.push(child);
if (isDebugMode()) console.log(`Parent ${name} added child: ${child.name}`);
}
},
removeChild(childId: string) {
if (childrenMap.has(childId)) {
const child = childrenMap.get(childId)!;
childrenMap.delete(childId);
const index = children.findIndex(c => c.id === childId);
if (index > -1) children.splice(index, 1);
if (isDebugMode()) console.log(`Parent ${name} removed child: ${childId}`);
}
},
broadcast,
broadcastToChildren,
getChildren: () => Array.from(childrenMap.values()),
getExposed: () => instance.exposed || {},
getChildExposed(childId: string) {
const child = childrenMap.get(childId);
return child?.getExposed?.() || {};
},
getChildrenExposed() {
return Array.from(childrenMap.values())
.filter(child => child.getExposed)
.map(child => ({
id: child.id,
name: child.name,
exposed: child.getExposed()
}))
.filter(item => Object.keys(item.exposed).length > 0);
},
getInstance: () => instance
};
if (instance.proxy) {
(instance.proxy as any)[PARENT_CONTEXT_SYMBOL] = parentContext;
}
onUnmounted(() => {
childrenMap.forEach((_, childId) => parentContext.removeChild(childId));
if (instance.proxy) {
delete (instance.proxy as any)[PARENT_CONTEXT_SYMBOL];
}
if (isDebugMode()) console.log(`Parent ${name} unmounted and cleaned up`);
});
return {
parentName: name,
children,
broadcast,
broadcastToChildren,
getChildren: parentContext.getChildren,
getChildExposed: parentContext.getChildExposed,
getChildrenExposed: parentContext.getChildrenExposed,
getExposed: parentContext.getExposed,
getInstance: parentContext.getInstance
};
}
/**
* 子组件 Hook
*/
export function useChildren(componentName?: string, parentName?: string) {
const instance = getCurrentInstance();
if (!instance) {
throw new Error('useChildren must be called within setup function');
}
const name = componentName || instance.type.name || instance.type.__name;
// 生成实例ID,同组件多次使用useChildren会重复创建
// const instanceId = generateInstanceId(name || 'anonymous');
// 尝试从实例上复用已有的 child id,避免同个组件内多次调用 useChildren 导致重复注册
let instanceId: string | undefined = (instance.proxy as any)?.[CHILD_ID_SYMBOL];
if (!instanceId) {
instanceId = generateInstanceId(name || 'anonymous');
if (instance.proxy) (instance.proxy as any)[CHILD_ID_SYMBOL] = instanceId;
}
const parentRef = ref<any | null>(null);
const parentExposed = ref<Record<string, any>>({});
const createSimulatedParentContext = (parentInstance: any): ParentContext => ({
name: parentInstance?.type?.name || parentInstance?.type?.__name || 'unknown',
addChild: () => isDebugMode() && console.log('Simulated parent added child'),
removeChild: () => isDebugMode() && console.log('Simulated parent removed child'),
broadcast: () => isDebugMode() && console.log('Simulated parent broadcasting'),
broadcastToChildren: () => isDebugMode() && console.log('Simulated parent broadcasting to children'),
getChildren: () => [],
getExposed: () => parentInstance?.exposed || {},
getChildExposed: () => ({}),
getChildrenExposed: () => [],
getInstance: () => parentInstance
});
const getParentExposed = (): Record<string, any> => {
if (parentRef.value) {
const exposed = parentRef.value.getExposed();
parentExposed.value = exposed;
return exposed;
}
return {};
};
const getExposed = (): Record<string, any> => instance.exposed || {};
const findParent = (): ParentContext | null => {
if (parentName) {
const parentContext = getParentContext(parentName, instance);
if (parentContext) {
if (!parentContext.getInstance) {
parentContext.getInstance = () => findParentInstance(parentName, instance);
}
return parentContext;
}
const parentInstance = findParentInstance(parentName, instance);
if (parentInstance) {
return createSimulatedParentContext(parentInstance);
}
}
let current = instance.parent;
while (current) {
const context = (current.proxy as any)?.[PARENT_CONTEXT_SYMBOL];
if (context) {
if (!context.getInstance) {
context.getInstance = () => current;
}
return context;
}
current = current.parent;
}
return instance.parent ? createSimulatedParentContext(instance.parent) : null;
};
const linkParent = (): boolean => {
const parent = findParent();
if (parent) {
parentRef.value = parent;
if (parent.addChild && childContext) {
parent.addChild(childContext);
}
getParentExposed();
if (isDebugMode()) console.log(`Child ${name || 'anonymous'} linked to parent ${parent.name}`);
return true;
}
if (isDebugMode()) console.log(`Child ${name || 'anonymous'} no parent found, working in standalone mode`);
return false;
};
const emitToParent = (event: string, data?: any) => {
if (parentRef.value) {
const exposed = getParentExposed();
if (exposed && typeof exposed[event] === 'function') {
try {
exposed[event](data, instanceId, name);
} catch (error) {
if (isDebugMode('error')) console.error(`Error calling parent method ${event}:`, error);
}
}
}
};
const getChildIndex = () => {
if (!parentRef.value) return -1;
try {
const children = parentRef.value.getChildren();
return children.findIndex((child: ChildContext) => child.id === instanceId);
} catch (error) {
return -1;
}
};
const childContext: ChildContext = {
id: instanceId,
name: name || 'anonymous',
getChildIndex,
emitToParent,
getParentExposed,
getInstance: () => instance,
getExposed
};
if (isDebugMode()) console.log(`Child ${name || 'anonymous'} registered, looking for parent`);
onMounted(() => {
let connected = linkParent();
nextTick(() => {
connected = linkParent();
if (!connected) {
setTimeout(linkParent, 500);
}
});
});
onUnmounted(() => {
if (parentRef.value?.removeChild) {
parentRef.value.removeChild(instanceId);
}
if (isDebugMode()) console.log(`Child ${name || 'anonymous'} unmounted`);
});
return {
childId: instanceId,
childName: name || 'anonymous',
childIndex: computed(getChildIndex),
parent: parentRef,
emitToParent,
getParentExposed,
parentExposed: computed(() => parentExposed.value),
getExposed
};
}
/**
* 检查父组件是否存在
*/
export function hasParent(parentName?: string): boolean {
const instance = getCurrentInstance();
if (!instance) return false;
if (parentName) {
return getParentContext(parentName, instance) !== null;
}
let current = instance.parent;
while (current) {
if ((current.proxy as any)?.[PARENT_CONTEXT_SYMBOL]) return true;
current = current.parent;
}
return false;
}
/**
* 获取父组件上下文
*/
export function getParentContextByName(parentName: string): ParentContext | null {
const instance = getCurrentInstance();
return instance ? getParentContext(parentName, instance) : null;
}
/**
* 热更新清理函数
*/
export function cleanupComponentRelations(): void {
if (isDebugMode()) console.log('Cleaning up component relations for hot reload');
}
// 热更新处理
if (import.meta.hot) {
import.meta.hot.accept(() => {
if (isDebugMode()) console.log('Hot reload detected, relations will be automatically reconnected');
});
}
export default {
useParent,
useChildren,
hasParent,
getParentContextByName,
cleanupComponentRelations
};
@@ -0,0 +1,15 @@
export function useDebounce(delay: number = 500) {
let timeout: ReturnType<typeof setTimeout> | null = null;
// 防抖函数
function debounce(callback: () => void, debounceTime?: number) {
debounceTime = debounceTime || delay;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
callback();
}, debounceTime);
}
return {
debounce
};
}
@@ -0,0 +1,79 @@
import { type ComponentInternalInstance, getCurrentInstance } from 'vue';
/**
* 将事件名转换为驼峰格式
* @param str 需要转换的字符串
* @description 例如:on-form-change -> onFormChange
* @returns
*/
function formatToCamelCase(str: string): string {
return str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
}
export function useEmitter(name: string) {
const instance: ComponentInternalInstance | null | undefined = getCurrentInstance();
/** * 向上查找父组件并派发事件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function dispatch(componentName: string, eventName: string, ...params: any[]) {
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,派发事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
parent.emit && parent.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
parent.exposed?.[formatToCamelCase(eventName)] &&
parent.exposed[formatToCamelCase(eventName)](...params);
break;
}
parent = parent.parent;
}
}
/**
* 向下递归查找子组件并广播事件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function broadcast(componentName: string, eventName: string, ...params: any[]) {
if (!instance) return;
const subTree = (instance.subTree as any)?.children || [];
const children = Array.isArray(subTree) ? subTree : [subTree];
children.forEach((vnode: any) => {
const child = vnode.component as ComponentInternalInstance | undefined;
if (child) {
const name = (child.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,广播事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
child.emit && child.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
child.exposed?.[formatToCamelCase(eventName)] &&
child.exposed[formatToCamelCase(eventName)](...params);
} else {
broadcast.call(child, componentName, eventName, ...params);
}
}
});
}
return {
dispatch,
broadcast
};
}
@@ -0,0 +1,34 @@
import configProvider from '../util/config-provider';
/**
* 国际化 composable
* 提供:
* - currentLocale / locales 响应式引用
* - t(key, replacements) 翻译方法
* - setLocale(name) 切换语言
* - initLocales(locales?, defaultName?) 初始化语言包
*/
export function useLocale(namespace?: string) {
// 创建翻译函数,支持命名空间
const createTranslateFunction = (ns?: string) => {
return (key: string, replacements?: any, localeName?: string): string => {
// 如果有命名空间,自动添加前缀
const fullKey = ns ? `${ns}.${key}` : key;
return configProvider.t(fullKey, replacements, localeName);
};
};
return {
// 响应式引用
currentLocale: configProvider.currentLocaleRef,
locales: configProvider.localesRef,
// 方法
t: createTranslateFunction(namespace),
setLocale: (name: string) => configProvider.setLocale(name),
getLocales: () => configProvider.getLocales(),
getCurrentLocale: () => configProvider.getCurrentLocale(),
initLocales: (locales?: any[], defaultLocaleName?: string, isForce?: boolean) =>
configProvider.initLocales(locales, defaultLocaleName, isForce)
};
}
@@ -0,0 +1,131 @@
import {
U_MODAL_EVENT_SHOW,
U_MODAL_EVENT_HIDE,
U_MODAL_EVENT_CLEAR_LOADING,
U_MODAL_GLOBAL_EVENT_SHOW,
U_MODAL_GLOBAL_EVENT_HIDE,
U_MODAL_GLOBAL_EVENT_CLEAR_LOADING,
getEventWithCurrentPage,
type ModalPayload
} from '../../components/u-modal/service';
export type UseModalShowOptions = ModalPayload;
export type UseModal = {
/**
* 显示 modal
* - show('标题')
* - show({ title, content, showCancelButton, ... })
*/
show: (contentOrOptions: string | UseModalShowOptions) => void;
/**
* 显示 confirm 类型的 modal(带确认和取消按钮)
* - confirm('标题')
* - confirm({ title, content, onConfirm, onCancel })
*/
confirm: (contentOrOptions: string | UseModalShowOptions) => void;
/** 关闭 modal */
close: () => void;
/** 清除 loading 状态 */
clearLoading: () => void;
};
export type UseModalOptions = {
/** 是否使用全局根部 <u-modal global /> */
global?: boolean;
/** 是否使用页面级 <u-modal page /> 或某个页面的 <u-modal page="pageId" /> */
page?: boolean | string;
};
function normalize(contentOrOptions: string | UseModalShowOptions): UseModalShowOptions {
if (typeof contentOrOptions === 'string') {
// 如果是简单的字符串,可能是标题或内容
return { content: contentOrOptions };
}
return contentOrOptions || {};
}
function getPage(optionsOrGlobal: UseModalOptions | boolean): string {
if (typeof optionsOrGlobal === 'boolean') {
return '';
}
if (optionsOrGlobal.page && typeof optionsOrGlobal.page === 'string' && optionsOrGlobal.page !== '') {
return optionsOrGlobal.page;
}
return '';
}
/**
* Modal 函数式调用
* @description 需要页面/应用中至少存在一个 <u-modal global /> 或 <u-modal page /> 实例用于承接事件;不影响原 ref 调用方式。
*
* 支持两种调用方式:
* - 应用级 useModal() / useModal(true) / useModal({ global: true })
* - 页面级 useModal(false) / useModal({ page: true }) / useModal({ page: 'pageId' }) (pageId 应和页面中 <u-modal page="pageId" /> 的 page 属性一致)
*/
export function useModal(optionsOrGlobal: UseModalOptions | boolean = true): UseModal {
const isGlobal = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === true : !!optionsOrGlobal.global;
const isPage = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === false : !!optionsOrGlobal.page;
const showEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_SHOW
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_SHOW, getPage(optionsOrGlobal))
: '';
const hideEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_HIDE
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_HIDE, getPage(optionsOrGlobal))
: '';
const clearLoadingEvent = isGlobal
? U_MODAL_GLOBAL_EVENT_CLEAR_LOADING
: isPage
? getEventWithCurrentPage(U_MODAL_EVENT_CLEAR_LOADING, getPage(optionsOrGlobal))
: '';
function emitShow(payload: UseModalShowOptions) {
if (showEvent) {
uni?.$emit && uni.$emit(showEvent, payload);
}
}
function emitHide() {
if (hideEvent) {
uni?.$emit && uni.$emit(hideEvent);
}
}
function emitClearLoading() {
if (clearLoadingEvent) {
uni?.$emit && uni.$emit(clearLoadingEvent);
}
}
function show(contentOrOptions: string | UseModalShowOptions) {
emitShow(normalize(contentOrOptions));
}
function confirm(contentOrOptions: string | UseModalShowOptions) {
const options = normalize(contentOrOptions);
emitShow({
...options,
showCancelButton: options.showCancelButton ?? true,
showConfirmButton: options.showConfirmButton ?? true
});
}
function clearLoading() {
emitClearLoading();
}
function close() {
emitHide();
}
return {
show,
confirm,
close,
clearLoading
};
}
@@ -0,0 +1,40 @@
import { getCurrentInstance, nextTick, ref } from 'vue';
/**
* useRect - 获取元素的位置信息(响应式,原生实现)
* @param selector 选择器(如 #id 或 .class)
* @param all 是否获取所有匹配元素
* @returns rect 响应式的节点信息,refresh 主动刷新方法
*/
export function useRect(selector: string | null = null, all = false) {
const rect = ref<any>(all ? [] : null);
const instance = getCurrentInstance();
async function getRect(realSelector: string | null = null, delay = 0): Promise<any> {
realSelector = realSelector || selector;
if (!realSelector) return rect.value;
await nextTick();
return new Promise(resolve => {
setTimeout(() => {
uni.createSelectorQuery()
.in(instance?.proxy)
[all ? 'selectAll' : 'select'](realSelector as string)
.boundingClientRect((res: any) => {
rect.value = res;
resolve(res);
})
.exec();
}, delay);
});
}
function refresh(selector?: string | null, delay?: number): Promise<any> {
return getRect(selector, delay);
}
return {
rect,
getRect,
refresh
};
}
@@ -0,0 +1,173 @@
/**
* 路由跳转 hooks,基于 route.ts 的 Router 类实现
* 提供 Vue Composition API 风格的路由跳转方法
*/
import { ref } from 'vue';
interface RouterConfig {
type?: string;
url?: string;
delta?: number;
params?: Record<string, any>;
animationType?: string;
animationDuration?: number;
intercept?: boolean;
}
declare const uni: any;
const config: RouterConfig = {
type: 'navigateTo',
url: '',
delta: 1,
params: {},
animationType: 'pop-in',
animationDuration: 300,
intercept: false
};
// 响应式配置,支持动态修改
const configRef = ref({ ...config });
/**
* 判断 url 前面是否有 "/",如果没有则加上
*/
const addRootPath = (url: string): string => {
return url[0] === '/' ? url : `/${url}`;
};
/**
* 整合路由参数
*/
const mixinParam = (url: string, params: Record<string, any>): string => {
url = url && addRootPath(url);
let query = '';
if (/.*\/.*\?.*=.*/.test(url)) {
query = uni.$u.queryParams(params, false);
return url + '&' + query;
} else {
query = uni.$u.queryParams(params);
return url + query;
}
};
/**
* 执行路由跳转
*/
const openPage = (cfg: RouterConfig): void => {
const { url = '', type = '', delta = 1, animationDuration = 300 } = cfg;
if (type === 'navigateTo' || type === 'to') {
uni.navigateTo({ url, animationDuration });
}
if (type === 'redirectTo' || type === 'redirect') {
uni.redirectTo({ url });
}
if (type === 'switchTab' || type === 'tab') {
uni.switchTab({ url });
}
if (type === 'reLaunch' || type === 'launch') {
uni.reLaunch({ url });
}
if (type === 'navigateBack' || type === 'back') {
uni.navigateBack({ delta });
}
};
/**
* 路由跳转主方法
*/
const route = async (options: string | RouterConfig = {}, params: Record<string, any> = {}): Promise<void> => {
let mergeConfig: RouterConfig = {};
if (typeof options === 'string') {
mergeConfig.url = mixinParam(options, params);
mergeConfig.type = 'navigateTo';
} else {
mergeConfig = uni.$u.deepMerge(configRef.value, options);
mergeConfig.url = mixinParam(options.url || '', options.params || {});
}
if (params.intercept !== undefined) {
configRef.value.intercept = params.intercept;
}
mergeConfig.params = params;
mergeConfig = uni.$u.deepMerge(configRef.value, mergeConfig);
if (uni.$u.routeIntercept && typeof uni.$u.routeIntercept === 'function') {
const isNext = await new Promise<boolean>(resolve => {
uni.$u.routeIntercept(mergeConfig, resolve);
});
isNext && openPage(mergeConfig);
} else {
openPage(mergeConfig);
}
};
/**
* 跳转到指定页面
*/
const to = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'navigateTo' }, params || {});
};
/**
* 关闭当前页面,跳转到指定页面
*/
const redirect = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'redirectTo' }, params || {});
};
/**
* 跳转到 tabBar 页面
*/
const tab = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'switchTab' }, params || {});
};
/**
* 关闭所有页面,跳转到指定页面
*/
const reLaunch = (url: string, params?: Record<string, any>): Promise<void> => {
return route({ url, type: 'reLaunch' }, params || {});
};
/**
* 返回上一页
*/
const back = (delta: number = 1): Promise<void> => {
return route({ type: 'navigateBack', delta });
};
/**
* 设置默认路由配置
*/
const setConfig = (newConfig: Partial<RouterConfig>): void => {
configRef.value = uni.$u.deepMerge(configRef.value, newConfig);
};
/**
* 获取当前路由配置
*/
const getConfig = (): RouterConfig => {
return { ...configRef.value };
};
export function useRouter() {
return {
// 核心跳转方法
route,
// 便捷方法
to,
redirect,
tab,
reLaunch,
back,
// 配置方法
setConfig,
getConfig
};
}
export default useRouter;
@@ -0,0 +1,173 @@
/**
* 主题管理 composable
* 提供主题切换、持久化、CSS 变量注入、暗黑模式等功能
*
* 使用方式:
* const { currentTheme, themes, setTheme, getDarkMode, setDarkMode, isInDarkMode, getAvailableThemes, initTheme } = useTheme()
*/
import type { DarkMode, Theme } from '../../types/global';
import configProvider, { type DefaultThemeConfig } from '../util/config-provider';
import { defaultThemes } from '../config/theme-tokens';
const THEME_STORAGE_KEY = 'uview-pro-theme';
const DARK_MODE_STORAGE_KEY = 'uview-pro-dark-mode';
const themesRef = configProvider.themesRef;
const currentTheme = configProvider.currentThemeRef;
const darkModeRef = configProvider.darkModeRef;
/**
* 保存主题到 Storage
*/
function saveThemeToStorage(themeName: string) {
try {
uni.setStorageSync(THEME_STORAGE_KEY, themeName);
} catch (e) {
console.warn('[useTheme] failed to write storage', e);
}
}
/**
* 保存暗黑模式设置到 Storage
*/
function saveDarkModeToStorage(mode: DarkMode) {
try {
uni.setStorageSync(DARK_MODE_STORAGE_KEY, mode);
} catch (e) {
console.warn('[useTheme] failed to write storage', e);
}
}
/**
* 设置主题
*/
function setTheme(themeName: string) {
configProvider.setTheme(themeName);
currentTheme.value = configProvider.getCurrentTheme();
saveThemeToStorage(themeName);
}
/**
* 获取当前主题
*/
function getCurrentTheme(): Theme | null {
return currentTheme.value || configProvider.getCurrentTheme();
}
/**
* 获取所有可用主题
*/
function getAvailableThemes() {
return configProvider.getThemes();
}
/**
* 初始化主题系统
* @param themes 可选的主题列表,如果未提供则尝试从 uni.$u.themes 读取
* @param defaultConfig 可选的默认主题配置,支持字符串(默认主题名)或对象({ defaultTheme?, defaultDarkMode? })
*/
export function initTheme(themes?: Theme[], defaultConfig?: string | DefaultThemeConfig, isForce?: boolean) {
// 如果有传入主题列表,使用传入的
if (Array.isArray(themes) && themes.length > 0) {
configProvider.initTheme(themes, defaultConfig, isForce);
return;
}
// // 若已通过插件或其他方式完成初始化,则不再覆盖,最多按需切换默认主题
// const existingThemes = configProvider.getThemes();
// if (existingThemes.length > 0) {
// if (typeof defaultConfig === 'string') {
// configProvider.setTheme(defaultConfig);
// } else if (defaultConfig && typeof defaultConfig === 'object' && (defaultConfig as any).defaultTheme) {
// configProvider.setTheme(defaultConfig.defaultTheme);
// } else if (!configProvider.getCurrentTheme()) {
// configProvider.setTheme(existingThemes[0].name);
// } else {
// // 触发一次 apply,便于初始化 CSS 变量
// configProvider.setTheme(configProvider.getCurrentTheme()!.name);
// }
// return;
// }
// // 初始化 configProvider(如果运行时提供了内置主题)
// try {
// const builtin = (typeof uni !== 'undefined' && (uni as any).$u && (uni as any).$u.themes) || [];
// if (Array.isArray(builtin) && builtin.length > 0) {
// configProvider.initTheme(builtin as Theme[], defaultConfig);
// return;
// }
// } catch (e) {
// // ignore
// }
// 回退到内置默认主题
configProvider.initTheme(defaultThemes as Theme[], defaultConfig);
}
/**
* 初始化暗黑模式
* @param darkMode 暗黑模式设置
* @param isForce 是否强制初始化
*/
function initDarkMode(darkMode?: DarkMode, isForce?: boolean) {
configProvider.initDarkMode(darkMode, isForce);
}
/**
* 获取当前暗黑模式设置
*/
function getDarkMode(): DarkMode {
return configProvider.getDarkMode();
}
/**
* 设置暗黑模式
* @param mode 'auto' (跟随系统) | 'light' (强制亮色) | 'dark' (强制暗黑)
*/
function setDarkMode(mode: DarkMode) {
configProvider.setDarkMode(mode);
darkModeRef.value = mode;
saveDarkModeToStorage(mode);
}
/**
* 检查当前是否处于暗黑模式
*/
function isInDarkMode(): boolean {
return configProvider.isInDarkMode();
}
/**
* 切换暗黑模式(在当前模式的基础上切换)
*/
function toggleDarkMode() {
const current = getDarkMode();
const nextMode = current === 'dark' ? 'light' : 'dark';
setDarkMode(nextMode);
}
/**
* 使用主题的 composable
* 返回所有主题相关的响应式引用和方法
*/
export function useTheme() {
return {
// 响应式引用
currentTheme,
themes: themesRef,
darkMode: darkModeRef,
cssVars: configProvider.cssVarsRef,
// 主题相关方法
initTheme,
setTheme,
getCurrentTheme,
getAvailableThemes,
// 暗黑模式相关方法
initDarkMode,
getDarkMode,
setDarkMode,
isInDarkMode,
toggleDarkMode
};
}
@@ -0,0 +1,16 @@
export function useThrottle(delay: number = 500) {
let previous: number = 0;
// 节流函数
function throttle(callback: () => void, throttleTime?: number) {
throttleTime = throttleTime || delay;
let now = Date.now();
if (now - previous > throttleTime) {
callback();
previous = now;
}
}
return {
throttle
};
}
@@ -0,0 +1,115 @@
import {
U_TOAST_EVENT_HIDE,
U_TOAST_EVENT_SHOW,
U_TOAST_GLOBAL_EVENT_HIDE,
U_TOAST_GLOBAL_EVENT_SHOW,
getEventWithCurrentPage,
type ToastPayload
} from '../../components/u-toast/service';
import type { ThemeType } from '../../types/global';
export type UseToastShowOptions = ToastPayload;
export type UseToast = {
/**
* 显示 toast
* - show('文本')
* - show({ title, type, duration, ... })
*/
show: (titleOrOptions: string | UseToastShowOptions) => void;
/** 关闭 toast */
close: () => void;
/** 成功 */
success: (titleOrOptions: string | UseToastShowOptions) => void;
/** 错误 */
error: (titleOrOptions: string | UseToastShowOptions) => void;
/** 警告 */
warning: (titleOrOptions: string | UseToastShowOptions) => void;
/** 信息 */
info: (titleOrOptions: string | UseToastShowOptions) => void;
/** 加载中(默认常驻,需 close 关闭) */
loading: (titleOrOptions: string | UseToastShowOptions) => void;
};
export type UseToastOptions = {
/** 是否使用全局根部 <u-toast global />,默认 true;为 false 时走页面级 <u-toast /> */
global?: boolean;
/** 是否使用页面级 <u-toast page /> 或某个页面的 <u-toast page="pageId" /> */
page?: boolean | string;
};
function normalize(titleOrOptions: string | UseToastShowOptions): UseToastShowOptions {
if (typeof titleOrOptions === 'string') return { title: titleOrOptions };
return titleOrOptions || {};
}
function getPage(optionsOrGlobal: UseToastOptions | boolean): string {
if (typeof optionsOrGlobal === 'boolean') {
return '';
}
if (optionsOrGlobal.page && typeof optionsOrGlobal.page === 'string' && optionsOrGlobal.page !== '') {
return optionsOrGlobal.page;
}
return '';
}
/**
* Toast 函数式调用
* @description 需要页面/应用中至少存在一个 <u-toast global /> 或 <u-toast page /> 实例用于承接事件;不影响原 ref 调用方式。
*
* 支持两种调用方式:
* - 应用级 useToast() / useToast(true) / useToast({ global: true })
* - 页面级 useToast(false) / useToast({ page: true }) / useToast({ page: 'pageId' }) (pageId 应和页面中 <u-toast page="pageId" /> 的 page 属性一致)
*/
export function useToast(optionsOrGlobal: UseToastOptions | boolean = true): UseToast {
const isGlobal = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === true : !!optionsOrGlobal.global;
const isPage = typeof optionsOrGlobal === 'boolean' ? optionsOrGlobal === false : !!optionsOrGlobal.page;
const showEvent = isGlobal
? U_TOAST_GLOBAL_EVENT_SHOW
: isPage
? getEventWithCurrentPage(U_TOAST_EVENT_SHOW, getPage(optionsOrGlobal))
: '';
const hideEvent = isGlobal
? U_TOAST_GLOBAL_EVENT_HIDE
: isPage
? getEventWithCurrentPage(U_TOAST_EVENT_HIDE, getPage(optionsOrGlobal))
: '';
function emitShow(payload: UseToastShowOptions) {
if (showEvent) {
uni?.$emit && uni.$emit(showEvent, payload);
}
}
function emitHide() {
if (hideEvent) {
uni?.$emit && uni.$emit(hideEvent);
}
}
function show(titleOrOptions: string | UseToastShowOptions) {
emitShow(normalize(titleOrOptions));
}
function close() {
emitHide();
}
function withType(type: ThemeType, titleOrOptions: string | UseToastShowOptions) {
const options = normalize(titleOrOptions);
emitShow({ ...options, type });
}
return {
show,
close,
success: (v: any) => withType('success', v),
error: (v: any) => withType('error', v),
warning: (v: any) => withType('warning', v),
info: (v: any) => withType('info', v),
loading: (v: any) => {
const options = normalize(v);
// loading 通常需要常驻,除非用户显式传 duration
emitShow({ ...options, loading: true, duration: options.duration ?? 0 });
}
};
}
@@ -0,0 +1,349 @@
// post类型对象参数转为get类型url参数
import queryParams from './function/queryParams';
// 路由封装
import route from './function/route';
// 时间格式化
import timeFormat from './function/timeFormat';
// 时间戳格式化,返回多久之前
import timeFrom from './function/timeFrom';
// 颜色渐变相关,colorGradient-颜色渐变,hexToRgb-十六进制颜色转rgb颜色,rgbToHex-rgb转十六进制
import colorGradients from './function/colorGradient';
// 生成全局唯一guid字符串
import guid from './function/guid';
// 主题相关颜色,info|success|warning|primary|default|error,此颜色已在uview.scss中定义,但是为js中也能使用,故也定义一份
import { color } from './config/color';
import { getColor, setColor } from './function/color';
// 根据type获取图标名称
import type2icon from './function/type2icon';
// 打乱数组的顺序
import randomArray from './function/randomArray';
// 对象和数组的深度克隆
import deepClone from './function/deepClone';
// 对象深度拷贝
import deepMerge from './function/deepMerge';
// 添加单位
import addUnit from './function/addUnit';
// 规则检验
import test from './function/test';
// 随机数
import random from './function/random';
// 去除空格
import trim from './function/trim';
// toast提示,对uni.showToast的封装
import toast from './function/toast';
// 获取父组件参数
import getParent from './function/getParent';
// 获取整个父组件
import $parent from './function/$parent';
// 获取sys()和os()工具方法
// 获取设备信息,挂载到$u的sys()(system的缩写)属性中,
// 同时把安卓和ios平台的名称"ios"和"android"挂到$u.os()中,方便取用
import { sys, os } from './function/sys';
// 防抖方法
import debounce from './function/debounce';
// 节流方法
import throttle from './function/throttle';
// 获取元素的位置信息
import getRect from './function/getRect';
// 剪贴板
import { clipboard } from './function/clipboard';
// 配置信息
import config from './config/config';
// 各个需要fixed的地方的z-index配置文件
import zIndex from './config/zIndex';
import { mitt } from './util/mitt';
// http相关
import httpPlugin, {
Request,
http,
type RequestOptions,
type RequestConfig,
type RequestInterceptor,
type RequestMeta
} from './request/index';
/**
* @description 数字格式化
* @param number 要格式化的数字
* @param decimals 保留几位小数
* @param decimalPoint 小数点符号
* @param thousandsSeparator 千分位符号
* @returns 格式化后的数字
*/
export function formatPrice(
number: number | string,
decimals: number = 0,
decimalPoint: string = '.',
thousandsSeparator: string = ','
): string {
// 辅助函数:四舍五入到指定小数位
function round(num: number, precision: number): string {
const factor = Math.pow(10, precision);
return (Math.round(num * factor) / factor).toFixed(precision);
}
let numStr = String(number).replace(/[^0-9+\-Ee.]/g, '');
const n = !isFinite(+numStr) ? 0 : +numStr;
const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
const sep = thousandsSeparator ?? ',';
const dec = decimalPoint ?? '.';
let s: string[] = [];
s = (prec ? round(n, prec) : Math.round(n).toString()).split('.');
const re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, `$1${sep}$2`);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += '0'.repeat(prec - s[1].length);
}
return s.join(dec);
}
// 默认的姓名脱敏规则
export function formatName(name: string): string {
if (name.length === 2) {
return name.charAt(0) + '*';
} else if (name.length > 2) {
const masked = '*'.repeat(name.length - 2);
return name.charAt(0) + masked + name.charAt(name.length - 1);
} else {
return name;
}
}
/**
* @description 样式转换
* 对象转字符串,或者字符串转对象
* @param {object | string} customStyle 需要转换的目标
* @param {String} target 转换的目的,object-转为对象,string-转为字符串
* @returns {object|string}
*/
export function addStyle(
customStyle: Record<string, string> | string,
target: 'object' | 'string' = 'object'
): Record<string, string> | string {
// 字符串转字符串,对象转对象情形,直接返回
if (
test.empty(customStyle) ||
(typeof customStyle === 'object' && target === 'object') ||
(target === 'string' && typeof customStyle === 'string')
) {
return customStyle;
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格
const trimmedStyle = trim(customStyle as string);
const styleArray = trimmedStyle.split(';');
const style: Record<string, string> = {};
for (let i = 0; i < styleArray.length; i++) {
if (styleArray[i]) {
const item = styleArray[i].split(':');
if (item.length === 2) {
style[trim(item[0])] = trim(item[1]);
}
}
}
return style;
}
// 对象转字符串
let string = '';
for (const i in customStyle as Record<string, string>) {
if (Object.prototype.hasOwnProperty.call(customStyle, i)) {
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase();
string += `${key}:${(customStyle as Record<string, string>)[i]};`;
}
}
return trim(string);
}
/**
* 将外部传入的样式格式化为可读的 CSS 样式。
* @param {object | object[]} styles 外部传入的样式对象或数组
* @returns {string} 格式化后的 CSS 样式字符串
*/
export function toStyle(...styles: Array<Record<string, any> | string | null | undefined>): string {
// 支持多参数:每个参数可以是 object 或 string,后面的参数优先级更高,会覆盖同名样式
// 如果传入单个数组(兼容旧调用),解构展开
if (styles.length === 1 && Array.isArray(styles[0])) {
styles = (styles[0] as any[]).slice();
}
// 用于合并样式的 Map,key 使用 kebab-case
const map = new Map<string, any>();
const processString = (str: string) => {
if (!str) return;
// 移除可能的末尾分号,再按分号分割
const parts = str.split(';');
for (let part of parts) {
part = part.trim();
if (!part) continue;
const idx = part.indexOf(':');
if (idx === -1) continue;
const key = trim(part.slice(0, idx));
const val = trim(part.slice(idx + 1));
if (key === '' || val === '') continue;
const k = kebabCase(key);
map.set(k, val);
}
};
const processObject = (obj: Record<string, any>) => {
if (!obj) return;
Object.keys(obj).forEach(key => {
const val = obj[key];
if (val == null || val === '') return;
const k = kebabCase(key);
map.set(k, val);
});
};
for (const item of styles) {
if (item == null || item === '') continue;
if (test.string(item)) {
processString(item as string);
} else if (test.array(item)) {
// 若传入数组作为参数,递归处理数组元素
(item as any[]).forEach(el => {
if (test.string(el)) processString(el as string);
else if (test.object(el)) processObject(el as Record<string, any>);
});
} else if (test.object(item)) {
processObject(item as Record<string, any>);
}
}
if (map.size === 0) return '';
// 按插入顺序构造样式字符串,值转成字符串
const result = Array.from(map.entries())
.map(([k, v]) => `${k}:${String(v)}`)
.join(';');
return result ? (result.endsWith(';') ? result : result + ';') : '';
}
/**
* 将驼峰命名转换为短横线命名。
* @param {string} word 待转换的词条
* @returns {string} 转换后的结果
*/
export function kebabCase(word: string): string {
// 使用正则表达式匹配所有大写字母,并在前面加上短横线,然后转换为小写
const newWord: string = word
.replace(/[A-Z]/g, function (match) {
return '-' + match;
})
.toLowerCase();
return newWord;
}
/**
* @description 进行延时,以达到可以简写代码的目的 比如: await uni.$u.sleep(20)将会阻塞20ms
* @param {number} value 堵塞时间 单位ms 毫秒
* @returns {Promise} 返回promise
*/
export function sleep(value: number = 30): Promise<boolean> {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, value);
});
}
export {
queryParams,
route,
timeFormat,
timeFrom,
guid,
color,
getColor,
setColor,
sys,
os,
type2icon,
randomArray,
deepClone,
deepMerge,
addUnit,
test,
random,
trim,
toast,
debounce,
throttle,
getRect,
getParent,
$parent,
clipboard,
config,
zIndex,
mitt
};
export const $u = {
queryParams: queryParams,
route: route,
timeFormat: timeFormat,
date: timeFormat, // 另名date
timeFrom,
colorGradient: colorGradients.colorGradient,
colorToRgba: colorGradients.colorToRgba,
guid,
color,
getColor,
setColor,
sys,
os,
type2icon,
randomArray,
hexToRgb: colorGradients.hexToRgb,
rgbToHex: colorGradients.rgbToHex,
test,
random,
deepClone,
deepMerge,
getParent,
$parent,
clipboard,
addUnit,
trim,
type: ['primary', 'success', 'error', 'warning', 'info'],
http,
toast,
config, // uView配置信息相关,比如版本号
zIndex,
debounce,
throttle,
mitt: mitt(),
getRect,
formatPrice,
formatName,
addStyle,
toStyle,
kebabCase,
sleep
};
// 颜色相关方法单独导出
export const { colorGradient, colorToRgba, hexToRgb, rgbToHex } = colorGradients;
// http相关导出
export {
Request,
httpPlugin,
http,
type RequestOptions,
type RequestConfig,
type RequestInterceptor,
type RequestMeta
};
export * from './hooks';
export * from './util/config-provider';
@@ -0,0 +1,76 @@
import deepMerge from '../function/deepMerge';
export function isFunction(f: any): boolean {
return typeof f === 'function';
}
export function isPromise(p: any): boolean {
return !!(p && p.then && p.catch);
}
export function isArray(arr: any) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
/**
* 构建基础类
*/
class Builder<T> {
instance: any;
constructor(instance: any) {
this.instance = instance;
}
/**
*
* @param urlConfig url 配置表
* @param extra 其他请求方法对象
* @returns Object
*/
dispatch(urlConfig: Record<string, any>, extra: Record<string, any> = {}): Record<string, any> {
const builder: Record<string, any> = {};
// 创建 API
Object.keys(urlConfig).forEach(name => {
builder[name] = this.use.bind(this, urlConfig[name]);
});
return { ...builder, ...extra };
}
/**
* 发送请求
* @param {*} urlConfig : url 配置表
* @demo urlConfig = { login: { url: '/user/login', method: 'GET', loading: true } }
* @param {*} config : 开放配置,用户主动配置的
* @demo api.login({ params: { username: "admin" } })
* @returns Promise
*/
use(urlConfig: Record<string, any>, config: Record<string, any> = {}): Promise<T> {
// 请求地址
let url = config?.url ?? urlConfig.url;
// 兼容 restful url,如果是使用url为function,则为restful格式
if (config.url && isFunction(config.url)) {
url = `${urlConfig.url}${config.url()}`;
}
// 请求类型,get,post,put,delete
const method = config?.method ?? urlConfig?.method ?? 'GET';
// 如果有自定义的工厂函数基础类
const options = { ...deepMerge(urlConfig, config), url, method };
if (isFunction(this.instance) || isPromise(this.instance)) {
return this.instance(options);
}
// 如果是使用的 instance
// 默认的请求基础类
return this.instance.request(options);
}
}
/**
* Http 基础类
*/
class AutoHttp {
static get Builder() {
return Builder;
}
constructor() {}
}
export { AutoHttp };
@@ -0,0 +1,301 @@
import deepMerge from '../function/deepMerge';
/**
* 请求配置项Meta类型定义
*/
export interface RequestMeta {
toast?: boolean;
loading?: boolean;
originalData?: boolean;
[key: string]: any;
}
/**
* 请求配置项类型定义
*/
export interface RequestConfig {
baseUrl?: string;
header?: Record<string, any>;
method?: string;
dataType?: string;
responseType?: string;
timeout?: number;
meta?: RequestMeta;
[key: string]: any;
}
/**
* 忽略的请求参数类型定义
*/
const IGNORE_REQUEST_KEYS = ['baseUrl', 'meta'];
/**
* 请求拦截器类型定义
*/
export interface RequestInterceptor {
request?: ((options: RequestOptions) => RequestOptions | false) | null;
response?: ((response: any) => any | false) | null;
}
/**
* 请求参数类型定义
*/
export interface RequestOptions {
url: string;
header: Record<string, any>;
method: 'GET' | 'POST' | 'OPTIONS' | 'HEAD' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
data?: any;
dataType?: string;
responseType?: string;
params?: Record<string, any>;
complete?: (response: any) => void;
meta?: RequestMeta;
[key: string]: any;
}
export class Request {
public config: RequestConfig;
public interceptor: RequestInterceptor;
public options?: RequestOptions;
constructor() {
this.config = {
baseUrl: '', // 请求的根域名
header: {}, // 默认的请求头
method: 'POST', // 请求方式
dataType: 'json', // 设置为json,返回后uni.request会对数据进行一次JSON.parse
responseType: 'text', // 此参数无需处理,因为5+和支付宝小程序不支持,默认为text即可
timeout: 60000,
meta: {
originalData: true, // 是否在拦截器中返回服务端的原始数据,见文档说明
toast: false, // 是否在请求出错时,弹出toast
loading: false // 是否显示加载中
}
};
this.interceptor = {
request: null,
response: null
};
}
/**
* 将全局配置合并到本次请求的 options 中
* - 忽略 IGNORE_REQUEST_KEYS 中的字段(如 meta)
* - 对 header 使用深合并(全局 header 为默认,options.header 优先)
* - 对对象类型的字段尝试深合并,基础类型以 options 值优先
* - 处理 baseUrl:若存在全局 baseUrl 且 options.url 非完整 url(非 http 开头),则合并成完整 URL
*/
private mergeGlobalConfigToOptions(options: RequestOptions): RequestOptions {
const mergedOptions: RequestOptions = { ...options };
for (const key of Object.keys(this.config)) {
if (IGNORE_REQUEST_KEYS.includes(key)) {
continue;
}
const cfgVal = this.config[key];
const optVal = options[key];
// 跳过未设置的全局配置
if (cfgVal === undefined) continue;
// header 需要做深合并,且以 options.header 为准覆盖同名属性
if (key === 'header') {
mergedOptions.header = deepMerge(cfgVal || {}, optVal || {});
continue;
}
// 针对 method 等枚举字符串,优先使用 options 中的值,否则使用全局配置
if (typeof cfgVal === 'string' || typeof cfgVal === 'number' || typeof cfgVal === 'boolean') {
mergedOptions[key] = optVal !== undefined ? optVal : cfgVal;
continue;
}
// 对对象类型的配置(如自定义扩展)尝试做深合并
if (typeof cfgVal === 'object' && !Array.isArray(cfgVal)) {
mergedOptions[key] = deepMerge(cfgVal || {}, optVal || {});
continue;
}
// 其他类型,若 options 未传入则使用全局配置
if (optVal === undefined) {
mergedOptions[key] = cfgVal;
}
}
// 如果存在 baseUrl,并且 options.url 为相对地址,则拼接成完整 url
const baseUrl = this.config.baseUrl;
if (
baseUrl &&
mergedOptions.url &&
typeof mergedOptions.url === 'string' &&
mergedOptions.url.indexOf('http') !== 0
) {
mergedOptions.url =
baseUrl + (mergedOptions.url.indexOf('/') === 0 ? mergedOptions.url : `/${mergedOptions.url}`);
}
// 确保 url 存在,且为 string
if (!mergedOptions.url) {
mergedOptions.url = '';
}
return mergedOptions;
}
/**
* 设置全局默认配置
* @param customConfig 自定义配置
*/
setConfig(customConfig: Partial<RequestConfig>): void {
this.config = deepMerge(this.config, customConfig);
}
/**
* 主要请求部分
* @param options 请求参数
*/
request<T = unknown>(options: RequestOptions): Promise<T> {
// 合并 meta 配置,优先级:单次请求 > 全局
const mergedMeta: RequestMeta = {
...this.config.meta,
...(options.meta || {})
};
// 让 options.meta 传递到拦截器
options.meta = mergedMeta;
options.url = options.url || '';
options.params = options.params || {};
// 将全局配置合并到本次请求 options 中(注意忽略一些特殊字段如 baseUrl/meta)
options = this.mergeGlobalConfigToOptions(options);
if (this.interceptor.request && typeof this.interceptor.request === 'function') {
const interceptorRequest = this.interceptor.request(options);
if (!interceptorRequest) {
// 返回一个处于pending状态中的Promise,来取消原promise,避免进入then()回调
return new Promise(() => {});
}
this.options = interceptorRequest;
}
return new Promise<T>((resolve, reject) => {
options.complete = (response: any) => {
// 读取 meta 配置
const meta = options.meta || this.config.meta || {};
const originalData = meta.originalData ?? false;
// 拦截器处理,加入request的配置参数
response.config = options;
if (originalData) {
// 判断是否存在拦截器
if (this.interceptor.response && typeof this.interceptor.response === 'function') {
const resInterceptors = this.interceptor.response(response);
// 如果拦截器不返回false,就将拦截器返回的内容给请求的then回调
if (resInterceptors !== false) {
resolve(resInterceptors);
} else {
// 如果拦截器返回false,意味着拦截器定义者认为返回有问题,直接接入catch回调
reject(response);
}
} else {
// 如果要求返回原始数据,就算没有拦截器,也返回最原始的数据
resolve(response);
}
} else {
if (response.statusCode === 200) {
if (this.interceptor.response && typeof this.interceptor.response === 'function') {
const resInterceptors = this.interceptor.response(response.data);
if (resInterceptors !== false) {
resolve(resInterceptors);
} else {
reject(response.data);
}
} else {
// 如果不是返回原始数据(originalData=false),且没有拦截器的情况下,返回纯数据给then回调
resolve(response.data);
}
} else {
reject(response);
}
}
};
uni.request(options);
});
}
get<T = unknown>(
url: string,
data: any = {},
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
): Promise<T> {
return this.request<T>({
method: 'GET',
url,
data,
header: options.header || {},
meta: options.meta
});
}
post<T = unknown>(
url: string,
data: any = {},
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
): Promise<T> {
return this.request<T>({
url,
method: 'POST',
data,
header: options.header || {},
meta: options.meta
});
}
put<T = unknown>(
url: string,
data: any = {},
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
): Promise<T> {
return this.request<T>({
url,
method: 'PUT',
data,
header: options.header || {},
meta: options.meta
});
}
delete<T = unknown>(
url: string,
data: any = {},
options: { header?: Record<string, any>; meta?: RequestMeta } = {}
): Promise<T> {
return this.request<T>({
url,
method: 'DELETE',
data,
header: options.header || {},
meta: options.meta
});
}
}
// 插件化导出,支持 app.use(http, { interceptor })
const httpInstance = new Request();
interface HttpPluginOptions {
requestConfig?: Partial<RequestConfig>;
interceptor?: RequestInterceptor;
}
// 全局导出,支持 import { httpPlugin } from 'uview-pro'
const httpPlugin = {
install(app: any, options: HttpPluginOptions = {}) {
if (options.interceptor) {
const { request, response } = options.interceptor;
if (request) httpInstance.interceptor.request = request;
if (response) httpInstance.interceptor.response = response;
}
if (options.requestConfig) {
httpInstance.setConfig(options.requestConfig);
}
app.config.globalProperties.$http = httpInstance;
}
};
// 全局导出,支持 import { http } from 'uview-pro'
export { httpInstance as http };
// 插件化导出,支持 app.use(http, { interceptor })
export default httpPlugin;
@@ -0,0 +1,88 @@
/**
* 支持通过 name 路径批量/单项赋值 Pinia store 的工具方法
* @param store Pinia store 实例
* @param params { name: string, value: any }
* name 支持 a.b.c 形式嵌套赋值
*/
/**
* 用法示例:
* setStoreValue(userStore, { name: 'profile.avatar', value: 'xxx.png' })
* setStoreValueTyped(userStore, 'token', 'xxxx')
* setStoreValues(userStore, [{ name: 'token', value: 'xxx' }, { name: 'profile.avatar', value: 'img.png' }])
* getStoreValue(userStore, 'profile.avatar')
* resetStore(userStore, { token: '', profile: { avatar: '' } })
*/
export function setStoreValue<T extends object>(store: T, params: { name: string; value: any }): void {
const nameArr = params.name.split('.');
let obj: any = store;
if (nameArr.length >= 2) {
for (let i = 0; i < nameArr.length - 1; i++) {
if (!(nameArr[i] in obj)) {
obj[nameArr[i]] = {};
}
obj = obj[nameArr[i]];
}
obj[nameArr[nameArr.length - 1]] = params.value;
} else {
(store as any)[params.name] = params.value;
}
}
/**
* 类型安全的嵌套属性赋值工具
* @param store Pinia store 实例
* @param path 属性路径(如 profile.avatar)
* @param value 赋值内容
*/
export function setStoreValueTyped<T, K extends keyof T>(store: T, path: K, value: T[K]): void;
export function setStoreValueTyped<T>(store: T, path: string, value: any): void {
const nameArr = path.split('.');
let obj: any = store;
if (nameArr.length >= 2) {
for (let i = 0; i < nameArr.length - 1; i++) {
if (!(nameArr[i] in obj)) {
obj[nameArr[i]] = {};
}
obj = obj[nameArr[i]];
}
obj[nameArr[nameArr.length - 1]] = value;
} else {
(store as any)[path] = value;
}
}
/**
* 批量赋值 Pinia store 工具方法
* @param store Pinia store 实例
* @param values { name: string, value: any }[]
*/
export function setStoreValues<T extends object>(store: T, values: Array<{ name: string; value: any }>): void {
values.forEach(item => setStoreValue(store, item));
}
/**
* 获取嵌套属性值
* @param store Pinia store 实例
* @param path 属性路径(如 profile.avatar)
* @returns 属性值
*/
export function getStoreValue<T>(store: T, path: string): any {
const nameArr = path.split('.');
let obj: any = store;
for (let i = 0; i < nameArr.length; i++) {
if (obj == null) return undefined;
obj = obj[nameArr[i]];
}
return obj;
}
/**
* 重置 Pinia store 为初始值
* @param store Pinia store 实例
* @param initial 初始值对象
*/
export function resetStore<T extends object>(store: T, initial: Partial<T>): void {
Object.keys(initial).forEach(key => {
(store as any)[key] = (initial as any)[key];
});
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
/**
* async-validator 类型声明文件
* 适用于 uView Pro 内置 async-validator.js
* 支持 Schema、规则、校验回调、辅助类型等
*/
declare type ValidateError = {
message: string;
field?: string;
[key: string]: any;
};
declare type ValidateCallback = (
errors?: ValidateError[] | null,
fields?: Record<string, ValidateError[]> | null
) => void;
declare interface ValidateRule {
type?: string;
required?: boolean;
message?: string;
validator?: (
rule: ValidateRule,
value: any,
callback: ValidateCallback,
source: Record<string, any>,
options: ValidateOptions
) => void | boolean | string | Error | string[] | Promise<any>;
asyncValidator?: (
rule: ValidateRule,
value: any,
callback: ValidateCallback,
source: Record<string, any>,
options: ValidateOptions
) => Promise<any>;
enum?: any[];
len?: number;
min?: number;
max?: number;
pattern?: RegExp | string;
whitespace?: boolean;
fields?: Record<string, ValidateRule | ValidateRule[]>;
defaultField?: ValidateRule | ValidateRule[];
transform?: (value: any) => any;
[key: string]: any;
}
declare interface ValidateOptions {
messages?: Record<string, any>;
suppressWarning?: boolean;
first?: boolean;
firstFields?: boolean | string[];
keys?: string[];
error?: (rule: ValidateRule, msg: string) => ValidateError;
[key: string]: any;
}
declare class Schema {
constructor(descriptor: Record<string, ValidateRule | ValidateRule[]>);
messages(messages?: Record<string, any>): Record<string, any>;
define(rules: Record<string, ValidateRule | ValidateRule[]>): void;
validate(
source: Record<string, any>,
options?: ValidateOptions | ValidateCallback,
callback?: ValidateCallback
): Promise<void>;
getType(rule: ValidateRule): string;
getValidationMethod(rule: ValidateRule): Function | false;
static register(type: string, validator: Function): void;
static warning: (...args: any[]) => void;
static messages: Record<string, any>;
}
export = Schema;
export as namespace Schema;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
// Type definitions for calendar.js (农历/公历互转)
// Project: https://github.com/jjonline/calendar.js
export interface Solar2LunarResult {
lYear: number; // 农历年
lMonth: number; // 农历月
lDay: number; // 农历日
Animal: string; // 生肖
IMonthCn: string; // 农历月中文
IDayCn: string; // 农历日中文
cYear: number; // 公历年
cMonth: number; // 公历月
cDay: number; // 公历日
gzYear: string; // 干支年
gzMonth: string; // 干支月
gzDay: string; // 干支日
isToday: boolean; // 是否今天
isLeap: boolean; // 是否闰月
nWeek: number; // 星期几(1-7,周一为1)
ncWeek: string; // 星期几中文
isTerm: boolean; // 是否节气
Term: string | null; // 节气名
astro: string; // 星座
}
export interface Lunar2SolarResult extends Solar2LunarResult {}
export interface Calendar {
lunarInfo: number[];
solarMonth: number[];
Gan: string[];
Zhi: string[];
Animals: string[];
solarTerm: string[];
sTermInfo: string[];
nStr1: string[];
nStr2: string[];
nStr3: string[];
lYearDays(y: number): number;
leapMonth(y: number): number;
leapDays(y: number): number;
monthDays(y: number, m: number): number;
solarDays(y: number, m: number): number;
toGanZhiYear(lYear: number): string;
toAstro(cMonth: number, cDay: number): string;
toGanZhi(offset: number): string;
getTerm(y: number, n: number): number;
toChinaMonth(m: number): string;
toChinaDay(d: number): string;
getAnimal(y: number): string;
solar2lunar(y: number, m: number, d: number): Solar2LunarResult;
lunar2solar(y: number, m: number, d: number, isLeapMonth?: boolean): Lunar2SolarResult;
}
declare const calendar: Calendar;
export default calendar;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,182 @@
/**
* 适配 canvas 2d 上下文
* @param ctx canvas 2d 上下文
* @returns
*/
export function canvas2d(ctx: CanvasRenderingContext2D): UniApp.CanvasContext {
// 绑定 this 到 ctx,避免头条小程序等环境中出现 Illegal invocation 错误
const boundCtx = {
arc: ctx.arc.bind(ctx),
arcTo: ctx.arcTo.bind(ctx),
beginPath: ctx.beginPath.bind(ctx),
bezierCurveTo: ctx.bezierCurveTo.bind(ctx),
clearRect: ctx.clearRect.bind(ctx),
clip: ctx.clip.bind(ctx),
closePath: ctx.closePath.bind(ctx),
createImageData: ctx.createImageData.bind(ctx),
createLinearGradient: ctx.createLinearGradient.bind(ctx),
createPattern: ctx.createPattern.bind(ctx),
createRadialGradient: ctx.createRadialGradient.bind(ctx),
drawImage: ctx.drawImage.bind(ctx),
fill: ctx.fill.bind(ctx),
fillRect: ctx.fillRect.bind(ctx),
fillText: ctx.fillText.bind(ctx),
getImageData: ctx.getImageData.bind(ctx),
getLineDash: ctx.getLineDash.bind(ctx),
isPointInPath: ctx.isPointInPath.bind(ctx),
isPointInStroke: ctx.isPointInStroke.bind(ctx),
lineTo: ctx.lineTo.bind(ctx),
measureText: ctx.measureText.bind(ctx),
moveTo: ctx.moveTo.bind(ctx),
putImageData: ctx.putImageData.bind(ctx),
quadraticCurveTo: ctx.quadraticCurveTo.bind(ctx),
rect: ctx.rect.bind(ctx),
restore: ctx.restore.bind(ctx),
rotate: ctx.rotate.bind(ctx),
save: ctx.save.bind(ctx),
scale: ctx.scale.bind(ctx),
setLineDash: ctx.setLineDash.bind(ctx),
setTransform: ctx.setTransform.bind(ctx),
stroke: ctx.stroke.bind(ctx),
strokeRect: ctx.strokeRect.bind(ctx),
strokeText: ctx.strokeText.bind(ctx),
transform: ctx.transform.bind(ctx),
translate: ctx.translate.bind(ctx),
resetTransform: ctx.resetTransform?.bind(ctx),
roundRect: (ctx as any).roundRect?.bind(ctx),
setFillStyle(color: string | CanvasGradient) {
ctx.fillStyle = color;
},
setStrokeStyle(color: string | CanvasGradient | CanvasPattern) {
ctx.strokeStyle = color;
},
setLineWidth(lineWidth: number) {
ctx.lineWidth = lineWidth;
},
setLineCap(lineCap: 'butt' | 'round' | 'square') {
ctx.lineCap = lineCap;
},
setFontSize(font: string) {
ctx.font = font;
},
setGlobalAlpha(alpha: number) {
ctx.globalAlpha = alpha;
},
setLineJoin(lineJoin: 'bevel' | 'round' | 'miter') {
ctx.lineJoin = lineJoin;
},
setTextAlign(align: 'left' | 'center' | 'right') {
ctx.textAlign = align;
},
setMiterLimit(miterLimit: number) {
ctx.miterLimit = miterLimit;
},
setShadow(offsetX: number, offsetY: number, blur: number, color: string) {
ctx.shadowOffsetX = offsetX;
ctx.shadowOffsetY = offsetY;
ctx.shadowBlur = blur;
ctx.shadowColor = color;
},
setTextBaseline(textBaseline: 'top' | 'bottom' | 'middle') {
ctx.textBaseline = textBaseline;
},
createCircularGradient() {},
draw() {},
addColorStop() {}
};
// 复制属性访问器
Object.defineProperties(boundCtx, {
fillStyle: {
get: () => ctx.fillStyle,
set: value => {
ctx.fillStyle = value;
}
},
strokeStyle: {
get: () => ctx.strokeStyle,
set: value => {
ctx.strokeStyle = value;
}
},
lineWidth: {
get: () => ctx.lineWidth,
set: value => {
ctx.lineWidth = value;
}
},
lineCap: {
get: () => ctx.lineCap,
set: value => {
ctx.lineCap = value;
}
},
lineJoin: {
get: () => ctx.lineJoin,
set: value => {
ctx.lineJoin = value;
}
},
miterLimit: {
get: () => ctx.miterLimit,
set: value => {
ctx.miterLimit = value;
}
},
globalAlpha: {
get: () => ctx.globalAlpha,
set: value => {
ctx.globalAlpha = value;
}
},
globalCompositeOperation: {
get: () => ctx.globalCompositeOperation,
set: value => {
ctx.globalCompositeOperation = value;
}
},
font: {
get: () => ctx.font,
set: value => {
ctx.font = value;
}
},
textAlign: {
get: () => ctx.textAlign,
set: value => {
ctx.textAlign = value;
}
},
textBaseline: {
get: () => ctx.textBaseline,
set: value => {
ctx.textBaseline = value;
}
},
shadowOffsetX: {
get: () => ctx.shadowOffsetX,
set: value => {
ctx.shadowOffsetX = value;
}
},
shadowOffsetY: {
get: () => ctx.shadowOffsetY,
set: value => {
ctx.shadowOffsetY = value;
}
},
shadowBlur: {
get: () => ctx.shadowBlur,
set: value => {
ctx.shadowBlur = value;
}
},
shadowColor: {
get: () => ctx.shadowColor,
set: value => {
ctx.shadowColor = value;
}
}
});
return boundCtx as unknown as UniApp.CanvasContext;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,804 @@
// libs/util/config-provider.ts
// 全局配置类:管理 Theme 的初始化、切换、持久化
import { ref } from 'vue';
import type { DarkMode, Theme, ThemeColor } from '../../types/global';
import config, { isDebugMode } from '../config/config';
import { defaultThemes } from '../config/theme-tokens';
import { color as reactiveColor } from '../config/color';
import { getSystemDarkMode as getNativeSystemDarkMode } from './system-theme';
import * as localePack from '../../locale';
declare const uni: any;
const THEME_STORAGE_KEY = 'uview-pro-theme';
const DARK_MODE_STORAGE_KEY = 'uview-pro-dark-mode';
const LOCALE_STORAGE_KEY = 'uview-pro-locale';
const DEFAULT_LIGHT_TOKENS = (defaultThemes[0]?.color || {}) as Partial<ThemeColor>;
const DEFAULT_DARK_TOKENS = (defaultThemes[0]?.darkColor || {}) as Partial<ThemeColor>;
const STRUCTURAL_TOKENS = new Set([
'bgColor',
'bgWhite',
'bgGrayLight',
'bgGrayDark',
'bgBlack',
'borderColor',
'lightColor',
'mainColor',
'contentColor',
'tipsColor',
'whiteColor',
'blackColor',
'dividerColor',
'maskColor',
'shadowColor'
]);
export type DefaultThemeConfig = {
/**
* 默认主题
*/
defaultTheme?: string;
/**
* 默认暗黑模式
*/
defaultDarkMode?: DarkMode;
};
/**
* ConfigProvider: 管理全局主题
* - init(themes, defaultName): 初始化主题系统
* - setTheme(name): 切换主题并持久化
* - getThemes/getCurrentTheme: 读取当前数据
* - setDarkMode/getDarkMode: 管理暗黑模式
*/
export class ConfigProvider {
// 响应式状态,供外部直接引用
public themesRef = ref<Theme[]>([]);
public currentThemeRef = ref<Theme | null>(null);
public darkModeRef = ref<DarkMode>(config.defaultDarkMode);
public cssVarsRef = ref<Record<string, string>>({});
// 国际化 i18n 状态
public localesRef = ref<any[]>([]);
public currentLocaleRef = ref<any | null>(null);
private baseColorTokens: Partial<ThemeColor> = DEFAULT_LIGHT_TOKENS;
private baseDarkColorTokens: Partial<ThemeColor> = DEFAULT_DARK_TOKENS;
private systemDarkModeMediaQuery: MediaQueryList | null = null;
private lastAppliedCssKeys: string[] = [];
private interval: ReturnType<typeof setInterval> | number = 0;
constructor() {
// 默认不自动初始化,调用 init 以传入主题列表
this.initSystemDarkModeListener();
}
/**
* 初始化系统暗黑模式监听器
* 支持 H5、App、小程序等平台
*/
private initSystemDarkModeListener() {
// H5 平台:使用 matchMedia API
try {
if (typeof window !== 'undefined' && window.matchMedia) {
this.systemDarkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const listener = () => {
if (this.darkModeRef.value === 'auto') {
this.applyTheme(this.currentThemeRef.value);
}
};
if (this.systemDarkModeMediaQuery.addEventListener) {
this.systemDarkModeMediaQuery.addEventListener('change', listener);
} else if (this.systemDarkModeMediaQuery.addListener) {
this.systemDarkModeMediaQuery.addListener(listener);
}
}
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] H5 system dark mode listener failed', e);
}
// uni-app 平台:使用 uni.onThemeChange API
try {
if (typeof uni !== 'undefined' && typeof uni.onThemeChange === 'function') {
uni.onThemeChange((res: { theme: string }) => {
console.log('[ConfigProvider] system theme changed', res);
if (this.darkModeRef.value === 'auto') {
// 系统主题变化时,重新应用主题
this.applyTheme(this.currentThemeRef.value);
}
});
}
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] uni-app system dark mode listener failed', e);
}
this.initAppEvent();
}
/**
* App 平台事件监听
* 经测试 uni.onThemeChange 在 App 平台目前没生效,暂时只能通过定时检查
*/
private initAppEvent(): void {
// #ifdef APP
try {
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => {
if (this.darkModeRef.value === 'auto') {
// 系统主题变化时,重新应用主题
this.applyTheme(this.currentThemeRef.value);
}
}, 5000);
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] setInterval failed', e);
}
// #endif
}
/**
* 检测当前是否应该使用暗黑模式
*/
private isSystemDarkMode(): boolean {
try {
if (this.systemDarkModeMediaQuery) {
return this.systemDarkModeMediaQuery.matches;
}
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] matchMedia check failed', e);
}
try {
return getNativeSystemDarkMode() === 'dark';
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] native system theme check failed', e);
return false;
}
}
/**
* 初始化主题系统
* @param themes 可用主题数组
* @param defaultTheme 可选默认主题名
*/
initTheme(themes?: Theme[], defaultConfig?: string | DefaultThemeConfig, isForce?: boolean) {
const normalizedThemes = this.normalizeThemes(themes);
if (!normalizedThemes.length) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] init called with empty themes');
return;
}
// 配置默认主题
if (defaultConfig) {
if (typeof defaultConfig === 'string') {
config.defaultTheme = defaultConfig || config.defaultTheme;
} else if (typeof defaultConfig === 'object') {
const { defaultTheme, defaultDarkMode } = defaultConfig;
config.defaultTheme = defaultTheme || config.defaultTheme;
config.defaultDarkMode = defaultDarkMode || config.defaultDarkMode;
}
}
// 设置主题列表,响应式
this.themesRef.value = normalizedThemes.slice();
// 先尝试从 Storage 读取已保存主题名
const saved = this.readStorage<string>(THEME_STORAGE_KEY);
let initialName = saved || config.defaultTheme || this.themesRef.value[0].name;
if (isForce && config.defaultTheme) initialName = config.defaultTheme;
let found = this.themesRef.value.find(t => t.name === initialName);
if (!found) found = this.themesRef.value.find(t => t.name === config.defaultTheme);
if (!found) found = this.themesRef.value[0];
// 设置当前主题,响应式
this.currentThemeRef.value = found;
// 初始化暗黑模式设置
this.initDarkMode(config.defaultDarkMode, isForce);
// 应用主题
this.applyTheme(found);
if (isDebugMode())
console.log('[ConfigProvider] initialized, theme=', found.name, 'darkMode=', this.darkModeRef.value);
return this;
}
/**
* 初始化暗黑模式设置
* @param darkMode
*/
initDarkMode(darkMode?: DarkMode, isForce?: boolean) {
// 尝试从 Storage 读取暗黑模式设置
const savedDarkMode = this.readStorage<DarkMode>(DARK_MODE_STORAGE_KEY);
let darkModeValue = savedDarkMode || darkMode || config.defaultDarkMode;
if (isForce && darkMode) darkModeValue = darkMode;
this.darkModeRef.value = darkModeValue;
}
/**
* 初始化国际化数据
* @param locales 可选的 locale 列表(对象数组,包含 name 字段)
* @param defaultLocaleName 可选默认 locale 名称
*/
initLocales(locales?: any[], defaultLocaleName?: string, isForce?: boolean) {
const normalized = this.normalizeLocales(locales);
if (!normalized.length) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] initLocales called with empty locales');
return;
}
this.localesRef.value = normalized.slice();
// 尝试从 Storage 读取已保存 locale 名称
const saved = this.readStorage<string>(LOCALE_STORAGE_KEY);
// 根据传入的 defaultLocaleName 或 saved 或 config.defaultLocale 查找 locale
let initialName = saved || defaultLocaleName || config.defaultLocale;
if (isForce && defaultLocaleName) initialName = defaultLocaleName;
let found = this.localesRef.value.find((l: any) => l.name === initialName);
if (!found) found = this.localesRef.value.find(l => l.name === config.defaultLocale);
if (!found) found = this.localesRef.value[0];
this.currentLocaleRef.value = found;
if (isDebugMode()) console.log('[ConfigProvider] locales initialized, locale=', found?.name);
return this;
}
/**
* 归一化 locale 配置,保证始终至少有一个默认 locale
*/
private normalizeLocales(locales?: any[]) {
// 获取内置语言包(可能包含 zh-CN / en-US 等)
let builtinList: any[] = [];
try {
builtinList = Object.values(localePack || {}).filter(v => v && typeof v === 'object');
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] normalizeLocales read builtin failed', e);
}
// 如果没有传入自定义 locales,直接返回内置列表
if (!Array.isArray(locales) || !locales.length) {
return builtinList.slice();
}
// 将 builtin 按 name 映射,便于合并
const map = new Map<string, any>();
builtinList.forEach(item => {
if (item && item.name) {
map.set(item.name, { ...(item || {}) });
}
});
// 合并用户传入的 locales:若 name 相同则对对象字段进行浅合并(嵌套对象尝试合并)
locales.forEach(loc => {
if (!loc || !loc.name) return;
const existing = map.get(loc.name);
if (!existing) {
// 新增语言直接加入
map.set(loc.name, { ...(loc || {}) });
return;
}
// 合并:对每个 key,如果是对象且现有为对象,则进行浅合并,否则覆盖
const merged: any = { ...existing };
Object.keys(loc).forEach(k => {
const v = (loc as any)[k];
if (v != null && typeof v === 'object' && !Array.isArray(v) && typeof merged[k] === 'object') {
merged[k] = { ...(merged[k] || {}), ...(v || {}) };
} else {
merged[k] = v;
}
});
map.set(loc.name, merged);
});
return Array.from(map.values());
}
/**
* 获取所有可用 locale
*/
getLocales() {
return this.localesRef.value.slice();
}
/**
* 获取当前 locale 对象
*/
getCurrentLocale() {
return this.currentLocaleRef.value;
}
/**
* 切换 locale 并持久化
*/
setLocale(localeName: string) {
if (!this.localesRef.value || this.localesRef.value.length === 0) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] setLocale called but locales list empty');
return;
}
const locale = this.localesRef.value.find(l => l.name === localeName);
if (!locale) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] locale not found:', localeName);
return;
}
this.currentLocaleRef.value = locale;
this.writeStorage(LOCALE_STORAGE_KEY, localeName);
if (isDebugMode()) console.log('[ConfigProvider] setLocale ->', localeName);
}
/**
* 翻译函数
* 支持 key 路径,例如 'calendar.placeholder'
* replacements 支持数组或对象替换占位符 {0} 或 {name}
*/
t(key: string, replacements?: any, localeName?: string): string {
// 如果 locales 尚未初始化,尝试懒初始化,使用内置语言包作为回退
try {
if (!Array.isArray(this.localesRef.value) || this.localesRef.value.length === 0) {
this.initLocales();
}
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] lazy initLocales failed', e);
}
const localeObj =
(localeName && this.localesRef.value.find(l => l.name === localeName)) || this.currentLocaleRef.value;
if (!localeObj) return key;
const parts = key.split('.');
let cur: any = localeObj;
for (let i = 0; i < parts.length; i++) {
if (cur == null) break;
cur = cur[parts[i]];
}
let text = typeof cur === 'string' ? cur : key;
if (replacements != null) {
if (Array.isArray(replacements)) {
replacements.forEach((val, idx) => {
text = text.split(`{${idx}}`).join(String(val));
});
} else if (typeof replacements === 'object') {
Object.keys(replacements).forEach(k => {
text = text.split(`{${k}}`).join(String(replacements[k]));
});
}
}
return text;
}
/**
* 获取所有可用主题
*/
getThemes(): Theme[] {
return this.themesRef.value.slice();
}
/**
* 获取当前主题
*/
getCurrentTheme(): Theme | null {
return this.currentThemeRef.value;
}
/**
* 切换主题并持久化
*/
setTheme(themeName: string) {
if (!this.themesRef.value || this.themesRef.value.length === 0) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] setTheme called but themes list empty');
return;
}
const theme = this.themesRef.value.find(t => t.name === themeName);
if (!theme) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] theme not found:', themeName);
return;
}
this.currentThemeRef.value = theme;
// 应用
this.applyTheme(theme);
// 持久化
this.writeStorage(THEME_STORAGE_KEY, themeName);
if (isDebugMode()) console.log('[ConfigProvider] setTheme ->', themeName);
}
/**
* 运行时更新当前主题颜色并应用(不持久化)
* @param colors 主题颜色键值,支持部分更新
*/
public setThemeColor(colors: Partial<ThemeColor>) {
if (!colors || Object.keys(colors).length === 0) return;
if (!this.currentThemeRef.value) {
if (isDebugMode('warn')) console.warn('[ConfigProvider] setThemeColor called but no current theme');
return;
}
const mode = this.getActiveMode();
if (mode === 'dark') {
const existing = this.currentThemeRef.value.darkColor || {};
this.currentThemeRef.value.darkColor = {
...existing,
...colors
};
} else {
const existing = this.currentThemeRef.value.color || {};
this.currentThemeRef.value.color = {
...existing,
...colors
};
}
// 重新应用当前主题以同步运行时 color、CSS 变量等
this.applyTheme(this.currentThemeRef.value);
if (isDebugMode()) console.log('[ConfigProvider] setThemeColor ->', colors);
}
/**
* 获取当前暗黑模式设置
*/
getDarkMode(): DarkMode {
return this.darkModeRef.value;
}
/**
* 设置暗黑模式
* @param mode 'auto' (跟随系统) | 'light' (强制亮色) | 'dark' (强制暗黑)
*/
setDarkMode(mode: DarkMode) {
this.darkModeRef.value = mode;
// 持久化
this.writeStorage(DARK_MODE_STORAGE_KEY, mode);
// 重新应用主题
this.applyTheme(this.currentThemeRef.value);
if (isDebugMode()) console.log('[ConfigProvider] setDarkMode ->', mode);
}
/**
* 检查当前是否处于暗黑模式
*/
isInDarkMode(): boolean {
const mode = this.darkModeRef.value;
if (mode === 'dark') return true;
if (mode === 'light') return false;
// auto 模式下检查系统设置
return this.isSystemDarkMode();
}
/**
* 归一化主题配置,保证始终至少有一个默认主题
*/
private normalizeThemes(themes?: Theme[]): Theme[] {
if (Array.isArray(themes) && themes.length) {
return this.mergeThemes(defaultThemes, themes);
}
return defaultThemes.slice();
}
private mergeThemes(...lists: Array<Theme[] | undefined>): Theme[] {
const map = new Map<string, Theme>();
lists
.filter((list): list is Theme[] => Array.isArray(list) && list.length > 0)
.forEach(list => {
list.forEach(theme => {
const normalized = this.ensureDarkVariant({
...theme,
color: this.applyColorFallbacks(theme.color),
darkColor: theme.darkColor ? { ...theme.darkColor } : undefined,
css: theme.css ? { ...theme.css } : undefined,
darkCss: theme.darkCss ? { ...theme.darkCss } : undefined
});
map.set(normalized.name, normalized);
});
});
return Array.from(map.values());
}
private ensureDarkVariant(theme: Theme): Theme {
const finalDark = this.buildDarkPalette(theme);
return {
...theme,
darkColor: this.applyDarkFallbacks(finalDark)
};
}
private buildDarkPalette(theme: Theme): Partial<ThemeColor> {
const provided = theme.darkColor || {};
const generated = this.generateDarkFromLight(theme.color || {}, provided);
return {
...generated,
...provided
};
}
/**
* 应用亮色主题
*/
private applyColorFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
return {
...(this.baseColorTokens || {}),
...(color || {})
};
}
/**
* 应用暗黑主题
*/
private applyDarkFallbacks(color?: Partial<ThemeColor>): Partial<ThemeColor> {
return {
...(this.baseDarkColorTokens || {}),
...(color || {})
};
}
private filterNonStructuralTokens(palette: Partial<ThemeColor>): Partial<ThemeColor> {
const result: Partial<ThemeColor> = {};
Object.entries(palette || {}).forEach(([key, value]) => {
if (!this.isStructuralToken(key)) {
(result as any)[key] = value;
}
});
return result;
}
private generateDarkFromLight(palette: Partial<ThemeColor>, provided: Partial<ThemeColor>): Partial<ThemeColor> {
const result: Partial<ThemeColor> = {};
const nonStructural = this.filterNonStructuralTokens(palette);
Object.entries(nonStructural).forEach(([key, value]) => {
if (typeof value !== 'string') return;
if (provided && Object.prototype.hasOwnProperty.call(provided, key)) {
return;
}
const fallback = (this.baseDarkColorTokens as any)?.[key];
(result as any)[key] = this.createDarkVariantFromLight(value, fallback);
});
return result;
}
private createDarkVariantFromLight(color: string, fallback?: string): string {
const normalized = this.normalizeHex(color);
const fallbackHex = fallback ? this.normalizeHex(fallback) : null;
if (normalized && fallbackHex) {
return this.mixHex(normalized, fallbackHex, 0.6);
}
if (fallbackHex) return fallbackHex;
return normalized || color;
}
private normalizeHex(color: string): string | null {
if (!color) return null;
const hex = color.trim();
if (/^#([0-9a-fA-F]{6})$/.test(hex)) return hex.toLowerCase();
return null;
}
private mixHex(fromHex: string, toHex: string, ratio: number): string {
const from = this.hexToRgb(fromHex);
const to = this.hexToRgb(toHex);
if (!from || !to) return toHex;
const clamp = (val: number) => Math.min(255, Math.max(0, Math.round(val)));
const r = clamp(from.r * (1 - ratio) + to.r * ratio);
const g = clamp(from.g * (1 - ratio) + to.g * ratio);
const b = clamp(from.b * (1 - ratio) + to.b * ratio);
return this.rgbToHex(r, g, b);
}
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const match = /^#([0-9a-fA-F]{6})$/.exec(hex);
if (!match) return null;
return {
r: parseInt(match[1].slice(0, 2), 16),
g: parseInt(match[1].slice(2, 4), 16),
b: parseInt(match[1].slice(4, 6), 16)
};
}
private rgbToHex(r: number, g: number, b: number): string {
const toHex = (val: number) => val.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
private isStructuralToken(token: string): boolean {
return STRUCTURAL_TOKENS.has(token);
}
/**
* 运行时同步主题颜色($u.color)
* 更新响应式 color 对象,确保所有使用 $u.color 的地方都能响应式更新
*/
private syncRuntimeTheme(palette: Partial<ThemeColor>) {
try {
// 合并默认值,确保所有颜色都有值
const defaultPalette = this.getActiveMode() === 'dark' ? this.baseDarkColorTokens : this.baseColorTokens;
const mergedPalette = {
...defaultPalette,
...palette
};
// 更新响应式 color 对象
Object.keys(mergedPalette).forEach(key => {
const value = (mergedPalette as any)[key];
if (value != null) {
(reactiveColor as any)[key] = value;
}
});
// 同步到 uni.$u.color(如果存在)
if (typeof uni !== 'undefined' && uni?.$u?.color) {
uni.$u.color = reactiveColor;
}
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] sync runtime theme failed', e);
}
}
/**
* 获取当前激活的模式
*/
private getActiveMode(): 'light' | 'dark' {
return this.isInDarkMode() ? 'dark' : 'light';
}
/**
* 获取当前主题的配色方案
*/
private getPaletteForMode(theme: Theme, mode: 'light' | 'dark') {
if (mode === 'dark') {
return theme.darkColor && Object.keys(theme.darkColor).length ? theme.darkColor : theme.color || {};
}
return theme.color || {};
}
/**
* 获取当前主题的CSS变量覆盖
*/
private getCssOverrides(theme: Theme, mode: 'light' | 'dark') {
if (mode === 'dark') {
return (theme.darkCss && Object.keys(theme.darkCss).length ? theme.darkCss : theme.css) || {};
}
return theme.css || {};
}
/**
* 读取Storage key
*/
private readStorage<T>(key: string): T | null {
try {
if (typeof uni === 'undefined' || typeof uni.getStorageSync !== 'function') return null;
const value = uni.getStorageSync(key);
return (value ?? null) as T | null;
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] failed to read storage', e);
return null;
}
}
/**
* 写入Storage key value
*/
private writeStorage(key: string, value: any) {
try {
if (typeof uni === 'undefined' || typeof uni.setStorageSync !== 'function') return;
uni.setStorageSync(key, value);
} catch (e) {
if (isDebugMode('error')) console.error('[ConfigProvider] failed to write storage', e);
}
}
/**
* 更新文档主题模式 H5
*/
private updateDocumentMode(mode: 'light' | 'dark') {
if (typeof document === 'undefined' || !document.documentElement) return;
const root = document.documentElement;
root.dataset.uThemeMode = mode;
root.classList.remove('u-theme-light', 'u-theme-dark');
root.classList.add(`u-theme-${mode}`);
}
/**
* 转换为 CSS 变量名称
*/
private toCssVarName(key: string, prefix: string = '--u'): string {
const types = config.type;
if (types.some(type => key.startsWith(type))) {
prefix += '-type';
}
const kebab = key
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase();
return `${prefix}-${kebab}`;
}
/**
* 添加 RGB 值
*/
private attachRgbVar(target: Record<string, string>, varName: string, value: string) {
if (typeof value !== 'string') return;
const hex = value.startsWith('#') ? value.slice(1) : '';
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return;
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
target[`${varName}-rgb`] = `${r}, ${g}, ${b}`;
}
/**
* 构建 CSS 变量映射表
* 生成格式:
*/
private buildCssVarMap(theme: Theme, mode: 'light' | 'dark'): Record<string, string> {
const map: Record<string, string> = {
'--u-theme-mode': mode
};
const palette = this.getPaletteForMode(theme, mode);
const cssOverrides = this.getCssOverrides(theme, mode);
const applyEntry = (key: string, value: string | number | undefined | null) => {
if (value == null) return;
const strValue = String(value);
if (key.startsWith('--')) {
map[key] = strValue;
this.attachRgbVar(map, key, strValue);
return;
}
const cssVarName = this.toCssVarName(key);
// const typeVarName = this.toCssVarName(key, '--u-type');
map[cssVarName] = strValue;
// map[typeVarName] = strValue;
this.attachRgbVar(map, cssVarName, strValue);
// this.attachRgbVar(map, typeVarName, strValue);
};
Object.entries(palette || {}).forEach(([key, value]) => applyEntry(key, value as any));
Object.entries(cssOverrides || {}).forEach(([key, value]) => applyEntry(key, value as any));
return map;
}
/**
* 刷新 CSS 变量 H5
*/
private flushCssVars(vars: Record<string, string>) {
if (typeof document === 'undefined' || !document.documentElement) return;
const root = document.documentElement;
this.lastAppliedCssKeys.forEach(key => {
root.style.removeProperty(key);
});
Object.entries(vars).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
this.lastAppliedCssKeys = Object.keys(vars);
}
/**
* 将主题应用到运行时:
* - 1) 调用 uni.$u.setColor(theme.color) 如果存在
* - 2) 在 H5 环境中,将 css map 注入到 document.documentElement 的 CSS 变量中
* - 3) 支持暗黑模式:根据 darkColor 或 color 应用相应的颜色
*/
private applyTheme(theme: Theme | null) {
if (!theme) return;
const mode = this.getActiveMode();
const palette = this.getPaletteForMode(theme, mode);
this.syncRuntimeTheme(palette);
const cssVarMap = this.buildCssVarMap(theme, mode);
this.cssVarsRef.value = cssVarMap;
this.flushCssVars(cssVarMap);
this.updateDocumentMode(mode);
}
}
// 单例导出
export const configProvider = new ConfigProvider();
export default configProvider;
@@ -0,0 +1,112 @@
import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
/**
* 适用于 uni-app Vue3 的事件派发/广播工具
* 用法:import { dispatch, broadcast } from './emitter'
*/
/**
* 向上查找父组件并派发事件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
// 将事件名转换为驼峰格式
// 例如:on-form-change -> onFormChange
function formatToCamelCase(str: string): string {
return str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
}
/**
* 向上查找父组件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @returns 父组件实例
*/
function parent(instance: ComponentInternalInstance | null | undefined = undefined, componentName: string = '') {
if (!instance) {
instance = getCurrentInstance();
}
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
if (!componentName) return parent;
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
return parent;
}
parent = parent.parent;
}
return null;
}
/** * 向上查找父组件并派发事件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function dispatch(
instance: ComponentInternalInstance | null | undefined,
componentName: string,
eventName: string,
...params: any[]
) {
let parent = instance && (instance.parent as ComponentInternalInstance | null | undefined);
while (parent) {
const name = (parent.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,派发事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
parent.emit && parent.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
parent.exposed?.[formatToCamelCase(eventName)] && parent.exposed[formatToCamelCase(eventName)](...params);
break;
}
parent = parent.parent;
}
}
/**
* 向下递归查找子组件并广播事件
* @param instance 当前组件实例(setup中可用getCurrentInstance())
* @param componentName 目标组件名
* @param eventName 事件名
* @param params 参数
*/
function broadcast(
instance: ComponentInternalInstance | null | undefined,
componentName: string,
eventName: string,
...params: any[]
) {
if (!instance) return;
const subTree = (instance.subTree as any)?.children || [];
const children = Array.isArray(subTree) ? subTree : [subTree];
children.forEach((vnode: any) => {
const child = vnode.component as ComponentInternalInstance | undefined;
if (child) {
const name = (child.type as any)?.name as string | undefined;
if (name === componentName) {
// 找到目标组件,广播事件
// Vue3未解决,目标组件事件监听失效,待优化,暂时使用下面的方式解决,如果你有好的方式也可以告诉我或者提PR
child.emit && child.emit(eventName, ...params);
// 如果有对应的方法,执行方法
// 这里可以考虑将 eventName 转换为驼峰格式
// 例如:on-form-change -> onFormChange
child.exposed?.[formatToCamelCase(eventName)] && child.exposed[formatToCamelCase(eventName)](...params);
} else {
broadcast(child, componentName, eventName, ...params);
}
}
});
}
export { dispatch, broadcast, parent };
@@ -0,0 +1,118 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/**
* copy to https://github.com/developit/mitt
* Expand clear method
*/
export type EventType = string | symbol;
// An event handler can take an optional event argument
// and should not return a value
export type Handler<T = unknown> = (event: T) => void;
export type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
// An array of all currently registered event handlers for a type
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
// A map of event types and their corresponding event handlers.
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
'*' | keyof Events,
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;
export interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
on(type: '*', handler: WildcardHandler<Events>): void;
off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
off(type: '*', handler: WildcardHandler<Events>): void;
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
clear(): void;
}
/**
* Mitt: Tiny (~200b) functional event emitter / pubsub.
* @name mitt
* @returns {Mitt}
*/
export function mitt<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>): Emitter<Events> {
type GenericEventHandler = Handler<Events[keyof Events]> | WildcardHandler<Events>;
all = all || new Map();
return {
/**
* A Map of event names to registered handler functions.
*/
all,
/**
* Register an event handler for the given type.
* @param {string|symbol} type Type of event to listen for, or `'*'` for all events
* @param {Function} handler Function to call in response to given event
* @memberOf mitt
*/
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
if (handlers) {
handlers.push(handler);
} else {
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
}
},
/**
* Remove an event handler for the given type.
* If `handler` is omitted, all handlers of the given type are removed.
* @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
* @param {Function} [handler] Handler function to remove
* @memberOf mitt
*/
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
if (handlers) {
if (handler) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
all!.set(type, []);
}
}
},
/**
* Invoke all handlers for the given type.
* If present, `'*'` handlers are invoked after type-matched handlers.
*
* Note: Manually firing '*' handlers is not supported.
*
* @param {string|symbol} type The event type to invoke
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
* @memberOf mitt
*/
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
let handlers = all!.get(type);
if (handlers) {
[...(handlers as EventHandlerList<Events[keyof Events]>)].forEach(handler => {
handler(evt as Events[Key]);
});
}
handlers = all!.get('*');
if (handlers) {
[...(handlers as WildCardEventHandlerList<Events>)].forEach(handler => {
handler(type, evt as Events[Key]);
});
}
},
/**
* Clear all
*/
clear() {
this.all.clear();
}
};
}
@@ -0,0 +1 @@
export default [{label:"北京市",value:"11"},{label:"天津市",value:"12"},{label:"河北省",value:"13"},{label:"山西省",value:"14"},{label:"内蒙古自治区",value:"15"},{label:"辽宁省",value:"21"},{label:"吉林省",value:"22"},{label:"黑龙江省",value:"23"},{label:"上海市",value:"31"},{label:"江苏省",value:"32"},{label:"浙江省",value:"33"},{label:"安徽省",value:"34"},{label:"福建省",value:"35"},{label:"江西省",value:"36"},{label:"山东省",value:"37"},{label:"河南省",value:"41"},{label:"湖北省",value:"42"},{label:"湖南省",value:"43"},{label:"广东省",value:"44"},{label:"广西壮族自治区",value:"45"},{label:"海南省",value:"46"},{label:"重庆市",value:"50"},{label:"四川省",value:"51"},{label:"贵州省",value:"52"},{label:"云南省",value:"53"},{label:"西藏自治区",value:"54"},{label:"陕西省",value:"61"},{label:"甘肃省",value:"62"},{label:"青海省",value:"63"},{label:"宁夏回族自治区",value:"64"},{label:"新疆维吾尔自治区",value:"65"},{label:"台湾",value:"66"},{label:"香港",value:"67"},{label:"澳门",value:"68"}];
@@ -0,0 +1,25 @@
declare const uni: any;
type SystemTheme = 'light' | 'dark';
/**
* 非 H5 平台获取当前系统主题
*/
export function getSystemDarkMode(): SystemTheme {
try {
if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
const systemInfo = uni.getSystemInfoSync();
const theme = systemInfo.osTheme || systemInfo.theme || 'light';
if (theme === 'dark') {
return 'dark';
}
if (theme === 'light') {
return 'light';
}
}
} catch (e) {
// 获取失败时默认返回亮色
console.warn('[system-theme] getSystemDarkMode failed', e);
}
return 'light';
}