refactor: improve TypeScript type safety across codebase

Add global pinia type declaration for the custom unistorage option.
Add typed interfaces for all Pinia stores and enforce type safety on state and actions.
Add strict type annotations to all utility classes and helper functions.
Fix API response typing in the quick booking list page.
Remove unused iconsMap import in RefundPopup component.
Update package build scripts and add missing terser dependency.
Clean up outdated auto-generated component type definitions.
This commit is contained in:
DEV_DSW
2026-06-04 13:36:32 +08:00
parent 354232b444
commit 9b33bffdca
20 changed files with 384 additions and 214 deletions

44
components.d.ts vendored
View File

@@ -12,111 +12,67 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AddCarCrad: typeof import('./src/components/AddCarCrad/index.vue')['default']
AiTabSwitch: typeof import('./src/components/AiTabSwitch/index.vue')['default']
Calender: typeof import('./src/components/Calender/index.vue')['default']
CheckBox: typeof import('./src/components/CheckBox/index.vue')['default']
CommandWrapper: typeof import('./src/components/CommandWrapper/index.vue')['default']
CreateServiceOrder: typeof import('./src/components/CreateServiceOrder/index.vue')['default']
CustomEmpty: typeof import('./src/components/CustomEmpty/index.vue')['default']
DateRangeSection: typeof import('./src/components/DateRangeSection/index.vue')['default']
Demo: typeof import('./src/components/FormCard/demo.vue')['default']
DetailPopup: typeof import('./src/components/DetailPopup/index.vue')['default']
Divider: typeof import('./src/components/Divider/index.vue')['default']
Feedback: typeof import('./src/components/Feedback/index.vue')['default']
FormCard: typeof import('./src/components/FormCard/index.vue')['default']
GoodDetail: typeof import('./src/components/GoodDetail/index.vue')['default']
ImageSwiper: typeof import('./src/components/ImageSwiper/index.vue')['default']
LocationCard: typeof import('./src/components/LocationCard/index.vue')['default']
LocationInfo: typeof import('./src/components/LocationInfo/index.vue')['default']
ModuleTitle: typeof import('./src/components/ModuleTitle/index.vue')['default']
Privacy: typeof import('./src/components/Privacy/index.vue')['default']
Qrcode: typeof import('./src/components/Qrcode/index.vue')['default']
RefundPopup: typeof import('./src/components/RefundPopup/index.vue')['default']
ResponseIntro: typeof import('./src/components/ResponseIntro/index.vue')['default']
ResponseWrapper: typeof import('./src/components/ResponseWrapper/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ServiceTipsWord: typeof import('./src/components/ServiceTipsWord/index.vue')['default']
SpriteAnimator: typeof import('./src/components/SpriteAnimator/index.vue')['default']
Stepper: typeof import('./src/components/Stepper/index.vue')['default']
SumCard: typeof import('./src/components/SumCard/index.vue')['default']
SurveyQuestionnaire: typeof import('./src/components/SurveyQuestionnaire/index.vue')['default']
SwipeCards: typeof import('./src/components/SwipeCards/index.vue')['default']
TagsGroup: typeof import('./src/components/TagsGroup/index.vue')['default']
TopNavBar: typeof import('./src/components/TopNavBar/index.vue')['default']
UseDateRange: typeof import('./src/components/UseDateRange/index.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanIcon: typeof import('vant/es')['Icon']
VanIcons: typeof import('vant/es')['Icons']
VanList: typeof import('vant/es')['List']
VanPopup: typeof import('vant/es')['Popup']
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSearch: typeof import('vant/es')['Search']
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanSwiperItem: typeof import('vant/es')['SwiperItem']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
ZnIcon: typeof import('./src/components/ZnIcon/index.vue')['default']
}
}
// For TSX support
declare global {
const AddCarCrad: typeof import('./src/components/AddCarCrad/index.vue')['default']
const AiTabSwitch: typeof import('./src/components/AiTabSwitch/index.vue')['default']
const Calender: typeof import('./src/components/Calender/index.vue')['default']
const CheckBox: typeof import('./src/components/CheckBox/index.vue')['default']
const CommandWrapper: typeof import('./src/components/CommandWrapper/index.vue')['default']
const CreateServiceOrder: typeof import('./src/components/CreateServiceOrder/index.vue')['default']
const CustomEmpty: typeof import('./src/components/CustomEmpty/index.vue')['default']
const DateRangeSection: typeof import('./src/components/DateRangeSection/index.vue')['default']
const Demo: typeof import('./src/components/FormCard/demo.vue')['default']
const DetailPopup: typeof import('./src/components/DetailPopup/index.vue')['default']
const Divider: typeof import('./src/components/Divider/index.vue')['default']
const Feedback: typeof import('./src/components/Feedback/index.vue')['default']
const FormCard: typeof import('./src/components/FormCard/index.vue')['default']
const GoodDetail: typeof import('./src/components/GoodDetail/index.vue')['default']
const ImageSwiper: typeof import('./src/components/ImageSwiper/index.vue')['default']
const LocationCard: typeof import('./src/components/LocationCard/index.vue')['default']
const LocationInfo: typeof import('./src/components/LocationInfo/index.vue')['default']
const ModuleTitle: typeof import('./src/components/ModuleTitle/index.vue')['default']
const Privacy: typeof import('./src/components/Privacy/index.vue')['default']
const Qrcode: typeof import('./src/components/Qrcode/index.vue')['default']
const RefundPopup: typeof import('./src/components/RefundPopup/index.vue')['default']
const ResponseIntro: typeof import('./src/components/ResponseIntro/index.vue')['default']
const ResponseWrapper: typeof import('./src/components/ResponseWrapper/index.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const ServiceTipsWord: typeof import('./src/components/ServiceTipsWord/index.vue')['default']
const SpriteAnimator: typeof import('./src/components/SpriteAnimator/index.vue')['default']
const Stepper: typeof import('./src/components/Stepper/index.vue')['default']
const SumCard: typeof import('./src/components/SumCard/index.vue')['default']
const SurveyQuestionnaire: typeof import('./src/components/SurveyQuestionnaire/index.vue')['default']
const SwipeCards: typeof import('./src/components/SwipeCards/index.vue')['default']
const TagsGroup: typeof import('./src/components/TagsGroup/index.vue')['default']
const TopNavBar: typeof import('./src/components/TopNavBar/index.vue')['default']
const UseDateRange: typeof import('./src/components/UseDateRange/index.vue')['default']
const VanButton: typeof import('vant/es')['Button']
const VanCell: typeof import('vant/es')['Cell']
const VanCheckbox: typeof import('vant/es')['Checkbox']
const VanDivider: typeof import('vant/es')['Divider']
const VanField: typeof import('vant/es')['Field']
const VanIcon: typeof import('vant/es')['Icon']
const VanIcons: typeof import('vant/es')['Icons']
const VanList: typeof import('vant/es')['List']
const VanPopup: typeof import('vant/es')['Popup']
const VanPullRefresh: typeof import('vant/es')['PullRefresh']
const VanSearch: typeof import('vant/es')['Search']
const VanSwipe: typeof import('vant/es')['Swipe']
const VanSwipeItem: typeof import('vant/es')['SwipeItem']
const VanSwiperItem: typeof import('vant/es')['SwiperItem']
const VanTab: typeof import('vant/es')['Tab']
const VanTabs: typeof import('vant/es')['Tabs']
const ZnIcon: typeof import('./src/components/ZnIcon/index.vue')['default']
}

View File

@@ -5,11 +5,11 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build --mode production",
"build:staging": "vue-tsc --noEmit && vite build --mode staging",
"build:prod": "vue-tsc --noEmit && vite build --mode production",
"build": "node ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit && node ./node_modules/vite/bin/vite.js build --mode production",
"build:staging": "node ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit && node ./node_modules/vite/bin/vite.js build --mode staging",
"build:prod": "node ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit && node ./node_modules/vite/bin/vite.js build --mode production",
"preview": "vite preview --host 0.0.0.0",
"typecheck": "vue-tsc --noEmit",
"typecheck": "node ./node_modules/vue-tsc/bin/vue-tsc.js --noEmit",
"test": "node --test src/**/*.test.ts"
},
"dependencies": {
@@ -36,6 +36,7 @@
"concurrently": "^9.2.1",
"sass": "^1.70.0",
"tailwindcss": "^4.1.12",
"terser": "^5.16.0",
"typescript": "^5.8.3",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0",

View File

@@ -28,7 +28,6 @@
<script setup>
import { ref, computed, defineExpose } from "vue";
import { useI18n } from "vue-i18n";
import { iconsMap } from "@/static/fonts/znicons";
import ZnIcon from '@/components/ZnIcon/index.vue'
const { t } = useI18n();

View File

@@ -84,22 +84,45 @@ export const LONG_TEXT_PREVIEW_KEYS = [
const CONFIGURED_KEYS = LONG_TEXT_FIELD_CONFIG.map((item) => item.key);
export const createLongTextData = () => ({
type LongTextKey = string;
export interface LongTextData {
values: Record<LongTextKey, string>;
parsedValues: Record<LongTextKey, unknown>;
}
export interface LongTextChunk {
contentKey?: string | number | null;
contentValue?: unknown;
}
interface ParseResult {
ok: boolean;
value: unknown;
}
export interface LongTextSection {
contentKey: string;
contentValue: string;
parsedValue: unknown;
}
export const createLongTextData = (): LongTextData => ({
values: {},
parsedValues: {},
});
const toText = (value) => {
const toText = (value: unknown): string => {
if (value === undefined || value === null) return "";
return typeof value === "string" ? value : String(value);
};
const shouldParseJSON = (raw) => {
const shouldParseJSON = (raw: unknown): raw is string => {
if (!raw || typeof raw !== "string") return false;
return /^[\s]*[\[{]/.test(raw);
};
const tryParseJSON = (raw) => {
const tryParseJSON = (raw: string): ParseResult => {
if (!shouldParseJSON(raw)) {
return { ok: false, value: null };
}
@@ -110,7 +133,10 @@ const tryParseJSON = (raw) => {
}
};
export const appendLongTextChunk = (target, chunk = {}) => {
export const appendLongTextChunk = (
target: LongTextData | null | undefined,
chunk: LongTextChunk = {},
): LongTextData | null | undefined => {
if (!target || !chunk.contentKey) return target;
const key = String(chunk.contentKey);
@@ -131,19 +157,29 @@ export const appendLongTextChunk = (target, chunk = {}) => {
return target;
};
export const getLongTextValue = (data, key) => {
export const getLongTextValue = (
data: LongTextData | null | undefined,
key: string,
): string => {
if (!data || !data.values || !key) return "";
return data.values[key] || "";
};
export const getLongTextParsedValue = (data, key, fallback = undefined) => {
export const getLongTextParsedValue = <T = unknown>(
data: LongTextData | null | undefined,
key: string,
fallback?: T,
): T | unknown => {
if (!data || !data.parsedValues || !key) return fallback;
return Object.prototype.hasOwnProperty.call(data.parsedValues, key)
? data.parsedValues[key]
: fallback;
};
export const getLongTextPreviewText = (data, keys = LONG_TEXT_PREVIEW_KEYS) => {
export const getLongTextPreviewText = (
data: LongTextData | null | undefined,
keys: string[] = LONG_TEXT_PREVIEW_KEYS,
): string => {
for (const key of keys) {
const value = getLongTextValue(data, key);
if (value) return value;
@@ -151,12 +187,17 @@ export const getLongTextPreviewText = (data, keys = LONG_TEXT_PREVIEW_KEYS) => {
return "";
};
export const hasLongTextExtraSections = (data, previewKeys = LONG_TEXT_PREVIEW_KEYS) => {
export const hasLongTextExtraSections = (
data: LongTextData | null | undefined,
previewKeys: string[] = LONG_TEXT_PREVIEW_KEYS,
): boolean => {
if (!data || !data.values) return false;
return Object.keys(data.values).some((key) => !previewKeys.includes(key));
};
export const getLongTextSections = (data) => {
export const getLongTextSections = (
data: LongTextData | null | undefined,
): LongTextSection[] => {
if (!data || !data.values) return [];
const extraKeys = Object.keys(data.values).filter(

View File

@@ -65,6 +65,10 @@ type QuickTabItem = {
};
type QuickListItem = Record<string, any>;
type QuickListResponse = {
records?: QuickListItem[];
total?: number | string;
};
const PAGE_SIZE = 10;
const { t } = useI18n();
@@ -126,8 +130,9 @@ const queryList = async (pageNum = currentPage.value, pageSize = PAGE_SIZE) => {
}
const res = await quickBookingList(params);
const records = Array.isArray(res?.data?.records) ? res.data.records : [];
const total = Number(res?.data?.total ?? 0);
const pageData = res.data as QuickListResponse;
const records = Array.isArray(pageData.records) ? pageData.records : [];
const total = Number(pageData.total ?? 0);
const hasTotal = Number.isFinite(total) && total > 0;
if (pageNum === 1) {

View File

@@ -1,5 +1,10 @@
import { defineStore } from "pinia";
interface ServerConfig {
baseUrl: string;
wssUrl: string | undefined;
}
export const useAppStore = defineStore("app", {
state() {
return {
@@ -14,10 +19,10 @@ export const useAppStore = defineStore("app", {
getters: {},
actions: {
setSceneId(data) {
setSceneId(data: string) {
this.sceneId = data;
},
setServerConfig(data) {
setServerConfig(data: ServerConfig) {
this.serverConfig = data;
},
},

View File

@@ -1,5 +1,10 @@
import { defineStore } from "pinia";
interface LocationData {
latitude: number;
longitude: number;
}
export const useLocationStore = defineStore("location", {
state() {
return {
@@ -9,7 +14,7 @@ export const useLocationStore = defineStore("location", {
},
actions: {
setLocationData(data) {
setLocationData(data: LocationData) {
this.latitude = data.latitude;
this.longitude = data.longitude;
},

View File

@@ -3,7 +3,7 @@ import { defineStore } from "pinia";
export const usePictureStore = defineStore("picture", {
state() {
return {
previewImageData: [], // 预览图片数据
previewImageData: [] as any[],
};
},

View File

@@ -1,5 +1,11 @@
import { defineStore } from "pinia";
export interface SelectedDateState {
startDate: string;
endDate: string;
totalDays: number;
}
export const useSelectedDateStore = defineStore("selectedDate", {
state() {
return {
@@ -7,12 +13,12 @@ export const useSelectedDateStore = defineStore("selectedDate", {
startDate: "",
endDate: "",
totalDays: 1,
},
} as SelectedDateState,
};
},
actions: {
setData(data) {
setData(data: SelectedDateState) {
this.selectedDate = data;
},
},

7
src/types/pinia.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import "pinia";
declare module "pinia" {
export interface DefineStoreOptionsBase<S, Store> {
unistorage?: boolean;
}
}

View File

@@ -9,7 +9,11 @@ export class CallbackUtils {
* @param {string} callbackName - 回调函数名称
* @param {...any} args - 传递给回调函数的参数
*/
static safeCall(callbacks, callbackName, ...args) {
static safeCall(
callbacks: Record<string, (...args: any[]) => void> | null | undefined,
callbackName: string,
...args: any[]
): void {
if (callbacks && typeof callbacks[callbackName] === "function") {
try {
callbacks[callbackName](...args);
@@ -26,7 +30,10 @@ export class CallbackUtils {
* @param {Object} callbacks - 回调函数对象
* @param {Array} callbackConfigs - 回调配置数组 [{name, args}, ...]
*/
static safeBatchCall(callbacks, callbackConfigs) {
static safeBatchCall(
callbacks: Record<string, (...args: any[]) => void> | null | undefined,
callbackConfigs: Array<{ name: string; args?: any[] }>,
): void {
callbackConfigs.forEach((config) => {
const { name, args = [] } = config;
this.safeCall(callbacks, name, ...args);

View File

@@ -9,10 +9,10 @@ export class DebounceUtils {
* @param {number} delay - 防抖延迟时间
* @returns {Function} 防抖后的函数
*/
static createDebounce(func: Function, delay: number) {
let timerId: number | null = null;
static createDebounce(func: (...args: any[]) => void, delay: number) {
let timerId: ReturnType<typeof setTimeout> | null = null;
return function (...args: any[]) {
return function (this: unknown, ...args: any[]) {
if (timerId) {
clearTimeout(timerId);
}

View File

@@ -1,25 +1,19 @@
/**
* 消息处理工具类
* 提供消息相关的工具函数
*/
type MessageRecord = Record<string, any>;
export class MessageUtils {
/**
* 验证消息格式
* @param {any} message - 消息对象
* @returns {boolean} 是否为有效消息格式
*/
static validateMessage(message) {
return message && typeof message === "object" && message.type;
static validateMessage(message: unknown): message is MessageRecord {
return Boolean(
message &&
typeof message === "object" &&
(message as MessageRecord).type,
);
}
/**
* 格式化消息
* @param {string} type - 消息类型
* @param {any} content - 消息内容
* @param {Object} options - 额外选项
* @returns {Object} 格式化后的消息对象
*/
static formatMessage(type, content, options = {}) {
static formatMessage(
type: string,
content: unknown,
options: MessageRecord = {},
): MessageRecord {
return {
type,
content,
@@ -28,58 +22,36 @@ export class MessageUtils {
};
}
/**
* 检查是否为完整消息
* @param {any} message - 消息对象
* @returns {boolean} 是否为完整消息
*/
static isCompleteMessage(message) {
return message && message.isComplete === true;
static isCompleteMessage(message: MessageRecord | null | undefined): boolean {
return Boolean(message && message.isComplete === true);
}
/**
* 检查消息是否为心跳响应
* @param {any} messageData - 消息数据
* @returns {boolean} 是否为心跳响应
*/
static isPongMessage(messageData) {
static isPongMessage(messageData: unknown): boolean {
if (typeof messageData === "string") {
return (
messageData === "pong" || messageData.toLowerCase().includes("pong")
);
}
if (typeof messageData === "object" && messageData !== null) {
return messageData.type === "pong";
return (messageData as MessageRecord).type === "pong";
}
return false;
}
/**
* 安全解析JSON消息
* @param {string} messageStr - 消息字符串
* @returns {Object|null} 解析后的对象或null
*/
static safeParseJSON(messageStr) {
static safeParseJSON(messageStr: string): MessageRecord | null {
try {
return JSON.parse(messageStr);
} catch (error) {
console.warn("JSON解析失败:", messageStr);
} catch {
console.warn("JSON parse failed:", messageStr);
return null;
}
}
/**
* 创建打字机消息对象
* @param {string} content - 消息内容
* @param {boolean} isComplete - 是否完成
* @param {string} type - 消息类型
* @returns {Object} 消息对象
*/
static createTypewriterMessage(
content,
content: string,
isComplete = false,
type = "typewriter",
) {
): MessageRecord {
return {
type,
content,
@@ -88,12 +60,7 @@ export class MessageUtils {
};
}
/**
* 创建加载消息对象
* @param {string} content - 加载内容
* @returns {Object} 加载消息对象
*/
static createLoadingMessage(content = "加载中...") {
static createLoadingMessage(content = "loading..."): MessageRecord {
return {
type: "loading",
content,
@@ -101,15 +68,10 @@ export class MessageUtils {
};
}
/**
* 创建错误消息对象
* @param {string} error - 错误信息
* @returns {Object} 错误消息对象
*/
static createErrorMessage(error) {
static createErrorMessage(error: Error | string): MessageRecord {
return {
type: "error",
content: error.message || error || "未知错误",
content: error instanceof Error ? error.message : error || "Unknown Error",
timestamp: Date.now(),
};
}

View File

@@ -1,52 +1,88 @@
// 简单的流式数据管理器:开启流、更新流、订阅流、关闭流
const streams = {};
type StreamPayload = unknown;
type StreamSubscriber = (
text: string,
finished: boolean,
payload: StreamPayload,
) => void;
function notify(stream) {
interface StreamState {
text: string;
finished: boolean;
payload: StreamPayload;
subs: Set<StreamSubscriber>;
}
const streams: Record<string, StreamState> = {};
function createEmptyStream(): StreamState {
return { text: "", finished: false, payload: null, subs: new Set() };
}
function notify(stream: StreamState): void {
stream.subs.forEach((cb) => cb(stream.text, stream.finished, stream.payload));
}
function openStream(id, initial = '', finished = false, payload = null) {
function ensureStream(id: string): StreamState {
streams[id] = streams[id] || createEmptyStream();
return streams[id];
}
function openStream(
id: string,
initial = "",
finished = false,
payload: StreamPayload = null,
): void {
if (!id) return;
streams[id] = streams[id] || { text: '', finished: false, payload: null, subs: new Set() };
streams[id].text = initial || '';
streams[id].finished = !!finished;
streams[id].payload = payload || null;
// notify existing subscribers
notify(streams[id]);
const stream = ensureStream(id);
stream.text = initial || "";
stream.finished = Boolean(finished);
stream.payload = payload || null;
notify(stream);
}
function updateStream(id, text, finished = false, payload = null) {
function updateStream(
id: string,
text: string,
finished = false,
payload: StreamPayload = null,
): void {
if (!id || !streams[id]) return;
streams[id].text = text || '';
streams[id].finished = !!finished;
streams[id].text = text || "";
streams[id].finished = Boolean(finished);
streams[id].payload = payload || null;
notify(streams[id]);
}
function subscribe(id, cb) {
function subscribe(id: string, cb: StreamSubscriber): () => void {
if (!id) return () => {};
streams[id] = streams[id] || { text: '', finished: false, payload: null, subs: new Set() };
streams[id].subs.add(cb);
// send current snapshot immediately
cb(streams[id].text, streams[id].finished, streams[id].payload);
const stream = ensureStream(id);
stream.subs.add(cb);
cb(stream.text, stream.finished, stream.payload);
return () => {
streams[id] && streams[id].subs.delete(cb);
// 移除空流
if (streams[id] && streams[id].subs.size === 0 && streams[id].finished) {
const current = streams[id];
if (!current) return;
current.subs.delete(cb);
if (current.subs.size === 0 && current.finished) {
delete streams[id];
}
};
}
function closeStream(id) {
function closeStream(id: string): void {
if (!id || !streams[id]) return;
streams[id].subs.forEach((cb) => cb(streams[id].text, true, streams[id].payload));
delete streams[id];
}
function getSnapshot(id) {
if (!id || !streams[id]) return { text: '', finished: false, payload: null };
return { text: streams[id].text, finished: streams[id].finished, payload: streams[id].payload };
function getSnapshot(id: string): Omit<StreamState, "subs"> {
if (!id || !streams[id]) return { text: "", finished: false, payload: null };
return {
text: streams[id].text,
finished: streams[id].finished,
payload: streams[id].payload,
};
}
export default {

View File

@@ -9,10 +9,10 @@ export class ThrottleUtils {
* @param {number} delay - 节流延迟时间
* @returns {Function} 节流后的函数
*/
static createThrottle(func: Function, delay: number) {
static createThrottle(func: (...args: any[]) => void, delay: number) {
let prev = Date.now();
return function (...args: any[]) {
return function (this: unknown, ...args: any[]) {
let now = Date.now();
if (now - prev >= delay) {

View File

@@ -9,7 +9,10 @@ export class TimerUtils {
* @param {string} type - 定时器类型 'timeout' | 'interval'
* @returns {null} 返回null便于重置变量
*/
static safeClear(timerId, type = "timeout") {
static safeClear(
timerId: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval> | null,
type: "timeout" | "interval" = "timeout",
): null {
if (timerId) {
if (type === "interval") {
clearInterval(timerId);
@@ -26,7 +29,10 @@ export class TimerUtils {
* @param {string} type - 定时器类型 'timeout' | 'interval'
* @returns {null} 返回null便于重置变量
*/
static clearTimer(timerId, type = "timeout") {
static clearTimer(
timerId: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval> | null,
type: "timeout" | "interval" = "timeout",
): null {
return this.safeClear(timerId, type);
}
@@ -36,8 +42,8 @@ export class TimerUtils {
* @param {number} delay - 延时时间
* @returns {Object} 包含cancel方法的对象
*/
static createCancelableTimeout(callback, delay) {
let timerId = setTimeout(callback, delay);
static createCancelableTimeout(callback: () => void, delay: number) {
let timerId: ReturnType<typeof setTimeout> | null = setTimeout(callback, delay);
return {
cancel() {
if (timerId) {
@@ -57,8 +63,8 @@ export class TimerUtils {
* @param {number} interval - 间隔时间
* @returns {Object} 包含cancel方法的对象
*/
static createCancelableInterval(callback, interval) {
let timerId = setInterval(callback, interval);
static createCancelableInterval(callback: () => void, interval: number) {
let timerId: ReturnType<typeof setInterval> | null = setInterval(callback, interval);
return {
cancel() {
if (timerId) {

View File

@@ -1,16 +1,16 @@
export function getUrlParams(url) {
let theRequest = {};
export function getUrlParams(url: string): Record<string, string> {
const theRequest: Record<string, string> = {};
if(url.indexOf("#") != -1){
const str=url.split("#")[1];
const strs=str.split("&");
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split("=")[0]] = decodeURI(strs[i].split("=")[1]);
theRequest[strs[i].split("=")[0]] = decodeURI(strs[i].split("=")[1] ?? "");
}
}else if(url.indexOf("?") != -1){
const str=url.split("?")[1];
const strs=str.split("&");
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split("=")[0]] = decodeURI(strs[i].split("=")[1]);
theRequest[strs[i].split("=")[0]] = decodeURI(strs[i].split("=")[1] ?? "");
}
}
return theRequest;
@@ -21,14 +21,14 @@ export function getUrlParams(url) {
* @param {Object} args - 参数对象
* @returns {String} 返回key=value&格式的查询字符串
*/
export function objectToUrlParams(args) {
export function objectToUrlParams(args: Record<string, unknown> | null | undefined): string {
if (!args || typeof args !== 'object') {
return '';
}
const params = [];
const params: string[] = [];
for (const key in args) {
if (args.hasOwnProperty(key) && args[key] !== undefined && args[key] !== null) {
if (Object.prototype.hasOwnProperty.call(args, key) && args[key] !== undefined && args[key] !== null) {
params.push(`${key}=${args[key]}`);
}
}

View File

@@ -8,8 +8,86 @@ import { CallbackUtils } from "./CallbackUtils";
import { MessageUtils } from "./MessageUtils";
import { TimerUtils } from "./TimerUtils";
declare const uni: any;
type TimerHandle =
| ReturnType<typeof setTimeout>
| ReturnType<typeof setInterval>;
type WebSocketProtocol = string | string[];
type WebSocketMessage = Record<string, any> & {
retryCount?: number;
timestamp?: number;
};
type WebSocketCallback = (...args: any[]) => any;
type WebSocketLike = {
readyState?: number;
send?: (payload: any) => void;
close?: (...args: any[]) => void;
onOpen?: (callback: (event: unknown) => void) => void;
onMessage?: (callback: (event: unknown) => void) => void;
onClose?: (callback: (event: WebSocketCloseEvent) => void) => void;
onError?: (callback: (error: any) => void) => void;
onopen?: any;
onmessage?: any;
onclose?: any;
onerror?: any;
};
type WebSocketCloseEvent = {
code?: number;
reason?: string;
};
type WebSocketManagerCallbacks = {
onConnect?: WebSocketCallback;
onDisconnect?: WebSocketCallback;
onError?: WebSocketCallback;
onMessage?: WebSocketCallback;
getConversationId?: () => string;
getAgentId?: () => string;
[key: string]: WebSocketCallback | undefined;
};
type WebSocketManagerStats = {
messagesReceived: number;
messagesSent: number;
messagesDropped: number;
reconnectCount: number;
connectionStartTime: number | null;
lastMessageTime: number | null;
};
type WebSocketManagerOptions = {
wsUrl?: string;
protocols?: WebSocketProtocol;
reconnectInterval?: number;
maxReconnectAttempts?: number;
heartbeatInterval?: number;
onConnect?: WebSocketCallback;
onOpen?: WebSocketCallback;
onDisconnect?: WebSocketCallback;
onClose?: WebSocketCallback;
onError?: WebSocketCallback;
onMessage?: WebSocketCallback;
getConversationId?: () => string;
getAgentId?: () => string;
};
export class WebSocketManager {
constructor(options = {}) {
wsUrl: string;
protocols: WebSocketProtocol;
reconnectInterval: number;
maxReconnectAttempts: number;
heartbeatInterval: number;
ws: WebSocketLike | null;
reconnectAttempts: number;
isConnecting: boolean;
connectionState: boolean;
heartbeatTimer: TimerHandle | null;
reconnectTimer: TimerHandle | null;
callbacks: WebSocketManagerCallbacks;
messageQueue: WebSocketMessage[];
isProcessingQueue: boolean;
stats: WebSocketManagerStats;
constructor(options: WebSocketManagerOptions = {}) {
// 基础配置
console.log("WebSocketManager构造函数接收到的options:", options);
this.wsUrl = options.wsUrl || "";
@@ -58,14 +136,18 @@ export class WebSocketManager {
/**
* 安全调用回调函数
*/
_safeCallCallback(callbackName, ...args) {
CallbackUtils.safeCall(this.callbacks, callbackName, ...args);
_safeCallCallback(callbackName: string, ...args: any[]): void {
CallbackUtils.safeCall(
this.callbacks as Record<string, (...args: any[]) => void>,
callbackName,
...args,
);
}
/**
* 初始化连接
*/
async init(wsUrl) {
async init(wsUrl?: string): Promise<void> {
if (wsUrl) {
this.wsUrl = wsUrl;
}
@@ -80,7 +162,7 @@ export class WebSocketManager {
/**
* 建立WebSocket连接
*/
async connect() {
async connect(): Promise<void> {
if (this.isConnecting || this.connectionState) {
console.log("WebSocket已在连接中或已连接跳过连接");
return;
@@ -99,7 +181,7 @@ export class WebSocketManager {
success: () => {
console.log("uni.connectSocket调用成功");
},
fail: (error) => {
fail: (error: unknown) => {
console.error("uni.connectSocket调用失败:", error);
this.isConnecting = false;
this.connectionState = false;
@@ -114,11 +196,18 @@ export class WebSocketManager {
});
// 确保ws对象存在且有相应方法
if (this.ws && typeof this.ws.onOpen === "function") {
this.ws.onOpen(this.handleOpen.bind(this));
this.ws.onMessage(this.handleMessage.bind(this));
this.ws.onClose(this.handleClose.bind(this));
this.ws.onError(this.handleError.bind(this));
const socket = this.ws;
if (
socket &&
typeof socket.onOpen === "function" &&
typeof socket.onMessage === "function" &&
typeof socket.onClose === "function" &&
typeof socket.onError === "function"
) {
socket.onOpen(this.handleOpen.bind(this));
socket.onMessage(this.handleMessage.bind(this));
socket.onClose(this.handleClose.bind(this));
socket.onError(this.handleError.bind(this));
} else {
console.error("uni.connectSocket返回的对象无效");
this.isConnecting = false;
@@ -146,12 +235,13 @@ export class WebSocketManager {
} else {
// 在其他环境中使用标准 WebSocket
console.log("使用标准WebSocket创建连接");
this.ws = new WebSocket(this.wsUrl, this.protocols);
const socket = new WebSocket(this.wsUrl, this.protocols);
this.ws = socket;
this.ws.onopen = this.handleOpen.bind(this);
this.ws.onmessage = this.handleMessage.bind(this);
this.ws.onclose = this.handleClose.bind(this);
this.ws.onerror = this.handleError.bind(this);
socket.onopen = this.handleOpen.bind(this);
socket.onmessage = this.handleMessage.bind(this);
socket.onclose = this.handleClose.bind(this);
socket.onerror = this.handleError.bind(this);
}
console.log("WebSocket实例创建成功等待连接...");
@@ -171,7 +261,7 @@ export class WebSocketManager {
/**
* 连接成功处理
*/
handleOpen(event) {
handleOpen(event: unknown): void {
console.log("WebSocket连接已建立");
this.isConnecting = false;
this.connectionState = true;
@@ -190,12 +280,15 @@ export class WebSocketManager {
/**
* 处理消息队列
*/
_processMessageQueue() {
_processMessageQueue(): void {
while (this.messageQueue.length > 0) {
const queuedMessage = this.messageQueue.shift();
if (!queuedMessage) {
continue;
}
// 检查重试次数
if (queuedMessage.retryCount >= 3) {
if ((queuedMessage.retryCount ?? 0) >= 3) {
console.warn("消息重试次数超限,丢弃消息:", queuedMessage);
this.stats.messagesDropped++;
continue;
@@ -209,10 +302,10 @@ export class WebSocketManager {
/**
* 消息接收处理
*/
handleMessage(event) {
handleMessage(event: { data?: unknown } | unknown): void {
try {
// 在小程序环境中,消息数据可能在 event.data 中
const messageData = event.data || event;
const messageData = (event as { data?: unknown }).data || event;
// 处理心跳响应和其他非JSON消息 - 在JSON解析前检查
if (typeof messageData === "string") {
@@ -264,7 +357,7 @@ export class WebSocketManager {
/**
* 连接关闭处理
*/
handleClose(event) {
handleClose(event: WebSocketCloseEvent): void {
console.log("WebSocket连接已关闭:", event.code, event.reason);
this.connectionState = false;
this.isConnecting = false;
@@ -286,7 +379,7 @@ export class WebSocketManager {
/**
* 错误处理
*/
handleError(error) {
handleError(error: any): void {
const errorDetails = {
error,
wsUrl: this.wsUrl,
@@ -327,7 +420,7 @@ export class WebSocketManager {
/**
* 发送消息
*/
sendMessage(message) {
sendMessage(message: WebSocketMessage): boolean {
const messageData = {
...message,
timestamp: Date.now(),
@@ -348,7 +441,7 @@ export class WebSocketManager {
this.stats.messagesSent++;
//console.log("消息发送成功:", messageData);
},
fail: (error) => {
fail: (error: unknown) => {
console.error("发送消息失败:", error);
this._safeCallCallback("onError", error);
@@ -383,7 +476,7 @@ export class WebSocketManager {
/**
* 开始心跳
*/
startHeartbeat() {
startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
@@ -415,7 +508,7 @@ export class WebSocketManager {
/**
* 停止心跳
*/
stopHeartbeat() {
stopHeartbeat(): void {
TimerUtils.clearTimer(this.heartbeatTimer, "interval");
this.heartbeatTimer = null;
}
@@ -423,7 +516,7 @@ export class WebSocketManager {
/**
* 安排重连
*/
scheduleReconnect() {
scheduleReconnect(): void {
// 清理之前的重连定时器
if (this.reconnectTimer) {
TimerUtils.clearTimer(this.reconnectTimer);
@@ -474,7 +567,7 @@ export class WebSocketManager {
/**
* 重置连接状态
*/
resetConnectionState() {
resetConnectionState(): void {
this.connectionState = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
@@ -492,8 +585,8 @@ export class WebSocketManager {
/**
* 检查是否已连接
*/
isConnected() {
return (
isConnected(): boolean {
return Boolean(
this.connectionState &&
this.ws &&
(this.ws.readyState === 1 ||
@@ -505,7 +598,7 @@ export class WebSocketManager {
/**
* 手动重连
*/
async reconnect() {
async reconnect(): Promise<boolean> {
console.log("手动触发重连");
// 先关闭现有连接
@@ -558,7 +651,7 @@ export class WebSocketManager {
/**
* 重置统计信息
*/
resetStats() {
resetStats(): void {
this.stats = {
messagesReceived: 0,
messagesSent: 0,
@@ -573,7 +666,7 @@ export class WebSocketManager {
/**
* 关闭连接
*/
close() {
close(): void {
this.stopHeartbeat();
this.resetConnectionState();
@@ -591,7 +684,7 @@ export class WebSocketManager {
success: () => {
console.log("WebSocket连接已关闭");
},
fail: (error) => {
fail: (error: unknown) => {
console.error("关闭WebSocket连接失败:", error);
},
});
@@ -610,7 +703,7 @@ export class WebSocketManager {
/**
* 销毁实例
*/
destroy() {
destroy(): void {
// 输出最终统计信息
console.log("WebSocketManager销毁前统计:", this.getStats());

View File

@@ -9,11 +9,11 @@ export class DateUtils {
* @param {string} format - 格式化字符串,默认值为 "yyyy-MM-dd"
* @returns {string} 格式化后的日期字符串
*/
static formatDate(date = new Date(), format = "yyyy-MM-dd") {
static formatDate(date = new Date(), format = "yyyy-MM-dd"): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return format.replace("yyyy", year).replace("MM", month).replace("dd", day);
return format.replace("yyyy", String(year)).replace("MM", month).replace("dd", day);
}
}

View File

@@ -1026,12 +1026,20 @@
resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/source-map@^0.3.3":
version "0.3.11"
resolved "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba"
integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28":
version "0.3.31"
resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -1759,6 +1767,11 @@ browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.28.1:
node-releases "^2.0.36"
update-browserslist-db "^1.2.3"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
@@ -1815,6 +1828,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
concurrently@^9.2.1:
version "9.2.1"
resolved "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz#248ea21b95754947be2dad9c3e4b60f18ca4e44f"
@@ -2720,6 +2738,19 @@ socket.io-parser@~4.2.4:
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
speakingurl@^14.0.1:
version "14.0.1"
resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
@@ -2789,6 +2820,16 @@ tapable@^2.3.3:
resolved "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
terser@^5.16.0:
version "5.48.0"
resolved "https://registry.npmmirror.com/terser/-/terser-5.48.0.tgz#8b391171cfbb7ac4a88f9f04ba1cfabc54f643db"
integrity sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.15.0"
commander "^2.20.0"
source-map-support "~0.5.20"
tinyglobby@^0.2.13, tinyglobby@^0.2.15, tinyglobby@^0.2.16:
version "0.2.16"
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"