小程序初始提交
This commit is contained in:
1769
cool-unix/cool/animation/index.ts
Normal file
1769
cool-unix/cool/animation/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
93
cool-unix/cool/ctx/index.ts
Normal file
93
cool-unix/cool/ctx/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { isArray, parse } from "../utils";
|
||||
|
||||
type Page = {
|
||||
path: string;
|
||||
style?: UTSJSONObject;
|
||||
meta?: UTSJSONObject;
|
||||
};
|
||||
|
||||
type SubPackage = {
|
||||
root: string;
|
||||
pages: Page[];
|
||||
};
|
||||
|
||||
export type TabBarItem = {
|
||||
text?: string;
|
||||
pagePath: string;
|
||||
iconPath?: string;
|
||||
selectedIconPath?: string;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
export type TabBar = {
|
||||
custom?: boolean;
|
||||
color?: string;
|
||||
selectedColor?: string;
|
||||
backgroundColor?: string;
|
||||
borderStyle?: string;
|
||||
blurEffect?: "dark" | "extralight" | "light" | "none";
|
||||
list?: TabBarItem[];
|
||||
position?: "top" | "bottom";
|
||||
fontSize?: string;
|
||||
iconWidth?: string;
|
||||
spacing?: string;
|
||||
height?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundRepeat?: "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
|
||||
redDotColor?: string;
|
||||
};
|
||||
|
||||
export type Ctx = {
|
||||
appid: string;
|
||||
globalStyle: UTSJSONObject;
|
||||
pages: Page[];
|
||||
uniIdRouter: UTSJSONObject;
|
||||
theme: UTSJSONObject;
|
||||
tabBar: TabBar;
|
||||
subPackages: SubPackage[];
|
||||
SAFE_CHAR_MAP_LOCALE: string[][];
|
||||
color: UTSJSONObject;
|
||||
};
|
||||
|
||||
// 初始化 ctx 对象,不可修改!!
|
||||
export const ctx = parse<Ctx>({})!;
|
||||
|
||||
console.log(ctx);
|
||||
|
||||
// PAGES 用于存储所有页面的路径及样式信息
|
||||
export let PAGES: Page[] = [...ctx.pages];
|
||||
|
||||
// 遍历 ctx.subPackages,将所有子包下的页面信息合并到 PAGES 中
|
||||
if (isArray(ctx.subPackages)) {
|
||||
ctx.subPackages.forEach((a) => {
|
||||
a.pages.forEach((b) => {
|
||||
PAGES.push({
|
||||
path: a.root + "/" + b.path, // 拼接子包根路径和页面路径
|
||||
style: b.style,
|
||||
meta: b.meta
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 确保每个页面路径都以 "/" 开头,符合 uni-app x 规范
|
||||
PAGES.forEach((e) => {
|
||||
if (!e.path.startsWith("/")) {
|
||||
e.path = "/" + e.path;
|
||||
}
|
||||
});
|
||||
|
||||
// TABS 用于存储 tabBar 配置项
|
||||
export let TABS: TabBarItem[] = [];
|
||||
|
||||
// 如果 tabBar 配置存在且列表不为空,则初始化 TABS
|
||||
if (ctx.tabBar.list != null) {
|
||||
TABS = ctx.tabBar.list;
|
||||
|
||||
// 确保每个 tabBar 页面的路径都以 "/" 开头
|
||||
TABS.forEach((e) => {
|
||||
if (!e.pagePath.startsWith("/")) {
|
||||
e.pagePath = "/" + e.pagePath;
|
||||
}
|
||||
});
|
||||
}
|
||||
28
cool-unix/cool/hooks/cache.ts
Normal file
28
cool-unix/cool/hooks/cache.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { reactive, watch } from "vue";
|
||||
import { isDark } from "../theme";
|
||||
|
||||
type CacheData = {
|
||||
key: number;
|
||||
};
|
||||
|
||||
type UseCache = {
|
||||
cache: CacheData;
|
||||
};
|
||||
|
||||
export const useCache = (source: () => any[]): UseCache => {
|
||||
const cache = reactive<CacheData>({
|
||||
key: 0
|
||||
});
|
||||
|
||||
watch(source, () => {
|
||||
cache.key++;
|
||||
});
|
||||
|
||||
watch(isDark, () => {
|
||||
cache.key++;
|
||||
});
|
||||
|
||||
return {
|
||||
cache
|
||||
};
|
||||
};
|
||||
6
cool-unix/cool/hooks/index.ts
Normal file
6
cool-unix/cool/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./cache";
|
||||
export * from "./long-press";
|
||||
export * from "./pager";
|
||||
export * from "./parent";
|
||||
export * from "./refs";
|
||||
export * from "./wx";
|
||||
100
cool-unix/cool/hooks/long-press.ts
Normal file
100
cool-unix/cool/hooks/long-press.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { vibrate } from "@/uni_modules/cool-vibrate";
|
||||
import { onUnmounted, ref, type Ref } from "vue";
|
||||
|
||||
// 长按触发延迟时间,单位毫秒
|
||||
const DELAY = 500;
|
||||
// 长按重复执行间隔时间,单位毫秒
|
||||
const REPEAT = 100;
|
||||
|
||||
/**
|
||||
* 长按操作钩子函数返回类型
|
||||
*/
|
||||
type UseLongPress = {
|
||||
// 开始长按
|
||||
start: (cb: () => void) => void;
|
||||
// 停止长按
|
||||
stop: () => void;
|
||||
// 清除定时器
|
||||
clear: () => void;
|
||||
// 是否正在长按中
|
||||
isPressing: Ref<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 长按操作钩子函数
|
||||
* 支持长按持续触发,可用于数字输入框等需要连续操作的场景
|
||||
*/
|
||||
export const useLongPress = (): UseLongPress => {
|
||||
// 是否正在长按中
|
||||
const isPressing = ref(false);
|
||||
// 长按延迟定时器
|
||||
let pressTimer: number = 0;
|
||||
// 重复执行定时器
|
||||
let repeatTimer: number = 0;
|
||||
|
||||
/**
|
||||
* 清除所有定时器
|
||||
* 重置长按状态
|
||||
*/
|
||||
const clear = () => {
|
||||
// 清除长按延迟定时器
|
||||
if (pressTimer != 0) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = 0;
|
||||
}
|
||||
// 清除重复执行定时器
|
||||
if (repeatTimer != 0) {
|
||||
clearInterval(repeatTimer);
|
||||
repeatTimer = 0;
|
||||
}
|
||||
// 重置长按状态
|
||||
isPressing.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始长按操作
|
||||
* @param cb 长按时重复执行的回调函数
|
||||
*/
|
||||
const start = (cb: () => void) => {
|
||||
// 清除已有定时器
|
||||
clear();
|
||||
|
||||
// 立即执行一次回调
|
||||
cb();
|
||||
|
||||
// 延迟500ms后开始长按
|
||||
// @ts-ignore
|
||||
pressTimer = setTimeout(() => {
|
||||
// 震动
|
||||
vibrate(1);
|
||||
|
||||
// 设置长按状态
|
||||
isPressing.value = true;
|
||||
// 每100ms重复执行回调
|
||||
// @ts-ignore
|
||||
repeatTimer = setInterval(() => {
|
||||
cb();
|
||||
}, REPEAT);
|
||||
}, DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止长按操作
|
||||
* 清除定时器并重置状态
|
||||
*/
|
||||
const stop = () => {
|
||||
clear();
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
clear,
|
||||
isPressing
|
||||
};
|
||||
};
|
||||
113
cool-unix/cool/hooks/pager.ts
Normal file
113
cool-unix/cool/hooks/pager.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { assign, parse } from "../utils";
|
||||
import { useListView, type ClListViewItem } from "@/uni_modules/cool-ui";
|
||||
|
||||
// 分页参数类型
|
||||
type Pagination = {
|
||||
page: number; // 当前页码
|
||||
size: number; // 每页数量
|
||||
total: number; // 总数量
|
||||
};
|
||||
|
||||
// 分页响应数据类型
|
||||
type PagerResponse = {
|
||||
list: UTSJSONObject[]; // 列表数据
|
||||
pagination: Pagination; // 分页信息
|
||||
};
|
||||
|
||||
// 分页回调函数类型
|
||||
type PagerCallback = (params: UTSJSONObject, ctx: Pager) => void | Promise<void>;
|
||||
|
||||
// 分页器类
|
||||
export class Pager {
|
||||
public page = 1; // 当前页码
|
||||
public size = 20; // 每页数量
|
||||
public total = 0; // 总数量
|
||||
public list = ref<UTSJSONObject[]>([]); // 列表数据
|
||||
public loading = ref(false); // 加载状态
|
||||
public refreshing = ref(false); // 刷新状态
|
||||
public finished = ref(false); // 是否加载完成
|
||||
public params = {} as UTSJSONObject; // 请求参数
|
||||
public cb: PagerCallback | null = null; // 回调函数
|
||||
|
||||
// 构造函数
|
||||
constructor(cb: PagerCallback) {
|
||||
this.cb = cb;
|
||||
}
|
||||
|
||||
// 完成加载
|
||||
done() {
|
||||
this.loading.value = false;
|
||||
}
|
||||
|
||||
// 清空数据
|
||||
clear() {
|
||||
this.list.value = [];
|
||||
this.finished.value = false;
|
||||
this.refreshing.value = false;
|
||||
this.loading.value = false;
|
||||
}
|
||||
|
||||
// 渲染数据
|
||||
public render = (res: any) => {
|
||||
const { list, pagination } = parse<PagerResponse>(res)!;
|
||||
|
||||
// 更新分页信息
|
||||
this.page = pagination.page;
|
||||
this.size = pagination.size;
|
||||
this.total = pagination.total;
|
||||
|
||||
// 更新列表数据
|
||||
if (this.params.page == 1) {
|
||||
this.list.value = [...list];
|
||||
} else {
|
||||
this.list.value.push(...list);
|
||||
}
|
||||
|
||||
// 更新加载完成状态
|
||||
this.finished.value = this.list.value.length >= this.total;
|
||||
|
||||
// 完成加载
|
||||
this.done();
|
||||
};
|
||||
|
||||
// 刷新数据
|
||||
public refresh = async (params: UTSJSONObject) => {
|
||||
// 合并参数
|
||||
this.params = assign(this.params, params);
|
||||
|
||||
// 构建请求参数
|
||||
const data = {
|
||||
page: this.page,
|
||||
size: this.size,
|
||||
...this.params
|
||||
};
|
||||
|
||||
// 开始加载
|
||||
this.loading.value = true;
|
||||
|
||||
// 发起请求
|
||||
await this.cb!(data, this);
|
||||
};
|
||||
|
||||
// 加载更多数据
|
||||
public loadMore = () => {
|
||||
if (this.loading.value || this.finished.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refresh({
|
||||
page: this.page + 1
|
||||
});
|
||||
};
|
||||
|
||||
// 列表视图数据
|
||||
public listView = computed<ClListViewItem[]>(() => {
|
||||
return useListView(this.list.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建分页器实例
|
||||
export const usePager = (cb: PagerCallback): Pager => {
|
||||
return new Pager(cb);
|
||||
};
|
||||
22
cool-unix/cool/hooks/parent.ts
Normal file
22
cool-unix/cool/hooks/parent.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getCurrentInstance } from "vue";
|
||||
|
||||
/**
|
||||
* 获取父组件
|
||||
* @param name 组件名称
|
||||
* @example useParent<ClFormComponentPublicInstance>("cl-form")
|
||||
* @returns 父组件
|
||||
*/
|
||||
export function useParent<T>(name: string): T | null {
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
|
||||
let p = proxy?.$parent;
|
||||
|
||||
while (p != null) {
|
||||
if (p.$options.name == name) {
|
||||
return p as T | null;
|
||||
}
|
||||
p = p.$parent;
|
||||
}
|
||||
|
||||
return p as T | null;
|
||||
}
|
||||
122
cool-unix/cool/hooks/refs.ts
Normal file
122
cool-unix/cool/hooks/refs.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { reactive } from "vue";
|
||||
import { isNull } from "../utils";
|
||||
|
||||
// #ifdef APP
|
||||
// @ts-ignore
|
||||
type Instance = ComponentPublicInstance | null;
|
||||
// #endif
|
||||
|
||||
// #ifndef APP
|
||||
// @ts-ignore
|
||||
type Instance = any;
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Refs 类用于管理组件引用,便于在组合式 API 中获取、操作子组件实例。
|
||||
*/
|
||||
class Refs {
|
||||
// 存储所有 ref 的响应式对象,key 为 ref 名称,value 为组件实例
|
||||
data = reactive({} as UTSJSONObject);
|
||||
|
||||
/**
|
||||
* 生成 ref 绑定函数,用于在模板中设置 ref。
|
||||
* @param name ref 名称
|
||||
* @returns 绑定函数 (el: Instance) => void
|
||||
*/
|
||||
set(name: string) {
|
||||
return (el: Instance) => {
|
||||
this.data[name] = el;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定名称的组件实例
|
||||
* @param name ref 名称
|
||||
* @returns 组件实例或 null
|
||||
*/
|
||||
get(name: string): Instance {
|
||||
const d = this.data[name] as ComponentPublicInstance;
|
||||
|
||||
if (isNull(d)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件实例暴露的属性或方法(兼容不同平台)
|
||||
* @param name ref 名称
|
||||
* @param key 暴露的属性名
|
||||
* @returns 属性值或 null
|
||||
*/
|
||||
getExposed<T>(name: string, key: string): T | null {
|
||||
// #ifdef APP-ANDROID
|
||||
const d = this.get(name);
|
||||
|
||||
if (isNull(d)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安卓平台下,$exposed 为 Map<string, any>
|
||||
const ex = d!.$exposed as Map<string, any>;
|
||||
|
||||
if (isNull(ex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ex[key] as T | null;
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
// 其他平台直接通过属性访问
|
||||
return this.get(name)?.[key] as T;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用组件实例暴露的方法,并返回结果
|
||||
* @param name ref 名称
|
||||
* @param method 方法名
|
||||
* @param data 传递的数据
|
||||
* @returns 方法返回值
|
||||
*/
|
||||
call<T>(name: string, method: string, data: UTSJSONObject | null = null): T {
|
||||
return this.get(name)!.$callMethod(method, data) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用组件实例暴露的方法,无返回值
|
||||
* @param name ref 名称
|
||||
* @param method 方法名
|
||||
* @param data 传递的数据
|
||||
*/
|
||||
callMethod(name: string, method: string, data: UTSJSONObject | null = null): void {
|
||||
this.get(name)!.$callMethod(method, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用组件的 open 方法,常用于弹窗、抽屉等组件
|
||||
* @param name ref 名称
|
||||
* @param data 传递的数据
|
||||
*/
|
||||
open(name: string, data: UTSJSONObject | null = null) {
|
||||
this.callMethod(name, "open", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用组件的 close 方法,常用于弹窗、抽屉等组件
|
||||
* @param name ref 名称
|
||||
*/
|
||||
close(name: string) {
|
||||
return this.callMethod(name, "close");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* useRefs 组合式函数,返回 Refs 实例
|
||||
* @returns Refs 实例
|
||||
*/
|
||||
export function useRefs(): Refs {
|
||||
return new Refs();
|
||||
}
|
||||
247
cool-unix/cool/hooks/wx.ts
Normal file
247
cool-unix/cool/hooks/wx.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ref } from "vue";
|
||||
import { assign, getUrlParam, storage } from "../utils";
|
||||
import { request } from "../service";
|
||||
import { t } from "@/locale";
|
||||
import { config } from "@/config";
|
||||
|
||||
// #ifdef H5
|
||||
import wx from "weixin-js-sdk";
|
||||
// #endif
|
||||
|
||||
// 微信配置类型
|
||||
type WxConfig = {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
// 微信相关功能封装类
|
||||
export class Wx {
|
||||
// 微信登录code
|
||||
code = ref("");
|
||||
|
||||
/**
|
||||
* 获取微信登录code
|
||||
*/
|
||||
async getCode(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: "weixin",
|
||||
success: (res) => {
|
||||
this.code.value = res.code;
|
||||
resolve(res.code);
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve("");
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// 公众号配置
|
||||
mpConfig: WxConfig = {
|
||||
appId: ""
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断当前是否为微信浏览器
|
||||
*/
|
||||
isWxBrowser() {
|
||||
const ua: string = window.navigator.userAgent.toLowerCase();
|
||||
if (ua.match(/MicroMessenger/i) != null) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公众号配置信息,并初始化微信JS-SDK
|
||||
*/
|
||||
getMpConfig() {
|
||||
if (this.isWxBrowser()) {
|
||||
request({
|
||||
url: "/app/user/common/wxMpConfig",
|
||||
method: "POST",
|
||||
data: {
|
||||
url: `${location.origin}${location.pathname}`
|
||||
}
|
||||
}).then((res) => {
|
||||
if (res != null) {
|
||||
wx.config({
|
||||
debug: config.wx.debug,
|
||||
jsApiList: res.jsApiList || ["chooseWXPay"],
|
||||
appId: res.appId,
|
||||
timestamp: res.timestamp,
|
||||
nonceStr: res.nonceStr,
|
||||
signature: res.signature,
|
||||
openTagList: res.openTagList
|
||||
});
|
||||
|
||||
// 合并配置到mpConfig
|
||||
assign(this.mpConfig, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到微信授权页面
|
||||
*/
|
||||
mpAuth() {
|
||||
const { appId } = this.mpConfig;
|
||||
|
||||
const redirect_uri = encodeURIComponent(
|
||||
`${location.origin}${location.pathname}#/pages/user/login`
|
||||
);
|
||||
const response_type = "code";
|
||||
const scope = "snsapi_userinfo";
|
||||
const state = "STATE";
|
||||
|
||||
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}#wechat_redirect`;
|
||||
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众号登录,获取code
|
||||
*/
|
||||
mpLogin() {
|
||||
return new Promise((resolve) => {
|
||||
const code = getUrlParam("code");
|
||||
const mpCode = storage.get("mpCode");
|
||||
|
||||
// 去除url中的code参数,避免重复
|
||||
const url = window.location.href.replace(/(\?[^#]*)#/, "#");
|
||||
window.history.replaceState({}, "", url);
|
||||
|
||||
if (code != mpCode) {
|
||||
storage.set("mpCode", code, 1000 * 60 * 5);
|
||||
resolve(code);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众号微信支付
|
||||
* @param params 支付参数
|
||||
*/
|
||||
mpPay(params: wx.IchooseWXPay & { timeStamp: number }): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isWxBrowser()) {
|
||||
return reject({ message: t("请在微信浏览器中打开") });
|
||||
}
|
||||
|
||||
wx.chooseWXPay({
|
||||
...params,
|
||||
timestamp: params.timeStamp,
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
complete(e: { errMsg: string }) {
|
||||
switch (e.errMsg) {
|
||||
case "chooseWXPay:cancel":
|
||||
reject({ message: t("已取消支付") });
|
||||
break;
|
||||
|
||||
default:
|
||||
reject({ message: t("支付失败") });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
/**
|
||||
* 小程序登录,获取用户信息和code
|
||||
*/
|
||||
miniLogin(): Promise<{
|
||||
code: string;
|
||||
iv: string;
|
||||
encryptedData: string;
|
||||
signature: string;
|
||||
rawData: string;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 兼容 Mac,Mac 端需用 getUserInfo
|
||||
const k = uni.getDeviceInfo().platform === "mac" ? "getUserInfo" : "getUserProfile";
|
||||
|
||||
uni[k]({
|
||||
lang: "zh_CN",
|
||||
desc: t("授权信息仅用于用户登录"),
|
||||
success: ({ iv, encryptedData, signature, rawData }) => {
|
||||
const next = () => {
|
||||
resolve({
|
||||
iv,
|
||||
encryptedData,
|
||||
signature,
|
||||
rawData,
|
||||
code: this.code.value
|
||||
});
|
||||
};
|
||||
|
||||
// 检查登录状态是否过期
|
||||
uni.checkSession({
|
||||
success: () => {
|
||||
next();
|
||||
},
|
||||
fail: () => {
|
||||
this.getCode().then(() => {
|
||||
next();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(`[useWx.miniLogin] error`, err);
|
||||
this.getCode();
|
||||
|
||||
reject(t("登录授权失败"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序微信支付
|
||||
* @param params 支付参数
|
||||
*/
|
||||
miniPay(params: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.requestPayment({
|
||||
provider: "wxpay",
|
||||
...params,
|
||||
success() {
|
||||
resolve();
|
||||
},
|
||||
fail() {
|
||||
reject(t("已取消支付"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* useWx 钩子函数,后续可扩展
|
||||
*/
|
||||
export const useWx = (): Wx => {
|
||||
const wx = new Wx();
|
||||
|
||||
onReady(() => {
|
||||
wx.getCode();
|
||||
|
||||
// #ifdef H5
|
||||
wx.getMpConfig();
|
||||
// #endif
|
||||
});
|
||||
|
||||
return wx;
|
||||
};
|
||||
46
cool-unix/cool/index.ts
Normal file
46
cool-unix/cool/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { watch } from "vue";
|
||||
import { scroller } from "./scroller";
|
||||
import { initTheme, setH5 } from "./theme";
|
||||
import { initLocale, locale, updateTitle } from "@/locale";
|
||||
import "@/uni_modules/cool-ui";
|
||||
|
||||
export function cool(app: VueApp) {
|
||||
app.mixin({
|
||||
onPageScroll(e) {
|
||||
scroller.emit(e.scrollTop);
|
||||
},
|
||||
onShow() {
|
||||
// 更新标题
|
||||
updateTitle();
|
||||
|
||||
// #ifdef H5
|
||||
setTimeout(() => {
|
||||
setH5();
|
||||
}, 0);
|
||||
// #endif
|
||||
},
|
||||
onLoad() {
|
||||
// 监听语言切换,更新标题
|
||||
watch(locale, () => {
|
||||
updateTitle();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
initTheme();
|
||||
initLocale();
|
||||
|
||||
console.log(app);
|
||||
}
|
||||
|
||||
export * from "./animation";
|
||||
export * from "./ctx";
|
||||
export * from "./hooks";
|
||||
export * from "./router";
|
||||
export * from "./scroller";
|
||||
export * from "./service";
|
||||
export * from "./store";
|
||||
export * from "./theme";
|
||||
export * from "./upload";
|
||||
export * from "./utils";
|
||||
export * from "./types";
|
||||
362
cool-unix/cool/router/index.ts
Normal file
362
cool-unix/cool/router/index.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { PAGES, TABS } from "../ctx";
|
||||
import type { BackOptions, PageInstance, PushOptions } from "../types";
|
||||
import {
|
||||
storage,
|
||||
last,
|
||||
isNull,
|
||||
isEmpty,
|
||||
get,
|
||||
isFunction,
|
||||
toArray,
|
||||
map,
|
||||
debounce,
|
||||
nth,
|
||||
assign,
|
||||
parse
|
||||
} from "../utils";
|
||||
|
||||
// 路由信息类型
|
||||
type RouteInfo = {
|
||||
path: string;
|
||||
query: UTSJSONObject;
|
||||
meta: UTSJSONObject;
|
||||
isAuth?: boolean;
|
||||
};
|
||||
|
||||
// 跳转前钩子类型
|
||||
type BeforeEach = (to: RouteInfo, from: PageInstance, next: () => void) => void;
|
||||
// 登录后回调类型
|
||||
type AfterLogin = () => void;
|
||||
|
||||
// 路由事件集合
|
||||
type Events = {
|
||||
beforeEach?: BeforeEach;
|
||||
afterLogin?: AfterLogin;
|
||||
};
|
||||
|
||||
// 路由核心类
|
||||
export class Router {
|
||||
private eventsMap = {} as Events; // 事件存储
|
||||
|
||||
// 获取传递的 params 参数
|
||||
params() {
|
||||
return (storage.get("router-params") ?? {}) as UTSJSONObject;
|
||||
}
|
||||
|
||||
// 获取传递的 query 参数
|
||||
query() {
|
||||
return this.route()?.query ?? {};
|
||||
}
|
||||
|
||||
// 获取默认路径,支持 home 和 login
|
||||
defaultPath(name: "home" | "login") {
|
||||
const paths = {
|
||||
home: PAGES[0].path, // 首页为第一个页面
|
||||
login: "/pages/user/login"
|
||||
};
|
||||
|
||||
return get(paths, name) as string;
|
||||
}
|
||||
|
||||
// 获取当前页面栈的所有页面实例
|
||||
getPages(): PageInstance[] {
|
||||
return map(getCurrentPages(), (e) => {
|
||||
let path = e.route!;
|
||||
|
||||
// 根路径自动转为首页
|
||||
if (path == "/") {
|
||||
path = this.defaultPath("home");
|
||||
}
|
||||
|
||||
// 补全路径前缀
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
// 获取页面样式
|
||||
const page = PAGES.find((e) => e.path == path);
|
||||
const style = page?.style;
|
||||
const meta = page?.meta;
|
||||
|
||||
// 获取页面暴露的方法
|
||||
// @ts-ignore
|
||||
const vm = e.vm as any;
|
||||
|
||||
let exposed = vm;
|
||||
|
||||
// #ifdef H5
|
||||
exposed = get(e, "vm.$.exposed");
|
||||
// #endif
|
||||
|
||||
// 获取页面 query 参数
|
||||
// @ts-ignore
|
||||
const query = e.options;
|
||||
|
||||
return {
|
||||
path,
|
||||
vm,
|
||||
exposed,
|
||||
style,
|
||||
meta,
|
||||
query,
|
||||
isCustomNavbar: style?.navigationStyle == "custom"
|
||||
} as PageInstance;
|
||||
});
|
||||
}
|
||||
|
||||
// 获取指定路径的页面实例
|
||||
getPage(path: string) {
|
||||
return this.getPages().find((e) => e.path == path);
|
||||
}
|
||||
|
||||
// 获取当前路由页面实例
|
||||
route() {
|
||||
return last(this.getPages());
|
||||
}
|
||||
|
||||
// 获取当前页面路径
|
||||
path() {
|
||||
return this.route()?.path ?? "";
|
||||
}
|
||||
|
||||
// 简单跳转页面(默认 navigateTo)
|
||||
to(path: string) {
|
||||
this.push({
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
// 路由跳转,支持多种模式和参数
|
||||
push(options: PushOptions) {
|
||||
let {
|
||||
query = {},
|
||||
params = {},
|
||||
mode = "navigateTo",
|
||||
path,
|
||||
success,
|
||||
fail,
|
||||
complete,
|
||||
animationType,
|
||||
animationDuration,
|
||||
events,
|
||||
isAuth
|
||||
} = options;
|
||||
|
||||
// 拼接 query 参数到 url
|
||||
if (!isEmpty(query)) {
|
||||
const arr = toArray(query, (v, k) => {
|
||||
return `${k}=${v}`;
|
||||
});
|
||||
path += "?" + arr.join("&");
|
||||
}
|
||||
|
||||
// params 通过 storage 临时存储
|
||||
if (!isEmpty(params)) {
|
||||
storage.set("router-params", params, 0);
|
||||
}
|
||||
|
||||
// tabBar 页面强制使用 switchTab 跳转
|
||||
if (this.isTabPage(path)) {
|
||||
mode = "switchTab";
|
||||
}
|
||||
|
||||
// 跳转执行函数
|
||||
const next = () => {
|
||||
switch (mode) {
|
||||
case "navigateTo":
|
||||
uni.navigateTo({
|
||||
url: path,
|
||||
success,
|
||||
events,
|
||||
fail,
|
||||
complete,
|
||||
animationType,
|
||||
animationDuration
|
||||
});
|
||||
break;
|
||||
case "redirectTo":
|
||||
uni.redirectTo({
|
||||
url: path,
|
||||
success,
|
||||
fail,
|
||||
complete
|
||||
});
|
||||
break;
|
||||
case "reLaunch":
|
||||
uni.reLaunch({
|
||||
url: path,
|
||||
success,
|
||||
fail,
|
||||
complete
|
||||
});
|
||||
break;
|
||||
case "switchTab":
|
||||
uni.switchTab({
|
||||
url: path,
|
||||
success,
|
||||
fail,
|
||||
complete
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转前钩子处理
|
||||
if (this.eventsMap.beforeEach != null) {
|
||||
// 当前页
|
||||
const from = last(this.getPages());
|
||||
|
||||
// 跳转页
|
||||
const to = { path, meta: this.getMeta(path), query, isAuth } as RouteInfo;
|
||||
|
||||
// 调用跳转前钩子
|
||||
this.eventsMap.beforeEach(to, from!, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
// 回到首页
|
||||
home() {
|
||||
this.push({
|
||||
path: this.defaultPath("home")
|
||||
});
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
back(options: BackOptions | null = null) {
|
||||
if (this.isFirstPage()) {
|
||||
this.home();
|
||||
} else {
|
||||
const delta = options?.delta ?? 1;
|
||||
|
||||
// 执行跳转函数
|
||||
const next = () => {
|
||||
uni.navigateBack({ ...(options ?? {}) });
|
||||
};
|
||||
|
||||
// 跳转前钩子处理
|
||||
if (this.eventsMap.beforeEach != null) {
|
||||
// 当前页
|
||||
const from = last(this.getPages());
|
||||
|
||||
// 上一页
|
||||
const to = nth(this.getPages(), -delta - 1);
|
||||
|
||||
if (to != null) {
|
||||
// 调用跳转前钩子
|
||||
this.eventsMap.beforeEach(
|
||||
{
|
||||
path: to.path,
|
||||
query: to.query,
|
||||
meta: to.meta ?? ({} as UTSJSONObject)
|
||||
},
|
||||
from!,
|
||||
next
|
||||
);
|
||||
} else {
|
||||
console.error("[router] found to page is null");
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面元数据
|
||||
getMeta(path: string) {
|
||||
return PAGES.find((e) => path.includes(e.path))?.meta ?? ({} as UTSJSONObject);
|
||||
}
|
||||
|
||||
// 执行当前页面暴露的方法
|
||||
callMethod(name: string, data?: any): any | null {
|
||||
const fn = get(this.route()!, `$vm.$.exposed.${name}`) as (d?: any) => any | null;
|
||||
if (isFunction(fn)) {
|
||||
return fn(data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断页面栈是否只有一个页面
|
||||
isFirstPage() {
|
||||
return getCurrentPages().length == 1;
|
||||
}
|
||||
|
||||
// 判断是否为首页
|
||||
isHomePage() {
|
||||
return this.path() == this.defaultPath("home");
|
||||
}
|
||||
|
||||
// 判断是否为自定义导航栏页面
|
||||
isCustomNavbarPage() {
|
||||
return this.route()?.isCustomNavbar ?? false;
|
||||
}
|
||||
|
||||
// 判断是否为当前页面
|
||||
isCurrentPage(path: string) {
|
||||
return this.path() == path;
|
||||
}
|
||||
|
||||
// 判断是否为 tab 页面
|
||||
isTabPage(path: string | null = null) {
|
||||
if (path == null) {
|
||||
path = this.path();
|
||||
}
|
||||
|
||||
if (path == "/") {
|
||||
path = this.defaultPath("home");
|
||||
}
|
||||
return !isNull(TABS.find((e) => path == e.pagePath));
|
||||
}
|
||||
|
||||
// 判断是否为登录页
|
||||
isLoginPage(path: string) {
|
||||
return path == this.defaultPath("login");
|
||||
}
|
||||
|
||||
// 跳转到登录页(防抖处理)
|
||||
login = debounce(() => {
|
||||
if (!this.isLoginPage(this.path())) {
|
||||
this.push({
|
||||
path: "/pages/user/login",
|
||||
mode: "reLaunch"
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 登录成功后跳转逻辑
|
||||
nextLogin() {
|
||||
const pages = this.getPages();
|
||||
|
||||
// 找到登录页的索引
|
||||
const index = pages.findIndex((e) => this.defaultPath("login").includes(e.path));
|
||||
|
||||
// 未找到,则跳回首页
|
||||
if (index < 0) {
|
||||
this.home();
|
||||
} else {
|
||||
this.back({
|
||||
delta: pages.length - index
|
||||
});
|
||||
}
|
||||
// 登录后回调
|
||||
if (this.eventsMap.afterLogin != null) {
|
||||
this.eventsMap.afterLogin!();
|
||||
}
|
||||
// 触发全局 afterLogin 事件
|
||||
uni.$emit("afterLogin");
|
||||
}
|
||||
|
||||
// 注册跳转前钩子
|
||||
beforeEach(cb: BeforeEach) {
|
||||
this.eventsMap.beforeEach = cb;
|
||||
}
|
||||
|
||||
// 注册登录后回调
|
||||
afterLogin(cb: AfterLogin) {
|
||||
this.eventsMap.afterLogin = cb;
|
||||
}
|
||||
}
|
||||
|
||||
// 单例导出
|
||||
export const router = new Router();
|
||||
33
cool-unix/cool/scroller/index.ts
Normal file
33
cool-unix/cool/scroller/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { router } from "../router";
|
||||
|
||||
class Scroller {
|
||||
list: Map<string, ((top: number) => void)[]> = new Map();
|
||||
|
||||
// 触发滚动
|
||||
emit(top: number) {
|
||||
const cbs = this.list.get(router.path()) ?? [];
|
||||
cbs.forEach((cb) => {
|
||||
cb(top);
|
||||
});
|
||||
}
|
||||
|
||||
// 监听页面滚动
|
||||
on(callback: (top: number) => void) {
|
||||
const path = router.path();
|
||||
const cbs = this.list.get(path) ?? [];
|
||||
cbs.push(callback);
|
||||
this.list.set(path, cbs);
|
||||
}
|
||||
|
||||
// 取消监听页面滚动
|
||||
off = (callback: (top: number) => void) => {
|
||||
const path = router.path();
|
||||
const cbs = this.list.get(path) ?? [];
|
||||
this.list.set(
|
||||
path,
|
||||
cbs.filter((cb) => cb != callback)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const scroller = new Scroller();
|
||||
198
cool-unix/cool/service/index.ts
Normal file
198
cool-unix/cool/service/index.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { isDev, ignoreTokens, config } from "@/config";
|
||||
import { locale, t } from "@/locale";
|
||||
import { isNull, isObject, parse, storage } from "../utils";
|
||||
import { useStore } from "../store";
|
||||
|
||||
// 请求参数类型定义
|
||||
export type RequestOptions = {
|
||||
url: string; // 请求地址
|
||||
method?: RequestMethod; // 请求方法
|
||||
data?: any; // 请求体数据
|
||||
params?: any; // URL参数
|
||||
header?: any; // 请求头
|
||||
timeout?: number; // 超时时间
|
||||
withCredentials?: boolean; // 是否携带凭证
|
||||
firstIpv4?: boolean; // 是否优先使用IPv4
|
||||
enableChunked?: boolean; // 是否启用分块传输
|
||||
};
|
||||
|
||||
// 响应数据类型定义
|
||||
export type Response = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
// 请求队列(用于等待token刷新后继续请求)
|
||||
let requests: ((token: string) => void)[] = [];
|
||||
|
||||
// 标记token是否正在刷新
|
||||
let isRefreshing = false;
|
||||
|
||||
// 判断当前url是否忽略token校验
|
||||
const isIgnoreToken = (url: string) => {
|
||||
return ignoreTokens.some((e) => {
|
||||
const pattern = e.replace(/\*/g, ".*");
|
||||
return new RegExp(pattern).test(url);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
* @param options 请求参数
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
export function request(options: RequestOptions): Promise<any | null> {
|
||||
let { url, method = "GET", data = {}, params, header = {}, timeout = 60000 } = options;
|
||||
|
||||
const { user } = useStore();
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
if (isDev) {
|
||||
console.log(`[${method}] ${url}`, params || data);
|
||||
}
|
||||
|
||||
// 拼接基础url
|
||||
if (!url.startsWith("http")) {
|
||||
url = config.baseUrl + url;
|
||||
}
|
||||
|
||||
// 处理GET请求的params参数,拼接到URL上
|
||||
if (method === "GET" && params) {
|
||||
const queryString = Object.keys(params)
|
||||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
|
||||
if (queryString) {
|
||||
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
console.log(`[GET] 完整URL: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前token
|
||||
let Authorization: string | null = user.token;
|
||||
|
||||
// 如果是忽略token的接口,则不携带token
|
||||
if (isIgnoreToken(url)) {
|
||||
Authorization = null;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 发起请求的实际函数
|
||||
const next = () => {
|
||||
uni.request({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
header: {
|
||||
Authorization,
|
||||
language: locale.value,
|
||||
...(header as UTSJSONObject)
|
||||
},
|
||||
timeout,
|
||||
|
||||
success(res) {
|
||||
// 401 无权限
|
||||
if (res.statusCode == 401) {
|
||||
user.logout();
|
||||
reject({ message: t("无权限") } as Response);
|
||||
}
|
||||
|
||||
// 502 服务异常
|
||||
else if (res.statusCode == 502) {
|
||||
reject({
|
||||
message: t("服务异常")
|
||||
} as Response);
|
||||
}
|
||||
|
||||
// 404 未找到
|
||||
else if (res.statusCode == 404) {
|
||||
return reject({
|
||||
message: `[404] ${url}`
|
||||
} as Response);
|
||||
}
|
||||
|
||||
// 200 正常响应
|
||||
else if (res.statusCode == 200) {
|
||||
if (res.data == null) {
|
||||
resolve(null);
|
||||
} else if (!isObject(res.data as any)) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
// 解析响应数据
|
||||
const { code, message, data } = parse<Response>(
|
||||
res.data ?? { code: 0 }
|
||||
)!;
|
||||
|
||||
switch (code) {
|
||||
case 1000:
|
||||
resolve(data);
|
||||
break;
|
||||
default:
|
||||
reject({ message, code } as Response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reject({ message: t("服务异常") } as Response);
|
||||
}
|
||||
},
|
||||
|
||||
// 网络请求失败
|
||||
fail(err) {
|
||||
reject({ message: err.errMsg } as Response);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 非刷新token接口才进行token有效性校验
|
||||
if (!options.url.includes("/refreshToken")) {
|
||||
if (!isNull(Authorization)) {
|
||||
// 判断token是否过期
|
||||
if (storage.isExpired("token")) {
|
||||
// 判断refreshToken是否过期
|
||||
if (storage.isExpired("refreshToken")) {
|
||||
// 刷新token也过期,直接退出登录
|
||||
user.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前没有在刷新token,则发起刷新
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
user.refreshToken()
|
||||
.then((token) => {
|
||||
// 刷新成功后,执行队列中的请求
|
||||
requests.forEach((cb) => cb(token));
|
||||
requests = [];
|
||||
isRefreshing = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
user.logout();
|
||||
});
|
||||
}
|
||||
|
||||
// 将当前请求加入队列,等待token刷新后再执行
|
||||
new Promise((resolve) => {
|
||||
requests.push((token: string) => {
|
||||
// 重新设置token
|
||||
Authorization = token;
|
||||
next();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
// 此处return,等待token刷新
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// token有效,直接发起请求
|
||||
next();
|
||||
});
|
||||
}
|
||||
136
cool-unix/cool/store/dict.ts
Normal file
136
cool-unix/cool/store/dict.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { reactive } from "vue";
|
||||
import { request } from "../service";
|
||||
import { forInObject, isNull, parse } from "../utils";
|
||||
|
||||
// 字典项类型定义
|
||||
export type DictItem = {
|
||||
id: number; // 字典项ID
|
||||
typeId: number; // 字典类型ID
|
||||
label: string; // 显示标签
|
||||
name: string; // 可选名称
|
||||
value: any; // 字典项值
|
||||
orderNum: number; // 排序号
|
||||
parentId?: number | null; // 父级ID,可选
|
||||
};
|
||||
|
||||
// 字典数据类型定义
|
||||
export type DictData = {
|
||||
key: string; // 字典key
|
||||
list: DictItem[]; // 字典项列表
|
||||
};
|
||||
|
||||
// 字典管理类
|
||||
export class Dict {
|
||||
private data: DictData[] = reactive([]); // 存储所有字典数据
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* 获取指定key的字典数据
|
||||
* @param key 字典key
|
||||
* @returns 字典数据
|
||||
*/
|
||||
find(key: string) {
|
||||
return this.data.find((e) => e.key == key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定key的字典项列表
|
||||
* @param key 字典key
|
||||
* @returns 字典项数组
|
||||
*/
|
||||
get(key: string): DictItem[] {
|
||||
return this.find(key)?.list ?? new Array<DictItem>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定key和value的字典项
|
||||
* @param key 字典key
|
||||
* @param value 字典项值
|
||||
* @returns 字典项或null
|
||||
*/
|
||||
getItem(key: string, value: any): DictItem | null {
|
||||
const item = this.get(key).find((e) => e.value == value);
|
||||
|
||||
if (isNull(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定key和多个value的字典项数组
|
||||
* @param key 字典key
|
||||
* @param values 字典项值数组
|
||||
* @returns 字典项数组
|
||||
*/
|
||||
getItems(key: string, values: any[]): DictItem[] {
|
||||
return values.map((e) => this.getItem(key, e)).filter((e) => !isNull(e)) as DictItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定key和value的字典项的label
|
||||
* @param key 字典key
|
||||
* @param value 字典项值
|
||||
* @returns 字典项label字符串
|
||||
*/
|
||||
getItemLabel(key: string, value: any): string {
|
||||
const item = this.getItem(key, value);
|
||||
|
||||
if (isNull(item) || isNull(item?.label)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return item!.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新字典数据
|
||||
* @param types 可选,指定需要刷新的字典key数组
|
||||
*/
|
||||
async refresh(types?: string[] | null): Promise<void> {
|
||||
const res = await request({
|
||||
url: "/app/dict/info/data",
|
||||
method: "POST",
|
||||
data: { types }
|
||||
});
|
||||
|
||||
if (res == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历返回的字典数据
|
||||
forInObject(res, (arr, key) => {
|
||||
let list: DictItem[] = [];
|
||||
|
||||
(arr as UTSJSONObject[]).forEach((e) => {
|
||||
e["label"] = e["name"];
|
||||
const d = parse<DictItem>(e);
|
||||
|
||||
if (d != null) {
|
||||
list.push(d);
|
||||
}
|
||||
});
|
||||
|
||||
const item = this.find(key);
|
||||
|
||||
// 如果不存在则新增,否则更新
|
||||
if (isNull(item)) {
|
||||
this.data.push({
|
||||
key,
|
||||
list
|
||||
});
|
||||
} else {
|
||||
item!.list = list;
|
||||
}
|
||||
});
|
||||
|
||||
// #ifdef H5
|
||||
console.log("[DICT]", this.data);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
// 单例字典对象
|
||||
export const dict = new Dict();
|
||||
17
cool-unix/cool/store/index.ts
Normal file
17
cool-unix/cool/store/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Dict, dict } from "./dict";
|
||||
import { User, user } from "./user";
|
||||
|
||||
type Store = {
|
||||
user: User;
|
||||
dict: Dict;
|
||||
};
|
||||
|
||||
export function useStore(): Store {
|
||||
return {
|
||||
user,
|
||||
dict
|
||||
};
|
||||
}
|
||||
|
||||
export * from "./dict";
|
||||
export * from "./user";
|
||||
185
cool-unix/cool/store/user.ts
Normal file
185
cool-unix/cool/store/user.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { forInObject, isNull, isObject, parse, storage } from "../utils";
|
||||
import { router } from "../router";
|
||||
import { request } from "../service";
|
||||
import type { UserInfo } from "@/types";
|
||||
|
||||
export type Token = {
|
||||
token: string; // 访问token
|
||||
expire: number; // token过期时间(秒)
|
||||
refreshToken: string; // 刷新token
|
||||
refreshExpire: number; // 刷新token过期时间(秒)
|
||||
};
|
||||
|
||||
export class User {
|
||||
/**
|
||||
* 用户信息,响应式对象
|
||||
*/
|
||||
info = ref<UserInfo | null>(null);
|
||||
|
||||
/**
|
||||
* 当前token,字符串或null
|
||||
*/
|
||||
token: string | null = null;
|
||||
|
||||
constructor() {
|
||||
// 获取本地用户信息
|
||||
const userInfo = storage.get("userInfo");
|
||||
|
||||
// 获取本地token
|
||||
const token = storage.get("token") as string | null;
|
||||
|
||||
// 如果token为空字符串则置为null
|
||||
this.token = token == "" ? null : token;
|
||||
|
||||
// 初始化用户信息
|
||||
if (userInfo != null && isObject(userInfo)) {
|
||||
this.set(userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息(从服务端拉取最新信息并更新本地)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async get() {
|
||||
if (this.token != null) {
|
||||
await request({
|
||||
url: "/app/user/info/person"
|
||||
})
|
||||
.then((res) => {
|
||||
if (res != null) {
|
||||
this.set(res);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// this.logout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息并存储到本地
|
||||
* @param data 用户信息对象
|
||||
*/
|
||||
set(data: any) {
|
||||
if (isNull(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置
|
||||
this.info.value = parse<UserInfo>(data)!;
|
||||
|
||||
// 持久化到本地存储
|
||||
storage.set("userInfo", data, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息(本地与服务端同步)
|
||||
* @param data 新的用户信息
|
||||
*/
|
||||
async update(data: any) {
|
||||
if (isNull(data) || isNull(this.info.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地同步更新
|
||||
forInObject(data, (value, key) => {
|
||||
this.info.value![key] = value;
|
||||
});
|
||||
|
||||
// 同步到服务端
|
||||
await request({
|
||||
url: "/app/user/info/updatePerson",
|
||||
method: "POST",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户信息
|
||||
*/
|
||||
remove() {
|
||||
this.info.value = null;
|
||||
storage.remove("userInfo");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户信息是否为空
|
||||
* @returns boolean
|
||||
*/
|
||||
isNull() {
|
||||
return this.info.value == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除本地所有用户信息和token
|
||||
*/
|
||||
clear() {
|
||||
storage.remove("userInfo");
|
||||
storage.remove("token");
|
||||
storage.remove("refreshToken");
|
||||
this.token = null;
|
||||
this.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录,清除所有信息并跳转到登录页
|
||||
*/
|
||||
logout() {
|
||||
this.clear();
|
||||
router.login();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置token并存储到本地
|
||||
* @param data Token对象
|
||||
*/
|
||||
setToken(data: Token) {
|
||||
this.token = data.token;
|
||||
|
||||
// 访问token,提前5秒过期,防止边界问题
|
||||
storage.set("token", data.token, data.expire - 5);
|
||||
// 刷新token,提前5秒过期
|
||||
storage.set("refreshToken", data.refreshToken, data.refreshExpire - 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token(调用服务端接口,自动更新本地token)
|
||||
* @returns Promise<string> 新的token
|
||||
*/
|
||||
refreshToken(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
url: "/app/user/login/refreshToken",
|
||||
method: "POST",
|
||||
data: {
|
||||
refreshToken: storage.get("refreshToken")
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
if (res != null) {
|
||||
const token = parse<Token>(res);
|
||||
|
||||
if (token != null) {
|
||||
this.setToken(token);
|
||||
resolve(token.token);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单例用户对象,项目全局唯一
|
||||
*/
|
||||
export const user = new User();
|
||||
|
||||
/**
|
||||
* 用户信息,响应式对象
|
||||
*/
|
||||
export const userInfo = computed(() => user.info.value);
|
||||
253
cool-unix/cool/theme/index.ts
Normal file
253
cool-unix/cool/theme/index.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { computed, ref } from "vue";
|
||||
import uniTheme from "@/theme.json";
|
||||
import { router } from "../router";
|
||||
import { ctx } from "../ctx";
|
||||
import { isNull } from "../utils";
|
||||
|
||||
// 主题类型定义,仅支持 light 和 dark
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
// 是否为自动主题模式(跟随系统)
|
||||
export const isAuto = ref(true);
|
||||
|
||||
/**
|
||||
* 获取页面样式
|
||||
* @param key 样式 key
|
||||
* @returns 样式值
|
||||
*/
|
||||
export function getStyle(key: string): string | null {
|
||||
// 页面配置
|
||||
const style = router.route()?.style;
|
||||
|
||||
// 页面配置 key 映射
|
||||
const names = {
|
||||
bgColor: "backgroundColor",
|
||||
bgContentColor: "backgroundColorContent",
|
||||
navBgColor: "navigationBarBackgroundColor",
|
||||
navTextStyle: "navigationBarTextStyle"
|
||||
};
|
||||
|
||||
// 如果页面配置存在,则使用页面配置
|
||||
if (style != null) {
|
||||
if (names[key] != null) {
|
||||
const val = style[names[key]!] as string | null;
|
||||
|
||||
if (val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取颜色
|
||||
* @param name 颜色名称
|
||||
* @returns 颜色值
|
||||
*/
|
||||
export const getColor = (name: string) => {
|
||||
if (isNull(ctx.color)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return ctx.color[name] as string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 uniapp 主题配置
|
||||
*/
|
||||
export function getConfig(key: string): string {
|
||||
// 主题配置
|
||||
const themeVal = ((isDark.value ? uniTheme.dark : uniTheme.light) as UTSJSONObject)[key] as
|
||||
| string
|
||||
| null;
|
||||
|
||||
// 页面样式
|
||||
const styleVal = getStyle(key);
|
||||
|
||||
return styleVal ?? themeVal ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
* APP 下优先获取 appTheme,若为 auto 则跟随系统 osTheme
|
||||
* H5/小程序下优先获取 hostTheme,否则默认为 light
|
||||
*/
|
||||
const getTheme = () => {
|
||||
let value: string | null;
|
||||
|
||||
// #ifdef APP
|
||||
const appInfo = uni.getAppBaseInfo();
|
||||
// @ts-ignore
|
||||
const appTheme = appInfo.appTheme as string;
|
||||
const osTheme = uni.getSystemInfoSync().osTheme!;
|
||||
|
||||
// 如果 appTheme 为 auto,则跟随系统主题,否则使用 appTheme
|
||||
value = appTheme == "auto" ? osTheme : appTheme;
|
||||
isAuto.value = appTheme == "auto";
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP
|
||||
const hostTheme = uni.getAppBaseInfo().hostTheme;
|
||||
if (hostTheme) {
|
||||
// 如果有 hostTheme,则使用 hostTheme
|
||||
value = hostTheme;
|
||||
} else {
|
||||
// 默认使用 light 主题
|
||||
value = "light";
|
||||
}
|
||||
// #endif
|
||||
|
||||
return value as Theme;
|
||||
};
|
||||
|
||||
// 当前主题响应式变量
|
||||
export const theme = ref<Theme>(getTheme());
|
||||
|
||||
/**
|
||||
* 是否为暗色模式
|
||||
*/
|
||||
export const isDark = computed(() => {
|
||||
return theme.value == "dark";
|
||||
});
|
||||
|
||||
/**
|
||||
* 切换自动主题模式(仅 APP 有效)
|
||||
*/
|
||||
export const setIsAuto = () => {
|
||||
// #ifdef APP
|
||||
isAuto.value = !isAuto.value;
|
||||
|
||||
if (isAuto.value) {
|
||||
// 设置为自动主题,跟随系统
|
||||
uni.setAppTheme({
|
||||
theme: "auto"
|
||||
});
|
||||
} else {
|
||||
// 关闭自动,使用当前 theme
|
||||
setTheme(theme.value);
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param value 主题值("light" | "dark")
|
||||
*/
|
||||
export const setTheme = (value: Theme) => {
|
||||
// 如果当前主题与目标主题一致,则不做处理
|
||||
if (theme.value == value) return;
|
||||
|
||||
// 关闭自动主题
|
||||
isAuto.value = false;
|
||||
|
||||
// #ifdef APP
|
||||
uni.setAppTheme({
|
||||
theme: value,
|
||||
success: () => {
|
||||
// 设置成功后更新 theme
|
||||
theme.value = value;
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef APP
|
||||
theme.value = value;
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
setH5();
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 设置 H5 下的主题色
|
||||
export const setH5 = () => {
|
||||
const bgContentColor = getConfig("bgContentColor");
|
||||
const tabBgColor = getConfig("tabBgColor");
|
||||
const navBgColor = getConfig("navBgColor");
|
||||
const navTextStyle = getConfig("navTextStyle");
|
||||
|
||||
document.body.style.setProperty("--background-color-content", bgContentColor);
|
||||
|
||||
const tabbar = document.querySelector(".uni-tabbar");
|
||||
if (tabbar) {
|
||||
(tabbar as HTMLElement).style.backgroundColor = tabBgColor;
|
||||
}
|
||||
|
||||
const pageHead = document.querySelector(".uni-page-head");
|
||||
if (pageHead) {
|
||||
(pageHead as HTMLElement).style.backgroundColor = navBgColor;
|
||||
(pageHead as HTMLElement).style.color = navTextStyle;
|
||||
}
|
||||
|
||||
const pageHeadBtnPath = document.querySelector(".uni-page-head-btn path");
|
||||
if (pageHeadBtnPath) {
|
||||
(pageHeadBtnPath as HTMLElement).style.fill = navTextStyle;
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "theme-change",
|
||||
isDark: isDark.value
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
export const toggleTheme = () => {
|
||||
if (isDark.value) {
|
||||
setTheme("light");
|
||||
} else {
|
||||
setTheme("dark");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化主题监听
|
||||
* APP 下监听系统主题和 App 主题变化
|
||||
* H5/小程序下监听 hostTheme 变化
|
||||
*/
|
||||
export const initTheme = () => {
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
uni.onOsThemeChange((res) => {
|
||||
if (isAuto.value) {
|
||||
setTimeout(() => {
|
||||
uni.setAppTheme({
|
||||
theme: res.osTheme,
|
||||
success: () => {
|
||||
theme.value = res.osTheme;
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 App 主题变化
|
||||
uni.onAppThemeChange((res) => {
|
||||
theme.value = res.appTheme;
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
uni.onHostThemeChange((res) => {
|
||||
setTheme(res.hostTheme);
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// 监听父窗口发送的主题变化消息
|
||||
// [BUG] uni.onHostThemeChange 打包会丢失
|
||||
// uni.onHostThemeChange((res) => {
|
||||
// setTheme(res.hostTheme);
|
||||
// });
|
||||
window.addEventListener("message", (e) => {
|
||||
if (e.data?.type == "theme-change") {
|
||||
setTheme(e.data.isDark ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
58
cool-unix/cool/types/index.ts
Normal file
58
cool-unix/cool/types/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export type PushAnimationType =
|
||||
| "auto"
|
||||
| "none"
|
||||
| "slide-in-right"
|
||||
| "slide-in-left"
|
||||
| "slide-in-top"
|
||||
| "slide-in-bottom"
|
||||
| "fade-in"
|
||||
| "zoom-out"
|
||||
| "zoom-fade-out"
|
||||
| "pop-in";
|
||||
|
||||
export type BackAnimationType =
|
||||
| "auto"
|
||||
| "none"
|
||||
| "slide-out-right"
|
||||
| "slide-out-left"
|
||||
| "slide-out-top"
|
||||
| "slide-out-bottom"
|
||||
| "fade-out"
|
||||
| "zoom-in"
|
||||
| "zoom-fade-in"
|
||||
| "pop-out";
|
||||
|
||||
export type PushMode = "navigateTo" | "redirectTo" | "reLaunch" | "switchTab";
|
||||
|
||||
export type BackOptions = {
|
||||
delta?: number;
|
||||
animationType?: BackAnimationType;
|
||||
animationDuration?: number;
|
||||
success?: (result: any) => void;
|
||||
fail?: (result: any) => void;
|
||||
complete?: (result: any) => void;
|
||||
};
|
||||
|
||||
export type PushOptions = {
|
||||
path: string;
|
||||
mode?: PushMode;
|
||||
events?: any;
|
||||
query?: UTSJSONObject;
|
||||
isAuth?: boolean;
|
||||
params?: UTSJSONObject;
|
||||
animationType?: PushAnimationType;
|
||||
animationDuration?: number;
|
||||
success?: (result: any) => void;
|
||||
fail?: (result: any) => void;
|
||||
complete?: (result: any) => void;
|
||||
};
|
||||
|
||||
export type PageInstance = {
|
||||
path: string;
|
||||
vm: any;
|
||||
style?: UTSJSONObject;
|
||||
query: UTSJSONObject;
|
||||
exposed: any;
|
||||
isCustomNavbar: boolean;
|
||||
meta?: UTSJSONObject;
|
||||
};
|
||||
254
cool-unix/cool/upload/index.ts
Normal file
254
cool-unix/cool/upload/index.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { config } from "@/config";
|
||||
import { request } from "../service";
|
||||
import { basename, extname, filename, parse, parseObject, pathJoin, uuid } from "../utils";
|
||||
import { useStore } from "../store";
|
||||
|
||||
// 上传进度回调结果类型
|
||||
export type OnProgressUpdateResult = {
|
||||
progress: number;
|
||||
totalBytesSent: number;
|
||||
totalBytesExpectedToSend: number;
|
||||
};
|
||||
|
||||
// 上传任务类型定义
|
||||
export type UploadTask = {
|
||||
abort(): void;
|
||||
};
|
||||
|
||||
// 上传选项类型定义
|
||||
export type UploadOptions = {
|
||||
onProgressUpdate?: (result: OnProgressUpdateResult) => void; // 上传进度回调
|
||||
onTask?: (task: UploadTask) => void; // 上传任务回调
|
||||
};
|
||||
|
||||
// 上传模式类型
|
||||
export type UploadMode = {
|
||||
mode: "local" | "cloud"; // 上传模式:本地或云端
|
||||
type: string; // 云服务类型
|
||||
};
|
||||
|
||||
// 上传请求的参数类型
|
||||
export type UploadRequestOptions = {
|
||||
url: string;
|
||||
preview?: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
// 云上传返回数据类型
|
||||
export type CloudUploadResponse = {
|
||||
uploadUrl?: string;
|
||||
url?: string;
|
||||
host?: string;
|
||||
credentials?: any;
|
||||
OSSAccessKeyId?: string;
|
||||
policy?: string;
|
||||
signature?: string;
|
||||
publicDomain?: string;
|
||||
token?: string;
|
||||
fields?: any;
|
||||
};
|
||||
|
||||
// 本地上传返回数据类型
|
||||
export type LocalUploadResponse = {
|
||||
code: number;
|
||||
message?: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
// 获取上传模式(本地/云端及云类型)
|
||||
async function getUploadMode(): Promise<UploadMode> {
|
||||
const res = await request({
|
||||
url: "/app/base/comm/uploadMode"
|
||||
});
|
||||
|
||||
return parse<UploadMode>(res!)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径上传
|
||||
* @param path 文件路径
|
||||
*/
|
||||
export async function upload(path: string) {
|
||||
return uploadFile({
|
||||
path,
|
||||
size: 0,
|
||||
name: "",
|
||||
type: "image/png"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @param file 文件信息 ChooseImageTempFile
|
||||
* @param options 上传选项
|
||||
*/
|
||||
export async function uploadFile(
|
||||
file: ChooseImageTempFile,
|
||||
options: UploadOptions | null = null
|
||||
): Promise<string> {
|
||||
const { user } = useStore();
|
||||
|
||||
// 获取上传模式和类型
|
||||
const { mode, type } = await getUploadMode();
|
||||
|
||||
// 判断是否本地上传
|
||||
const isLocal = mode == "local";
|
||||
|
||||
// 判断是否是云上传
|
||||
const isCloud = mode == "cloud";
|
||||
|
||||
// 获取文件路径
|
||||
const filePath = file.path;
|
||||
|
||||
// 获取文件名
|
||||
let fileName = file.name;
|
||||
|
||||
// 如果文件名不存在,则使用文件路径的文件名
|
||||
if (fileName == "" || fileName == null) {
|
||||
fileName = basename(filePath);
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
let ext = extname(fileName);
|
||||
if (ext == "") {
|
||||
ext = "png";
|
||||
}
|
||||
|
||||
// 生成唯一key: 原文件名_uuid.扩展名
|
||||
let key = `${filename(fileName)}_${uuid()}.${ext}`;
|
||||
|
||||
// 云上传需要加上时间戳路径
|
||||
if (isCloud) {
|
||||
key = `app/${Date.now()}/${key}`;
|
||||
}
|
||||
|
||||
// 支持多种上传方式
|
||||
return new Promise((resolve, reject) => {
|
||||
/**
|
||||
* 实际上传文件的函数
|
||||
* @param param0 上传参数
|
||||
*/
|
||||
function next({ url, preview, data }: UploadRequestOptions) {
|
||||
// 发起上传请求
|
||||
const task = uni.uploadFile({
|
||||
url,
|
||||
filePath,
|
||||
name: "file",
|
||||
header: {
|
||||
// 本地上传带token
|
||||
Authorization: isLocal ? user.token : null
|
||||
},
|
||||
formData: {
|
||||
...(data as UTSJSONObject),
|
||||
key
|
||||
},
|
||||
success(res) {
|
||||
if (isLocal) {
|
||||
// 本地上传返回处理
|
||||
const { code, data, message } = parseObject<LocalUploadResponse>(res.data)!;
|
||||
|
||||
if (code == 1000) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(message);
|
||||
}
|
||||
} else {
|
||||
// 云上传直接拼接url
|
||||
resolve(pathJoin(preview ?? url, key!));
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传任务回调
|
||||
if (options?.onTask != null) {
|
||||
options.onTask!({
|
||||
abort: () => {
|
||||
task.abort();
|
||||
}
|
||||
} as UploadTask);
|
||||
}
|
||||
|
||||
// 上传进度回调
|
||||
if (options?.onProgressUpdate != null) {
|
||||
task.onProgressUpdate((result) => {
|
||||
const { progress, totalBytesSent, totalBytesExpectedToSend } = result;
|
||||
|
||||
options.onProgressUpdate!({
|
||||
progress,
|
||||
totalBytesSent,
|
||||
totalBytesExpectedToSend
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 本地上传
|
||||
if (isLocal) {
|
||||
next({
|
||||
url: config.baseUrl + "/app/base/comm/upload",
|
||||
data: {}
|
||||
});
|
||||
} else {
|
||||
// 云上传
|
||||
const data = {} as UTSJSONObject;
|
||||
|
||||
// AWS需要提前传key
|
||||
if (type == "aws") {
|
||||
data.key = key;
|
||||
}
|
||||
|
||||
// 获取云上传参数
|
||||
request({
|
||||
url: "/app/base/comm/upload",
|
||||
method: "POST",
|
||||
data
|
||||
})
|
||||
.then((res) => {
|
||||
const d = parse<CloudUploadResponse>(res!)!;
|
||||
|
||||
switch (type) {
|
||||
// 腾讯云COS
|
||||
case "cos":
|
||||
next({
|
||||
url: d.url!,
|
||||
data: d.credentials!
|
||||
});
|
||||
break;
|
||||
// 阿里云OSS
|
||||
case "oss":
|
||||
next({
|
||||
url: d.host!,
|
||||
data: {
|
||||
OSSAccessKeyId: d.OSSAccessKeyId,
|
||||
policy: d.policy,
|
||||
signature: d.signature
|
||||
}
|
||||
});
|
||||
break;
|
||||
// 七牛云
|
||||
case "qiniu":
|
||||
next({
|
||||
url: d.uploadUrl!,
|
||||
preview: d.publicDomain,
|
||||
data: {
|
||||
token: d.token
|
||||
}
|
||||
});
|
||||
break;
|
||||
// 亚马逊AWS
|
||||
case "aws":
|
||||
next({
|
||||
url: d.url!,
|
||||
data: d.fields!
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
612
cool-unix/cool/utils/comm.ts
Normal file
612
cool-unix/cool/utils/comm.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* 检查值是否为数组
|
||||
* @example isArray([1, 2, 3]) // true
|
||||
* @example isArray('123') // false
|
||||
*/
|
||||
export function isArray(value: any): boolean {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为对象
|
||||
* @example isObject({}) // true
|
||||
* @example isObject([]) // false
|
||||
*/
|
||||
export function isObject(value: any): boolean {
|
||||
return typeof value == "object" && !Array.isArray(value) && !isNull(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为字符串
|
||||
* @example isString('abc') // true
|
||||
* @example isString(123) // false
|
||||
*/
|
||||
export function isString(value: any): boolean {
|
||||
return typeof value == "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为数字
|
||||
* @example isNumber(123) // true
|
||||
* @example isNumber('123') // false
|
||||
*/
|
||||
export function isNumber(value: any): boolean {
|
||||
return typeof value == "number" && !isNaN(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为布尔值
|
||||
* @example isBoolean(true) // true
|
||||
* @example isBoolean(1) // false
|
||||
*/
|
||||
export function isBoolean(value: any): boolean {
|
||||
return typeof value == "boolean";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为函数
|
||||
* @example isFunction(() => {}) // true
|
||||
* @example isFunction({}) // false
|
||||
*/
|
||||
export function isFunction(value: any): boolean {
|
||||
return typeof value == "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为 null
|
||||
* @example isNull(null) // true
|
||||
* @example isNull(undefined) // true
|
||||
*/
|
||||
export function isNull(value?: any | null): boolean {
|
||||
// #ifdef APP
|
||||
return value == null;
|
||||
// #endif
|
||||
|
||||
// #ifndef APP
|
||||
return value == null || value == undefined;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为空
|
||||
* @example isEmpty([]) // true
|
||||
* @example isEmpty('') // true
|
||||
* @example isEmpty({}) // true
|
||||
*/
|
||||
export function isEmpty(value: any): boolean {
|
||||
if (isArray(value)) {
|
||||
return (value as any[]).length == 0;
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
return value == "";
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
return keys(value).length == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回对象的所有键名
|
||||
* @example keys({a: 1, b: 2}) // ['a', 'b']
|
||||
*/
|
||||
export function keys(value: any): string[] {
|
||||
// @ts-ignore
|
||||
return UTSJSONObject.keys(value as UTSJSONObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回数组的第一个元素
|
||||
* @example first([1, 2, 3]) // 1
|
||||
* @example first([]) // null
|
||||
*/
|
||||
export function first<T>(array: T[]): T | null {
|
||||
return isArray(array) && array.length > 0 ? array[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回数组的最后一个元素
|
||||
* @example last([1, 2, 3]) // 3
|
||||
* @example last([]) // null
|
||||
*/
|
||||
export function last<T>(array: T[]): T | null {
|
||||
return isArray(array) && array.length > 0 ? array[array.length - 1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取数组的一部分
|
||||
* @example slice([1, 2, 3], 1) // [2, 3]
|
||||
* @example slice([1, 2, 3], 1, 2) // [2]
|
||||
*/
|
||||
export function slice<T>(array: T[], start: number = 0, end: number = array.length): T[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = start; i < end && i < array.length; i++) {
|
||||
result.push(array[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否包含指定属性
|
||||
* @example has({a: 1}, 'a') // true
|
||||
* @example has({a: 1}, 'b') // false
|
||||
*/
|
||||
export function has(object: any, key: string): boolean {
|
||||
return keys(object).includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的属性值
|
||||
* @example get({a: {b: 1}}, 'a.b') // 1
|
||||
* @example get({a: {b: 1}}, 'a.c', 'default') // 'default'
|
||||
*/
|
||||
export function get(object: any, path: string, defaultValue: any | null = null): any | null {
|
||||
if (isNull(object)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const value = new UTSJSONObject(object).getAny(path);
|
||||
|
||||
if (isNull(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对象的属性值
|
||||
* @example set({a: 1}, 'b', 2) // {a: 1, b: 2}
|
||||
*/
|
||||
export function set(object: any, key: string, value: any | null): void {
|
||||
(object as UTSJSONObject)[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历数组并返回新数组
|
||||
* @example map([1, 2, 3], x => x * 2) // [2, 4, 6]
|
||||
*/
|
||||
export function map<T, U>(array: T[], iteratee: (item: T, index: number) => U): U[] {
|
||||
const result: U[] = [];
|
||||
|
||||
if (!isArray(array)) return result;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result.push(iteratee(array[i], i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组归约为单个值
|
||||
* @example reduce([1, 2, 3], (sum, n) => sum + n, 0) // 6
|
||||
*/
|
||||
export function reduce<T, U>(
|
||||
array: T[],
|
||||
iteratee: (accumulator: U, value: T, index: number) => U,
|
||||
initialValue: U
|
||||
): U {
|
||||
if (!isArray(array)) return initialValue;
|
||||
|
||||
let accumulator: U = initialValue;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
accumulator = iteratee(accumulator, array[i], i);
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数组中的所有元素是否都满足条件
|
||||
* @example every([2, 4, 6], x => x % 2 == 0) // true
|
||||
*/
|
||||
export function every<T>(array: T[], predicate: (value: T, index: number) => boolean): boolean {
|
||||
if (!isArray(array)) return true;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (!predicate(array[i], i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数组中是否有元素满足条件
|
||||
* @example some([1, 2, 3], x => x > 2) // true
|
||||
*/
|
||||
export function some<T>(array: T[], predicate: (value: T, index: number) => boolean): boolean {
|
||||
if (!isArray(array)) return false;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (predicate(array[i], i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建去重后的数组
|
||||
* @example uniq([1, 2, 2, 3]) // [1, 2, 3]
|
||||
*/
|
||||
export function uniq<T>(array: T[]): T[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result: T[] = [];
|
||||
const seen = new Map<T, boolean>();
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
const key = item;
|
||||
if (!seen.has(item)) {
|
||||
seen.set(key, true);
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组扁平化一层
|
||||
* @example flatten([1, [2, 3], 4]) // [1, 2, 3, 4]
|
||||
*/
|
||||
export function flatten(array: any[]): any[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
if (isArray(item)) {
|
||||
result.push(...(item as any[]));
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组完全扁平化
|
||||
* @example flattenDeep([1, [2, [3, [4]]]]) // [1, 2, 3, 4]
|
||||
*/
|
||||
export function flattenDeep(array: any[]): any[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
if (isArray(item)) {
|
||||
result.push(...flattenDeep(item as any[]));
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对数组进行排序
|
||||
* @example sort([3, 1, 2]) // [1, 2, 3]
|
||||
* @example sort(['c', 'a', 'b'], 'desc') // ['c', 'b', 'a']
|
||||
*/
|
||||
export function sort<T>(array: T[], order: "asc" | "desc" = "asc"): T[] {
|
||||
const result = [...array];
|
||||
|
||||
return result.sort((a, b) => {
|
||||
if (typeof a == "number" && typeof b == "number") {
|
||||
return order == "asc" ? a - b : b - a;
|
||||
}
|
||||
|
||||
if (typeof a == "string" && typeof b == "string") {
|
||||
return order == "asc" ? a.localeCompare(b) : b.localeCompare(a);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对象属性对数组进行排序
|
||||
* @example orderBy([{age: 30}, {age: 20}], 'age') // [{age: 20}, {age: 30}]
|
||||
*/
|
||||
export function orderBy<T>(array: T[], key: string, order: "asc" | "desc" = "asc"): T[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result = [...array];
|
||||
|
||||
result.sort((a, b) => {
|
||||
const valueA = get(a as any, key) as number;
|
||||
const valueB = get(b as any, key) as number;
|
||||
|
||||
if (order == "asc") {
|
||||
return valueA > valueB ? 1 : -1;
|
||||
} else {
|
||||
return valueA < valueB ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据对象属性对数组进行分组
|
||||
* @example groupBy([{type: 'a'}, {type: 'b'}, {type: 'a'}], 'type') // {a: [{type: 'a'}, {type: 'a'}], b: [{type: 'b'}]}
|
||||
*/
|
||||
export function groupBy<T>(array: T[], key: string) {
|
||||
if (!isArray(array)) return {};
|
||||
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
let value = get(item as any, key)!;
|
||||
|
||||
if (typeof value == "number") {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
if (typeof value == "string") {
|
||||
if (!isArray(result[value])) {
|
||||
result[value] = new Array<T>();
|
||||
}
|
||||
|
||||
(result[value] as T[]).push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多个对象的属性合并到一个对象中
|
||||
* @example assign({a: 1}, {b: 2}) // {a: 1, b: 2}
|
||||
*/
|
||||
export function assign(...items: any[]) {
|
||||
// @ts-ignore
|
||||
return UTSJSONObject.assign(...items.map((item) => item as UTSJSONObject));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组中指定索引的元素
|
||||
* @example nth([1, 2, 3], 1) // 2
|
||||
* @example nth([1, 2, 3], -1) // 3
|
||||
*/
|
||||
export function nth<T>(array: T[], index: number): T | null {
|
||||
if (index >= 0) {
|
||||
return array[index];
|
||||
}
|
||||
|
||||
return array[array.length + index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中移除指定的值
|
||||
* @example pull([1, 2, 3, 1, 2, 3], 1, 2) // [3, 3]
|
||||
*/
|
||||
export function pull<T>(array: T[], ...values: T[]): T[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
return array.filter((item) => !values.includes(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中移除满足条件的元素
|
||||
* @example remove([1, 2, 3, 4], x => x % 2 == 0) // [1, 3]
|
||||
*/
|
||||
export function remove<T>(array: T[], predicate: (value: T, index: number) => boolean): T[] {
|
||||
if (!isArray(array)) return [];
|
||||
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (!predicate(array[i], i)) {
|
||||
result.push(array[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历数组
|
||||
* @example forEach([1, 2, 3], x => console.log(x))
|
||||
*/
|
||||
export function forEach<T>(data: T[], iteratee: (value: T, index: number) => void): void {
|
||||
if (isArray(data)) {
|
||||
const array = data as T[];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] != null) {
|
||||
iteratee(array[i], i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找数组中第一个满足条件的元素
|
||||
* @example find([1, 2, 3, 4], x => x > 2) // 3
|
||||
*/
|
||||
export function find<T>(array: T[], predicate: (value: T, index: number) => boolean): T | null {
|
||||
if (!isArray(array)) return null;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (predicate(array[i], i)) {
|
||||
return array[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历对象
|
||||
* @example forInObject({a: 1, b: 2}, (value, key) => console.log(key, value))
|
||||
*/
|
||||
export function forInObject(data: any, iteratee: (value: any, key: string) => void): void {
|
||||
if (isObject(data)) {
|
||||
const objKeys = keys(data);
|
||||
for (let i = 0; i < objKeys.length; i++) {
|
||||
const key = objKeys[i];
|
||||
iteratee(get(data, key)!, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转数组
|
||||
* @example toArray({a: 1, b: 2}, (value, key) => ({key, value})) // [{key: 'a', value: 1}, {key: 'b', value: 2}]
|
||||
*/
|
||||
export function toArray<T>(data: any, iteratee: (value: any, key: string) => T): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
if (isObject(data)) {
|
||||
forInObject(data, (value, key) => {
|
||||
result.push(iteratee(value, key));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
* @example uuid() // "123e4567-e89b-12d3-a456-426614174000"
|
||||
*/
|
||||
export function uuid(): string {
|
||||
let uuid = "";
|
||||
let i: number;
|
||||
let random: number;
|
||||
|
||||
for (i = 0; i < 36; i++) {
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
||||
uuid += "-";
|
||||
} else if (i == 14) {
|
||||
uuid += "4";
|
||||
} else if (i == 19) {
|
||||
random = (Math.random() * 16) | 0;
|
||||
uuid += ((random & 0x3) | 0x8).toString(16);
|
||||
} else {
|
||||
random = (Math.random() * 16) | 0;
|
||||
uuid += random.toString(16);
|
||||
}
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个防抖函数,在指定延迟后执行函数,如果在延迟期间再次调用则重新计时
|
||||
* @example debounce(() => console.log('执行'), 300)
|
||||
*/
|
||||
export function debounce(func: () => void, delay: number): () => number {
|
||||
let timeoutId = 0;
|
||||
|
||||
return function (): number {
|
||||
// 清除之前的定时器
|
||||
if (timeoutId != 0) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
// @ts-ignore
|
||||
timeoutId = setTimeout(() => {
|
||||
func();
|
||||
timeoutId = 0;
|
||||
}, delay);
|
||||
|
||||
return timeoutId;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个节流函数,在指定时间间隔内只会执行一次
|
||||
* @example
|
||||
* const throttled = throttle(() => console.log('执行'), 300)
|
||||
* throttled()
|
||||
*/
|
||||
export function throttle(func: () => void, delay: number): () => number {
|
||||
let timeoutId: number = 0;
|
||||
let lastExec: number = 0;
|
||||
|
||||
return function (): number {
|
||||
const now: number = Date.now();
|
||||
|
||||
// 如果距离上次执行已超过delay,则立即执行
|
||||
if (now - lastExec >= delay) {
|
||||
func();
|
||||
lastExec = now;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 否则在剩余时间后执行
|
||||
if (timeoutId != 0) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const remaining: number = delay - (now - lastExec);
|
||||
// @ts-ignore
|
||||
timeoutId = setTimeout(() => {
|
||||
func();
|
||||
lastExec = Date.now();
|
||||
timeoutId = 0;
|
||||
}, remaining);
|
||||
|
||||
return timeoutId;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定范围内的随机数
|
||||
* @example random(1, 10) // 随机生成1到10之间的整数
|
||||
*/
|
||||
export function random(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将base64转换为blob
|
||||
* @param data base64数据
|
||||
* @returns blob数据
|
||||
*/
|
||||
export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
|
||||
// #ifdef H5
|
||||
let bytes = window.atob(data.split(",")[1]);
|
||||
let ab = new ArrayBuffer(bytes.length);
|
||||
let ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
ia[i] = bytes.charCodeAt(i);
|
||||
}
|
||||
return new Blob([ab], { type });
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个值是否相等
|
||||
* @param a 值1
|
||||
* @param b 值2
|
||||
* @returns 是否相等
|
||||
*/
|
||||
export function isEqual(a: any, b: any): boolean {
|
||||
if (isObject(a) && isObject(b)) {
|
||||
return isEqual(JSON.stringify(a), JSON.stringify(b));
|
||||
} else if (isArray(a) && isArray(b)) {
|
||||
return isEqual(JSON.stringify(a), JSON.stringify(b));
|
||||
}
|
||||
|
||||
return a == b;
|
||||
}
|
||||
324
cool-unix/cool/utils/day.ts
Normal file
324
cool-unix/cool/utils/day.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 轻量级日期工具类
|
||||
* 基于 uni-app-x UTS Date API
|
||||
* 参考 dayjs 设计理念
|
||||
*/
|
||||
|
||||
export class DayUts {
|
||||
private _date: Date;
|
||||
|
||||
constructor(date?: Date | string | number | null) {
|
||||
if (date == null || date == "") {
|
||||
this._date = new Date();
|
||||
} else if (typeof date == "string") {
|
||||
this._date = new Date(date);
|
||||
} else if (typeof date == "number") {
|
||||
this._date = new Date(date);
|
||||
} else if (date instanceof Date) {
|
||||
this._date = new Date(date.getTime());
|
||||
} else {
|
||||
this._date = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param template 格式模板,支持 YYYY-MM-DD HH:mm:ss 等
|
||||
*/
|
||||
format(template: string): string {
|
||||
// 使用传入的模板
|
||||
let actualTemplate: string = template;
|
||||
|
||||
const year = this._date.getFullYear();
|
||||
const month = this._date.getMonth() + 1;
|
||||
const date = this._date.getDate();
|
||||
const hours = this._date.getHours();
|
||||
const minutes = this._date.getMinutes();
|
||||
const seconds = this._date.getSeconds();
|
||||
const milliseconds = this._date.getMilliseconds();
|
||||
|
||||
// 使用数组来依次替换,避免UTS的replace方法兼容性问题
|
||||
let result: string = actualTemplate;
|
||||
|
||||
// 按照长度从长到短替换,避免冲突
|
||||
// 替换年份 (YYYY 必须在 YY 之前)
|
||||
result = result.replace("YYYY", year.toString());
|
||||
result = result.replace("YY", year.toString().slice(-2));
|
||||
|
||||
// 替换月份 (MM 必须在 M 之前)
|
||||
result = result.replace("MM", month.toString().padStart(2, "0"));
|
||||
result = result.replace("M", month.toString());
|
||||
|
||||
// 替换日期 (DD 必须在 D 之前)
|
||||
result = result.replace("DD", date.toString().padStart(2, "0"));
|
||||
result = result.replace("D", date.toString());
|
||||
|
||||
// 替换小时 (HH 必须在 H 之前)
|
||||
result = result.replace("HH", hours.toString().padStart(2, "0"));
|
||||
result = result.replace("H", hours.toString());
|
||||
|
||||
// 替换分钟 (mm 必须在 m 之前)
|
||||
result = result.replace("mm", minutes.toString().padStart(2, "0"));
|
||||
result = result.replace("m", minutes.toString());
|
||||
|
||||
// 替换秒数 (ss 必须在 s 之前)
|
||||
result = result.replace("ss", seconds.toString().padStart(2, "0"));
|
||||
result = result.replace("s", seconds.toString());
|
||||
|
||||
// 替换毫秒
|
||||
result = result.replace("SSS", milliseconds.toString().padStart(3, "0"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本月多少天
|
||||
*/
|
||||
getDays(): number {
|
||||
return new Date(this._date.getFullYear(), this._date.getMonth() + 1, 0).getDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为闰年
|
||||
*/
|
||||
isLeapYear(): boolean {
|
||||
return this._date.getFullYear() % 4 == 0 && this._date.getFullYear() % 100 != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 星期几
|
||||
*/
|
||||
getDay(): number {
|
||||
return this._date.getDay();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个单位的开始时间
|
||||
*/
|
||||
startOf(unit: "month" | "day" | "year" | "week"): DayUts {
|
||||
const newDate = new Date(this._date.getTime());
|
||||
|
||||
switch (unit) {
|
||||
case "year":
|
||||
newDate.setMonth(0);
|
||||
newDate.setDate(1);
|
||||
newDate.setHours(0);
|
||||
newDate.setMinutes(0);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
break;
|
||||
case "month":
|
||||
newDate.setDate(1);
|
||||
newDate.setHours(0);
|
||||
newDate.setMinutes(0);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
break;
|
||||
case "week":
|
||||
newDate.setDate(newDate.getDate() - newDate.getDay());
|
||||
newDate.setHours(0);
|
||||
newDate.setMinutes(0);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
break;
|
||||
case "day":
|
||||
newDate.setHours(0);
|
||||
newDate.setMinutes(0);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
break;
|
||||
}
|
||||
|
||||
return new DayUts(newDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个单位的结束时间
|
||||
*/
|
||||
endOf(unit: "month" | "day" | "year" | "week"): DayUts {
|
||||
const newDate = new Date(this._date.getTime());
|
||||
|
||||
switch (unit) {
|
||||
case "year":
|
||||
newDate.setMonth(11);
|
||||
newDate.setDate(31);
|
||||
newDate.setHours(23);
|
||||
newDate.setMinutes(59);
|
||||
newDate.setSeconds(59);
|
||||
newDate.setMilliseconds(999);
|
||||
break;
|
||||
case "month":
|
||||
newDate.setMonth(newDate.getMonth() + 1);
|
||||
newDate.setDate(0);
|
||||
newDate.setHours(23);
|
||||
newDate.setMinutes(59);
|
||||
newDate.setSeconds(59);
|
||||
newDate.setMilliseconds(999);
|
||||
break;
|
||||
case "week":
|
||||
const day = newDate.getDay();
|
||||
const diff = 6 - day;
|
||||
newDate.setDate(newDate.getDate() + diff);
|
||||
newDate.setHours(23);
|
||||
newDate.setMinutes(59);
|
||||
newDate.setSeconds(59);
|
||||
newDate.setMilliseconds(999);
|
||||
break;
|
||||
case "day":
|
||||
newDate.setHours(23);
|
||||
newDate.setMinutes(59);
|
||||
newDate.setSeconds(59);
|
||||
newDate.setMilliseconds(999);
|
||||
break;
|
||||
}
|
||||
|
||||
return new DayUts(newDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否早于另一个日期
|
||||
*/
|
||||
isBefore(date: DayUts | Date | string | number): boolean {
|
||||
const compareDate = this._parseDate(date);
|
||||
return this._date.getTime() < compareDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否晚于另一个日期
|
||||
*/
|
||||
isAfter(date: DayUts | Date | string | number): boolean {
|
||||
const compareDate = this._parseDate(date);
|
||||
return this._date.getTime() > compareDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否与另一个日期相同
|
||||
*/
|
||||
isSame(date: DayUts | Date | string | number): boolean {
|
||||
const compareDate = this._parseDate(date);
|
||||
return this._date.getTime() == compareDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算与另一个日期的差值(毫秒)
|
||||
*/
|
||||
diff(date: DayUts | Date | string | number): number {
|
||||
const compareDate = this._parseDate(date);
|
||||
return this._date.getTime() - compareDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算与另一个日期的差值(指定单位)
|
||||
*/
|
||||
diffUnit(
|
||||
date: DayUts | Date | string | number,
|
||||
unit: "day" | "hour" | "minute" | "second" | "millisecond"
|
||||
): number {
|
||||
const compareDate = this._parseDate(date);
|
||||
const diffMs = this._date.getTime() - compareDate.getTime();
|
||||
|
||||
switch (unit) {
|
||||
case "day":
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
case "hour":
|
||||
return Math.floor(diffMs / (1000 * 60 * 60));
|
||||
case "minute":
|
||||
return Math.floor(diffMs / (1000 * 60));
|
||||
case "second":
|
||||
return Math.floor(diffMs / 1000);
|
||||
case "millisecond":
|
||||
default:
|
||||
return diffMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
add(value: number, unit: "day" | "month" | "year" | "hour" | "minute" | "second"): DayUts {
|
||||
const newDate = new Date(this._date.getTime());
|
||||
|
||||
switch (unit) {
|
||||
case "year":
|
||||
newDate.setFullYear(newDate.getFullYear() + value);
|
||||
break;
|
||||
case "month":
|
||||
newDate.setMonth(newDate.getMonth() + value);
|
||||
break;
|
||||
case "day":
|
||||
newDate.setDate(newDate.getDate() + value);
|
||||
break;
|
||||
case "hour":
|
||||
newDate.setHours(newDate.getHours() + value);
|
||||
break;
|
||||
case "minute":
|
||||
newDate.setMinutes(newDate.getMinutes() + value);
|
||||
break;
|
||||
case "second":
|
||||
newDate.setSeconds(newDate.getSeconds() + value);
|
||||
break;
|
||||
}
|
||||
|
||||
return new DayUts(newDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少时间
|
||||
*/
|
||||
subtract(value: number, unit: "day" | "month" | "year" | "hour" | "minute" | "second"): DayUts {
|
||||
return this.add(-value, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间戳
|
||||
*/
|
||||
valueOf(): number {
|
||||
return this._date.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原生Date对象
|
||||
*/
|
||||
toDate(): Date {
|
||||
return new Date(this._date.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期数组
|
||||
*/
|
||||
toArray(): number[] {
|
||||
return [
|
||||
this._date.getFullYear(),
|
||||
this._date.getMonth() + 1,
|
||||
this._date.getDate(),
|
||||
this._date.getHours(),
|
||||
this._date.getMinutes(),
|
||||
this._date.getSeconds()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有方法:解析不同类型的日期参数
|
||||
*/
|
||||
private _parseDate(date: DayUts | Date | string | number): Date {
|
||||
if (date instanceof DayUts) {
|
||||
return date.toDate();
|
||||
} else if (date instanceof Date) {
|
||||
return date;
|
||||
} else if (typeof date == "string") {
|
||||
return new Date(date);
|
||||
} else if (typeof date == "number") {
|
||||
return new Date(date);
|
||||
} else {
|
||||
// 如果都不匹配,返回当前时间
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 DayUts 实例
|
||||
*/
|
||||
export function dayUts(date: Date | string | number | null = new Date()): DayUts {
|
||||
return new DayUts(date);
|
||||
}
|
||||
84
cool-unix/cool/utils/device.ts
Normal file
84
cool-unix/cool/utils/device.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 检查是否为小程序环境
|
||||
* @returns 是否为小程序环境
|
||||
*/
|
||||
export const isMp = (): boolean => {
|
||||
// #ifdef MP
|
||||
return true;
|
||||
// #endif
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为App环境
|
||||
* @returns 是否为App环境
|
||||
*/
|
||||
export const isApp = (): boolean => {
|
||||
// #ifdef APP
|
||||
return true;
|
||||
// #endif
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为App-IOS环境
|
||||
* @returns 是否为App-IOS环境
|
||||
*/
|
||||
export const isAppIOS = (): boolean => {
|
||||
// #ifdef APP-IOS
|
||||
return true;
|
||||
// #endif
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为App-Android环境
|
||||
* @returns 是否为App-Android环境
|
||||
*/
|
||||
export const isAppAndroid = (): boolean => {
|
||||
// #ifdef APP-ANDROID
|
||||
return true;
|
||||
// #endif
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为H5环境
|
||||
* @returns 是否为H5环境
|
||||
*/
|
||||
export const isH5 = (): boolean => {
|
||||
// #ifdef H5
|
||||
return true;
|
||||
// #endif
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为鸿蒙环境
|
||||
* @returns 是否为鸿蒙环境
|
||||
*/
|
||||
export const isHarmony = (): boolean => {
|
||||
// #ifdef APP-HARMONY
|
||||
return true;
|
||||
// #endif
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取设备像素比
|
||||
* @returns 设备像素比
|
||||
*/
|
||||
export const getDevicePixelRatio = (): number => {
|
||||
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
||||
|
||||
// #ifdef MP
|
||||
// 微信小程序高清处理
|
||||
return 3;
|
||||
// #endif
|
||||
|
||||
return dpr;
|
||||
};
|
||||
74
cool-unix/cool/utils/file.ts
Normal file
74
cool-unix/cool/utils/file.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { base64ToBlob, uuid } from "./comm";
|
||||
|
||||
/**
|
||||
* 将canvas转换为png图片
|
||||
* @param options 转换参数
|
||||
* @returns 图片路径
|
||||
*/
|
||||
export function canvasToPng(canvasRef: UniElement): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef APP
|
||||
canvasRef.parentElement!.takeSnapshot({
|
||||
success(res) {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail(err) {
|
||||
console.error(err);
|
||||
resolve("");
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
const url = URL.createObjectURL(
|
||||
base64ToBlob(
|
||||
(canvasRef as unknown as HTMLCanvasElement)?.toDataURL("image/png", 1) ?? ""
|
||||
)
|
||||
);
|
||||
|
||||
resolve(url);
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
uni.createCanvasContextAsync({
|
||||
id: canvasRef.id,
|
||||
component: canvasRef.$vm,
|
||||
success(context) {
|
||||
// 获取2D绘图上下文
|
||||
const ctx = context.getContext("2d")!;
|
||||
|
||||
// 获取canvas对象
|
||||
const canvas = ctx.canvas;
|
||||
|
||||
// 将canvas转换为base64格式的PNG图片数据
|
||||
const data = canvas.toDataURL("image/png", 1);
|
||||
|
||||
// 获取文件系统管理器
|
||||
const fileMg = uni.getFileSystemManager();
|
||||
|
||||
// 生成临时文件路径
|
||||
// @ts-ignore
|
||||
const filepath = `${wx.env.USER_DATA_PATH}/${uuid()}.png`;
|
||||
|
||||
// 将base64数据写入文件
|
||||
fileMg.writeFile({
|
||||
filePath: filepath,
|
||||
data: data.split(",")[1],
|
||||
encoding: "base64",
|
||||
success() {
|
||||
resolve(filepath);
|
||||
},
|
||||
fail(err) {
|
||||
console.error(err);
|
||||
resolve("");
|
||||
}
|
||||
});
|
||||
},
|
||||
fail(err) {
|
||||
console.error(err);
|
||||
resolve("");
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
9
cool-unix/cool/utils/index.ts
Normal file
9
cool-unix/cool/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./tailwind";
|
||||
export * from "./comm";
|
||||
export * from "./day";
|
||||
export * from "./device";
|
||||
export * from "./file";
|
||||
export * from "./parse";
|
||||
export * from "./path";
|
||||
export * from "./rect";
|
||||
export * from "./storage";
|
||||
244
cool-unix/cool/utils/parse.ts
Normal file
244
cool-unix/cool/utils/parse.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { forEach, forInObject, isArray, isObject, isString } from "./comm";
|
||||
|
||||
/**
|
||||
* 解析数据
|
||||
* @example parse<Response>(res.data)
|
||||
*/
|
||||
export function parse<T>(data: any): T | null {
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
return (data as UTSJSONObject).parse<T>();
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
return data as T;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JSON对象
|
||||
* @param data 要解析的数据
|
||||
* @returns 解析后的JSON对象
|
||||
*/
|
||||
export function parseObject<T>(data: string): T | null {
|
||||
// #ifdef APP-ANDROID
|
||||
return JSON.parseObject<T>(data);
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
return JSON.parse(data) as T;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析透传样式对象
|
||||
* @param data 要解析的数据
|
||||
* @returns 解析后的透传样式对象
|
||||
* @template T 透传样式对象的类型
|
||||
*/
|
||||
export function parsePt<T>(data: any): T {
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
return (data as UTSJSONObject).parse<T>() ?? ({} as T);
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
return data as T;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析对象为类名字符串
|
||||
* @param obj 要解析的对象,key为类名,value为布尔值表示是否启用该类名
|
||||
* @returns 解析后的类名字符串,多个类名以空格分隔
|
||||
* @example
|
||||
* parseClass({ 'active': true, 'disabled': false }) // 返回 'active'
|
||||
* parseClass(['ml-2', 'mr-2']) // 返回 'ml-2 mr-2'
|
||||
* parseClass([{ 'mr-2': true, 'mt-2': false }]) // 返回 'mr-2'
|
||||
* parseClass([[true, 'mr-2 pt-2', 'mb-2']]) // 返回 'mr-2 pt-2'
|
||||
*/
|
||||
export const parseClass = (data: any): string => {
|
||||
// 存储启用的类名
|
||||
const names: string[] = [];
|
||||
|
||||
// 解析数据
|
||||
function deep(d: any) {
|
||||
// 如果obj是数组,则将数组中的每个元素添加到names中
|
||||
if (isArray(d)) {
|
||||
forEach(d as any[], (value: any) => {
|
||||
if (isString(value)) {
|
||||
// @example 2
|
||||
names.push(value as string);
|
||||
} else if (isArray(value)) {
|
||||
// @example 4
|
||||
const [a, b] = value as any[];
|
||||
if (a as boolean) {
|
||||
names.push(b as string);
|
||||
} else {
|
||||
if (value.length > 2) {
|
||||
names.push(value[2] as string);
|
||||
}
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
// @example 3
|
||||
deep(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 遍历对象的每个属性
|
||||
if (isObject(d)) {
|
||||
// @example 1
|
||||
forInObject(d, (value, key) => {
|
||||
// 如果属性值为true,则将类名添加到数组中
|
||||
if (value == true && key != "") {
|
||||
names.push(key.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deep(data);
|
||||
|
||||
// 将类名数组用空格连接成字符串返回
|
||||
return names.join(" ");
|
||||
};
|
||||
|
||||
/**
|
||||
* 将自定义类型数据转换为UTSJSONObject对象
|
||||
* @param data 要转换的数据
|
||||
* @returns 转换后的UTSJSONObject对象
|
||||
*/
|
||||
export function parseToObject<T>(data: T): UTSJSONObject {
|
||||
// #ifdef APP-ANDROID
|
||||
return JSON.parseObject(JSON.stringify(data ?? {})!)!;
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
return JSON.parse(JSON.stringify(data || {})) as UTSJSONObject;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 将rpx单位转换为px单位
|
||||
* @param rpx 要转换的rpx值
|
||||
* @returns 转换后的px值
|
||||
* @example
|
||||
*/
|
||||
export const rpx2px = (rpx: number): number => {
|
||||
let px: number;
|
||||
|
||||
// #ifdef MP
|
||||
px = rpx / (750 / uni.getWindowInfo().windowWidth);
|
||||
// #endif
|
||||
|
||||
// #ifndef MP
|
||||
px = uni.rpx2px(rpx);
|
||||
// #endif
|
||||
|
||||
return px;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将px单位转换为rpx单位
|
||||
* @param px 要转换的px值
|
||||
* @returns 转换后的rpx值
|
||||
* @example
|
||||
*/
|
||||
export const px2rpx = (px: number): number => {
|
||||
return px / rpx2px(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数值或字符串转换为rpx单位的字符串
|
||||
* @param val 要转换的值,可以是数字或字符串
|
||||
* @returns 转换后的rpx单位字符串
|
||||
* @example
|
||||
* parseRpx(10) // 返回 '10rpx'
|
||||
* parseRpx('10rpx') // 返回 '10rpx'
|
||||
* parseRpx('10px') // 返回 '10px'
|
||||
*/
|
||||
export const parseRpx = (val: number | string): string => {
|
||||
if (typeof val == "number") {
|
||||
return val + "rpx";
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例: 获取数值部分
|
||||
* @example
|
||||
* getNum("10rpx") // 返回 10
|
||||
* getNum("10px") // 返回 10
|
||||
* getNum("10") // 返回 10
|
||||
* getNum("-5.5px") // 返回 -5.5
|
||||
* @param val - 输入值,例如 "10rpx"、"10px"、"10"
|
||||
* @returns number - 返回提取的数值
|
||||
*/
|
||||
export const getNum = (val: string): number => {
|
||||
// 使用正则提取数字部分,支持小数和负数
|
||||
const match = val.match(/-?\d+(\.\d+)?/);
|
||||
return match != null ? parseFloat(match[0] ?? "0") : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例: 获取单位
|
||||
* @example
|
||||
* getUnit("10rpx") // 返回 "rpx"
|
||||
* getUnit("10px") // 返回 "px"
|
||||
* @param val - 输入值,例如 "10rpx"、"10px"
|
||||
* @returns string - 返回单位字符串,如 "rpx" 或 "px"
|
||||
*/
|
||||
export const getUnit = (val: string): string => {
|
||||
const num = getNum(val);
|
||||
return val.replace(`${num}`, "");
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例: 转换为 rpx 值
|
||||
* @example
|
||||
* getRpx("10rpx") // 返回 10
|
||||
* getRpx("10px") // 返回 px2rpx(10)
|
||||
* getRpx(10) // 返回 10
|
||||
* @param val - 输入值,可以是 "10rpx"、"10px" 或数字 10
|
||||
* @returns number - 返回对应的 rpx 数值
|
||||
*/
|
||||
export const getRpx = (val: number | string): number => {
|
||||
if (typeof val == "number") {
|
||||
return val;
|
||||
}
|
||||
|
||||
const num = getNum(val);
|
||||
const unit = getUnit(val);
|
||||
|
||||
if (unit == "px") {
|
||||
return px2rpx(num);
|
||||
}
|
||||
|
||||
return num;
|
||||
};
|
||||
|
||||
/**
|
||||
* 示例: 转换为 px 值
|
||||
* @example
|
||||
* getPx("10rpx") // 返回 rpx2px(10)
|
||||
* getPx("10px") // 返回 10
|
||||
* getPx(10) // 返回 rpx2px(10)
|
||||
* @param val - 输入值,可以是 "10rpx"、"10px" 或数字 10
|
||||
* @returns number - 返回对应的 px 数值
|
||||
*/
|
||||
export const getPx = (val: string | number) => {
|
||||
if (typeof val == "number") {
|
||||
return rpx2px(val);
|
||||
}
|
||||
|
||||
const num = getNum(val);
|
||||
const unit = getUnit(val);
|
||||
|
||||
if (unit == "rpx") {
|
||||
return rpx2px(num);
|
||||
}
|
||||
|
||||
return num;
|
||||
};
|
||||
60
cool-unix/cool/utils/path.ts
Normal file
60
cool-unix/cool/utils/path.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 获取文件名
|
||||
* @example filename("a/b/c.txt") // "c"
|
||||
*/
|
||||
export function filename(path: string): string {
|
||||
return basename(path.substring(0, path.lastIndexOf(".")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径的最后一部分
|
||||
* @example basename("a/b/c.txt") // "c.txt"
|
||||
*/
|
||||
export function basename(path: string): string {
|
||||
let index = path.lastIndexOf("/");
|
||||
index = index > -1 ? index : path.lastIndexOf("\\");
|
||||
if (index < 0) {
|
||||
return path;
|
||||
}
|
||||
return path.substring(index + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @example extname("a/b/c.txt") // "txt"
|
||||
*/
|
||||
export function extname(path: string): string {
|
||||
let index = path.lastIndexOf(".");
|
||||
if (index < 0) {
|
||||
return "";
|
||||
}
|
||||
return path.substring(index + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
* @example firstUpperCase("useInfo") // "UseInfo"
|
||||
*/
|
||||
export function firstUpperCase(value: string): string {
|
||||
return value.charAt(0).toLocaleUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地址栏参数
|
||||
* @example getUrlParam("a") // "1"
|
||||
*/
|
||||
export function getUrlParam(name: string): string | null {
|
||||
// #ifdef H5
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get(name);
|
||||
return value !== null ? decodeURIComponent(value) : null;
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接路径
|
||||
* @example pathJoin("https://www.baidu.com/", "/a/b/c.txt") // "https://www.baidu.com/a/b/c.txt"
|
||||
*/
|
||||
export function pathJoin(...parts: string[]): string {
|
||||
return parts.map((part) => part.replace(/(^\/+|\/+$)/g, "")).join("/");
|
||||
}
|
||||
68
cool-unix/cool/utils/rect.ts
Normal file
68
cool-unix/cool/utils/rect.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { config } from "@/config";
|
||||
import { router } from "../router";
|
||||
import { isH5, isHarmony } from "./device";
|
||||
import { ctx } from "../ctx";
|
||||
import { getPx } from "./parse";
|
||||
|
||||
/**
|
||||
* 是否需要计算 tabBar 高度
|
||||
* @returns boolean
|
||||
*/
|
||||
export function hasCustomTabBar() {
|
||||
if (router.isTabPage()) {
|
||||
if (isHarmony()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.isCustomTabBar || isH5();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在自定义 topbar
|
||||
* @returns boolean
|
||||
*/
|
||||
export function hasCustomTopbar() {
|
||||
return router.route()?.isCustomNavbar ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安全区域高度
|
||||
* @param type 类型
|
||||
* @returns 安全区域高度
|
||||
*/
|
||||
export function getSafeAreaHeight(type: "top" | "bottom") {
|
||||
const { safeAreaInsets } = uni.getWindowInfo();
|
||||
|
||||
let h: number;
|
||||
|
||||
if (type == "top") {
|
||||
h = safeAreaInsets.top;
|
||||
} else {
|
||||
h = safeAreaInsets.bottom;
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
if (h == 0) {
|
||||
h = 16;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 tabBar 高度
|
||||
* @returns tabBar 高度
|
||||
*/
|
||||
export function getTabBarHeight() {
|
||||
let h = ctx.tabBar.height == null ? 50 : getPx(ctx.tabBar.height!);
|
||||
|
||||
if (hasCustomTabBar()) {
|
||||
h += getSafeAreaHeight("bottom");
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
158
cool-unix/cool/utils/storage.ts
Normal file
158
cool-unix/cool/utils/storage.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
// 过期时间后缀,用于标识存储数据的过期时间键名
|
||||
const EXPIRES_SUFFIX = "_deadtime";
|
||||
|
||||
/**
|
||||
* 存储管理类
|
||||
*
|
||||
* 封装了 uni-app 的存储 API,提供更便捷的存储操作
|
||||
* 支持数据过期时间管理,自动处理过期数据
|
||||
*/
|
||||
class Storage {
|
||||
/**
|
||||
* 获取存储数据
|
||||
*
|
||||
* @param key 存储键名
|
||||
* @returns 存储的数据,如果不存在则返回 null
|
||||
*
|
||||
* @example
|
||||
* const userData = storage.get('user');
|
||||
* if (userData != null) {
|
||||
* console.log(userData);
|
||||
* }
|
||||
*/
|
||||
get(key: string): any | null {
|
||||
return uni.getStorageSync(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有存储数据的信息
|
||||
*
|
||||
* 遍历所有存储键,返回包含所有键值对的对象
|
||||
* 注意:此方法会读取所有存储数据,大量数据时需注意性能
|
||||
*
|
||||
* @returns 包含所有存储数据的对象
|
||||
*
|
||||
* @example
|
||||
* const allData = storage.info();
|
||||
* console.log('所有存储数据:', allData);
|
||||
*/
|
||||
info() {
|
||||
// 获取存储信息,包含所有键名
|
||||
const info = uni.getStorageInfoSync();
|
||||
|
||||
// 创建空对象用于存放所有数据
|
||||
const d = {};
|
||||
|
||||
// 遍历所有键名,获取对应的值
|
||||
info.keys.forEach((e) => {
|
||||
d[e] = this.get(e);
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储数据
|
||||
*
|
||||
* @param key 存储键名
|
||||
* @param value 要存储的数据,支持任意类型
|
||||
* @param expires 过期时间(秒),默认为0表示永不过期
|
||||
*
|
||||
* @example
|
||||
* // 存储永久数据
|
||||
* storage.set('user', { name: '张三', age: 25 }, 0);
|
||||
*
|
||||
* // 存储5分钟后过期的数据
|
||||
* storage.set('token', 'abc123', 300);
|
||||
*/
|
||||
set(key: string, value: any, expires: number): void {
|
||||
// 存储主要数据
|
||||
uni.setStorageSync(key, value);
|
||||
|
||||
// 如果设置了过期时间,则存储过期时间戳
|
||||
if (expires > 0) {
|
||||
// 计算过期时间戳:当前时间 + 过期时间(秒转毫秒)
|
||||
const expireTime = new Date().getTime() + expires * 1000;
|
||||
uni.setStorageSync(`${key}${EXPIRES_SUFFIX}`, expireTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据是否已过期
|
||||
*
|
||||
* @param key 存储键名
|
||||
* @returns true表示已过期或无过期时间设置,false表示未过期
|
||||
*
|
||||
* @example
|
||||
* if (storage.isExpired('token')) {
|
||||
* console.log('token已过期');
|
||||
* }
|
||||
*/
|
||||
isExpired(key: string): boolean {
|
||||
// 获取过期时间戳
|
||||
const value = uni.getStorageSync(`${key}${EXPIRES_SUFFIX}`) as number | null;
|
||||
|
||||
// 如果没有设置过期时间,视为已过期
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 比较过期时间戳与当前时间,判断是否过期
|
||||
return value - new Date().getTime() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除存储数据
|
||||
*
|
||||
* 会同时删除数据本身和对应的过期时间
|
||||
*
|
||||
* @param key 存储键名
|
||||
*
|
||||
* @example
|
||||
* storage.remove('user');
|
||||
* storage.remove('token');
|
||||
*/
|
||||
remove(key: string) {
|
||||
// 删除主要数据
|
||||
uni.removeStorageSync(key);
|
||||
// 删除对应的过期时间数据
|
||||
uni.removeStorageSync(`${key}${EXPIRES_SUFFIX}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有存储数据
|
||||
*
|
||||
* 警告:此操作会删除所有本地存储数据,请谨慎使用
|
||||
*
|
||||
* @example
|
||||
* storage.clear(); // 清空所有数据
|
||||
*/
|
||||
clear() {
|
||||
uni.clearStorageSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据后立即删除(一次性读取)
|
||||
*
|
||||
* 适用于临时数据、一次性令牌等场景
|
||||
* 读取后数据会被自动删除,确保数据的一次性使用
|
||||
*
|
||||
* @param key 存储键名
|
||||
* @returns 存储的数据,如果不存在则返回 null
|
||||
*
|
||||
* @example
|
||||
* const tempToken = storage.once('temp_token');
|
||||
* // tempToken 使用后,存储中的 temp_token 已被删除
|
||||
*/
|
||||
once(key: string): any | null {
|
||||
// 先获取数据
|
||||
const value = this.get(key);
|
||||
// 立即删除数据
|
||||
this.remove(key);
|
||||
// 返回获取到的数据
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出存储实例,提供全局访问
|
||||
export const storage = new Storage();
|
||||
28
cool-unix/cool/utils/tailwind.ts
Normal file
28
cool-unix/cool/utils/tailwind.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 判断 Tailwind class 字符串中是否包含文本颜色类(如 text-red, text-red-500, text-sky 等)
|
||||
* @param className 传入的 class 字符串
|
||||
* @returns 是否包含文本颜色类
|
||||
*/
|
||||
export function hasTextColor(className: string): boolean {
|
||||
if (className == "") return false;
|
||||
|
||||
const regex =
|
||||
/\btext-(primary|surface|red|blue|green|yellow|purple|pink|indigo|gray|grey|black|white|orange|amber|lime|emerald|teal|cyan|sky|violet|fuchsia|rose|slate|zinc|neutral|stone)(?:-\d+)?\b/;
|
||||
|
||||
return regex.test(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Tailwind class 字符串中是否包含字体大小类
|
||||
* 支持如 text-xs, text-sm, text-base, text-lg, text-xl, 以及 text-[22px]、text-[22rpx] 等自定义写法
|
||||
* @param className 传入的 class 字符串
|
||||
* @returns 是否包含字体大小类
|
||||
*/
|
||||
export function hasTextSize(className: string): boolean {
|
||||
if (className == "") return false;
|
||||
|
||||
const regex =
|
||||
/\btext-(xs|sm|md|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|\[\d+[a-zA-Z%]*\])\b/;
|
||||
|
||||
return regex.test(className);
|
||||
}
|
||||
Reference in New Issue
Block a user