111
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Application.Domain
|
||||
{
|
||||
public class LoginParms
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string pwd { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,19 +17,24 @@ namespace Application.Web.Controllers.Login
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
/// <summary>
|
||||
/// 登录接口
|
||||
/// </summary>
|
||||
/// <param name="parms"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// 登录接口
|
||||
/// </summary>
|
||||
/// <param name="parms"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IPoAction> Login([FromBody] LoginParms parms)
|
||||
{
|
||||
return PoAction.Ok(parms.name);
|
||||
return PoAction.Ok(parms.code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试接口
|
||||
/// </summary>
|
||||
/// <param name="name">测试名</param>
|
||||
/// <param name="ttt">测试2</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IPoAction> Test(string name)
|
||||
public async Task<IPoAction> Test(string name,string ttt)
|
||||
{
|
||||
await _hubContext.Clients.All.SendAsync("ReceiveMessage", "系统");
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Application.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录请求参数
|
||||
/// </summary>
|
||||
public class LoginParms
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录名/手机号
|
||||
/// </summary>
|
||||
public string name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string pwd { get; set; }= string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 验证码
|
||||
/// </summary>
|
||||
public string code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ export default defineNuxtConfig({
|
||||
'stores',
|
||||
'composables',
|
||||
'extends',
|
||||
'services',
|
||||
"model"
|
||||
'services/**',
|
||||
"model/**"
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ const appStore = useAppStore()
|
||||
import { Loading } from 'vant'
|
||||
// 初始化应用配置
|
||||
onMounted(() => {
|
||||
|
||||
|
||||
// 初始化屏幕尺寸
|
||||
if (typeof window !== 'undefined') {
|
||||
appStore.updateScreenSize(window.innerWidth, window.innerHeight)
|
||||
@@ -36,7 +34,6 @@ onMounted(() => {
|
||||
appStore.setOnlineStatus(false)
|
||||
})
|
||||
}
|
||||
alert("main");
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { navigateTo } from "#app";
|
||||
import { RequestExtend } from "@/extends/RequestExtend";
|
||||
import { BaseConfig } from "@/config/BaseConfig";
|
||||
import type { IResultData } from "@/model/common/ResultData";
|
||||
|
||||
type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
|
||||
type RequestParams = Record<string, unknown>;
|
||||
@@ -35,7 +32,7 @@ export class ApiService {
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
void navigateTo("/home", { replace: true });
|
||||
PageExtend.Redirect("/home");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +92,7 @@ export class ApiService {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public static async ApiRequest<T = unknown>(
|
||||
public static async Request<T = unknown>(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
params: RequestParams = {}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* 权限校验组合式函数
|
||||
* 提供登录检查、权限验证等功能
|
||||
*/
|
||||
import { useUserStore } from '~/stores/user'
|
||||
import { useAppStore } from '~/stores/app'
|
||||
|
||||
/**
|
||||
* 权限校验hook
|
||||
* 用于页面/组件中的权限校验
|
||||
*/
|
||||
export const useAuth = () => {
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @returns boolean 是否已登录
|
||||
*/
|
||||
const isAuthenticated = (): boolean => {
|
||||
return userStore.isLogin
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否未登录
|
||||
* @returns boolean 是否未登录
|
||||
*/
|
||||
const isGuest = (): boolean => {
|
||||
return !userStore.isLogin
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否拥有指定角色
|
||||
* @param roles 角色数组
|
||||
* @returns boolean 是否有权限
|
||||
*/
|
||||
const hasRole = (roles: string | string[]): boolean => {
|
||||
if (!userStore.isLogin) return false
|
||||
|
||||
const userRole = userStore.userRole
|
||||
const roleList = Array.isArray(roles) ? roles : [roles]
|
||||
|
||||
return roleList.includes(userRole)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为管理员
|
||||
* @returns boolean 是否为管理员
|
||||
*/
|
||||
const isAdmin = (): boolean => {
|
||||
return hasRole('admin')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
* @returns number 用户ID
|
||||
*/
|
||||
const getUserId = (): number => {
|
||||
return userStore.userId
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns IUserInfo | null 用户信息
|
||||
*/
|
||||
const getUserInfo = () => {
|
||||
return userStore.userInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户昵称
|
||||
* @returns string 用户昵称
|
||||
*/
|
||||
const getNickname = (): string => {
|
||||
return userStore.userNickname
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页(如果未登录)
|
||||
* @param redirectUrl 登录后重定向的URL
|
||||
*/
|
||||
const requireAuth = (redirectUrl?: string) => {
|
||||
if (!userStore.isLogin) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = redirectUrl || window.location.href
|
||||
window.location.href = `/login?redirect=${encodeURIComponent(url)}`
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页(如果已登录)
|
||||
* @param redirectUrl 登录后重定向的URL
|
||||
*/
|
||||
const requireGuest = (redirectUrl: string = '/') => {
|
||||
if (userStore.isLogin) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = redirectUrl
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查功能权限(基于角色)
|
||||
* @param permission 权限标识
|
||||
* @returns boolean 是否有权限
|
||||
*/
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
// 简化实现,实际项目中可以结合后端返回的权限列表
|
||||
if (!userStore.isLogin) return false
|
||||
|
||||
// 管理员拥有所有权限
|
||||
if (userStore.userRole === 'admin') return true
|
||||
|
||||
// TODO: 可以从用户信息中获取权限列表进行匹配
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
* @param redirectUrl 退出后重定向的URL
|
||||
*/
|
||||
const logout = async (redirectUrl: string = '/login') => {
|
||||
try {
|
||||
// 调用退出登录API(如果需要)
|
||||
// const userService = new UserSERVICE()
|
||||
// await userService.logout()
|
||||
} finally {
|
||||
// 清除用户状态
|
||||
userStore.clearUserInfo()
|
||||
|
||||
// 跳转到登录页
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = redirectUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isLogin: computed(() => userStore.isLogin),
|
||||
userInfo: computed(() => userStore.userInfo),
|
||||
userId: computed(() => userStore.userId),
|
||||
nickname: computed(() => userStore.userNickname),
|
||||
userRole: computed(() => userStore.userRole),
|
||||
|
||||
// 方法
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
hasRole,
|
||||
isAdmin,
|
||||
getUserId,
|
||||
getUserInfo,
|
||||
getNickname,
|
||||
requireAuth,
|
||||
requireGuest,
|
||||
hasPermission,
|
||||
logout
|
||||
}
|
||||
}
|
||||
34
Web/src/composables/useEventBus.ts
Normal file
34
Web/src/composables/useEventBus.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { EventBusHandler } from '~/extends/EventBusExtend'
|
||||
|
||||
/**
|
||||
* 事件总线组合式封装
|
||||
* 在组件作用域内订阅时会自动解绑
|
||||
*/
|
||||
export const useEventBus = () => {
|
||||
const registerDispose = (unsubscribe: () => void) => {
|
||||
if (getCurrentScope()) {
|
||||
onScopeDispose(unsubscribe)
|
||||
}
|
||||
|
||||
return unsubscribe
|
||||
}
|
||||
|
||||
const on = <T = unknown>(event: string, handler: EventBusHandler<T>) => {
|
||||
return registerDispose(EventBusExtend.on(event, handler))
|
||||
}
|
||||
|
||||
const off = <T = unknown>(event: string, handler?: EventBusHandler<T>) => {
|
||||
EventBusExtend.off(event, handler)
|
||||
}
|
||||
|
||||
const emit = <T = unknown>(event: string, payload?: T) => {
|
||||
EventBusExtend.emit(event, payload)
|
||||
}
|
||||
|
||||
return {
|
||||
on,
|
||||
off,
|
||||
emit,
|
||||
clear: EventBusExtend.clear
|
||||
}
|
||||
}
|
||||
178
Web/src/extends/EventBusExtend.ts
Normal file
178
Web/src/extends/EventBusExtend.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
interface IEventBusStore {
|
||||
listeners: Map<string, Set<(payload?: unknown) => void>>
|
||||
channel: BroadcastChannel | null
|
||||
}
|
||||
|
||||
interface IEventBusWindow extends Window {
|
||||
__SEA_TIME_EVENT_BUS__?: IEventBusStore
|
||||
}
|
||||
|
||||
export type EventBusHandler<T = unknown> = (payload?: T) => void
|
||||
|
||||
interface IEventBusMessage<T = unknown> {
|
||||
event: string
|
||||
payload?: T
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局事件总线扩展
|
||||
* 基于 BroadcastChannel 实现跨页面通信
|
||||
*/
|
||||
export class EventBusExtend {
|
||||
private static readonly CHANNEL_NAME = 'sea-time-event-bus'
|
||||
|
||||
private static isClient(): boolean {
|
||||
return typeof window !== 'undefined'
|
||||
}
|
||||
|
||||
private static supportsBroadcastChannel(): boolean {
|
||||
return typeof BroadcastChannel !== 'undefined'
|
||||
}
|
||||
|
||||
private static createStore(): IEventBusStore {
|
||||
return {
|
||||
listeners: new Map(),
|
||||
channel: this.createChannel()
|
||||
}
|
||||
}
|
||||
|
||||
private static getStore(): IEventBusStore | null {
|
||||
if (!this.isClient()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const target = window as IEventBusWindow
|
||||
target.__SEA_TIME_EVENT_BUS__ ||= this.createStore()
|
||||
|
||||
return target.__SEA_TIME_EVENT_BUS__ || null
|
||||
}
|
||||
|
||||
private static createChannel(): BroadcastChannel | null {
|
||||
if (!this.supportsBroadcastChannel()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const channel = new BroadcastChannel(this.CHANNEL_NAME)
|
||||
channel.onmessage = (messageEvent: MessageEvent<IEventBusMessage>) => {
|
||||
const data = messageEvent.data
|
||||
if (!data?.event) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dispatch(data.event, data.payload)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
private static getListeners(event: string): Set<(payload?: unknown) => void> | null {
|
||||
const store = this.getStore()
|
||||
if (!store) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!store.listeners.has(event)) {
|
||||
store.listeners.set(event, new Set())
|
||||
}
|
||||
|
||||
return store.listeners.get(event) || null
|
||||
}
|
||||
|
||||
private static removeHandler(event: string, handler: (payload?: unknown) => void): void {
|
||||
const listeners = this.getListeners(event)
|
||||
if (!listeners) {
|
||||
return
|
||||
}
|
||||
|
||||
listeners.delete(handler)
|
||||
|
||||
if (listeners.size === 0) {
|
||||
this.getStore()?.listeners.delete(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
*/
|
||||
static on<T = unknown>(
|
||||
event: string,
|
||||
handler: EventBusHandler<T>
|
||||
): () => void {
|
||||
const listeners = this.getListeners(event)
|
||||
if (!listeners) {
|
||||
return () => undefined
|
||||
}
|
||||
|
||||
const wrappedHandler = handler as (payload?: unknown) => void
|
||||
listeners.add(wrappedHandler)
|
||||
|
||||
return () => {
|
||||
this.removeHandler(event, wrappedHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
* 不传 handler 时清空该事件下全部监听
|
||||
*/
|
||||
static off<T = unknown>(event: string, handler?: EventBusHandler<T>): void {
|
||||
const listeners = this.getListeners(event)
|
||||
if (!listeners) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!handler) {
|
||||
listeners.clear()
|
||||
return
|
||||
}
|
||||
|
||||
this.removeHandler(event, handler as (payload?: unknown) => void)
|
||||
}
|
||||
|
||||
private static dispatch<T = unknown>(event: string, payload?: T): void {
|
||||
const listeners = this.getListeners(event)
|
||||
if (!listeners) {
|
||||
return
|
||||
}
|
||||
|
||||
Array.from(listeners).forEach((listener) => {
|
||||
listener(payload)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布事件
|
||||
*/
|
||||
static emit<T = unknown>(
|
||||
event: string,
|
||||
payload?: T
|
||||
): void {
|
||||
const store = this.getStore()
|
||||
if (!store) {
|
||||
return
|
||||
}
|
||||
|
||||
this.dispatch(event, payload)
|
||||
store.channel?.postMessage({
|
||||
event,
|
||||
payload
|
||||
} satisfies IEventBusMessage<T>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空监听
|
||||
*/
|
||||
static clear(event?: string): void {
|
||||
const store = this.getStore()
|
||||
if (!store) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
store.listeners.clear()
|
||||
return
|
||||
}
|
||||
|
||||
store.listeners.delete(event)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
export class MessageExtend {
|
||||
// 消息通知
|
||||
static notify(type: 'primary' | 'success' | 'danger' | 'warning', message: any) {
|
||||
static Notify(type: 'primary' | 'success' | 'danger' | 'warning', message: any) {
|
||||
showNotify({ type, message })
|
||||
}
|
||||
|
||||
// 提示弹窗
|
||||
static dialog(
|
||||
static Dialog(
|
||||
title: string,
|
||||
message: string,
|
||||
theme: 'round-button' | 'default',
|
||||
@@ -31,7 +31,7 @@ export class MessageExtend {
|
||||
}
|
||||
|
||||
// 成功失败默认提示
|
||||
static showToast(type: 'success' | 'fail' | 'default', text: any) {
|
||||
static ShowToast(type: 'success' | 'fail' | 'default', text: any) {
|
||||
if (type == 'success') {
|
||||
showSuccessToast(text)
|
||||
} else if (type == 'fail') {
|
||||
@@ -42,7 +42,7 @@ export class MessageExtend {
|
||||
}
|
||||
|
||||
// 自定义图标提示
|
||||
static showIconToast(icon: any, text: any) {
|
||||
static ShowIconToast(icon: any, text: any) {
|
||||
showToast({
|
||||
message: text,
|
||||
icon: icon,
|
||||
@@ -50,7 +50,7 @@ export class MessageExtend {
|
||||
}
|
||||
|
||||
// 多条消息通知(堆叠展示)
|
||||
static notifyList(type: 'primary' | 'success' | 'danger' | 'warning', messages: string[], duration: number = 3000) {
|
||||
static NotifyList(type: 'primary' | 'success' | 'danger' | 'warning', messages: string[], duration: number = 3000) {
|
||||
if (typeof window === 'undefined') return
|
||||
if (!messages || messages.length === 0) return
|
||||
|
||||
|
||||
5
Web/src/extends/PageExtend.ts
Normal file
5
Web/src/extends/PageExtend.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class PageExtend {
|
||||
public static Redirect(route: string) {
|
||||
navigateTo(route, { replace: true })
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* 加密解密工具类(支持实例化)
|
||||
* 提供Base64加密解密、AES加解密等功能
|
||||
* 整合原 utils/base64.ts 的编解码方法
|
||||
*/
|
||||
export class CryptoEXTEND {
|
||||
// 默认加密密钥
|
||||
private static readonly DEFAULT_KEY = 'kx-ui-framework-key'
|
||||
|
||||
// 实例密钥
|
||||
private key: string
|
||||
|
||||
/**
|
||||
* 构造函数:初始化加密密钥
|
||||
* @param key 加密密钥(不同实例可传入不同密钥)
|
||||
*/
|
||||
constructor(key?: string) {
|
||||
this.key = key || CryptoEXTEND.DEFAULT_KEY
|
||||
}
|
||||
|
||||
// ========== Base64 编解码(原 utils/base64.ts 整合) ==========
|
||||
|
||||
/**
|
||||
* Base64编码
|
||||
* @param str 要编码的字符串
|
||||
* @returns 编码后的Base64字符串
|
||||
*/
|
||||
static encodeBase64(str: string): string {
|
||||
if (!str) return ''
|
||||
try {
|
||||
return btoa(encodeURIComponent(str))
|
||||
} catch (error) {
|
||||
console.error('Base64编码失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64解码
|
||||
* @param base64 要解码的Base64字符串
|
||||
* @returns 解码后的原始字符串
|
||||
*/
|
||||
static decodeBase64(base64: string): string {
|
||||
if (!base64) return ''
|
||||
try {
|
||||
return decodeURIComponent(atob(base64))
|
||||
} catch (error) {
|
||||
console.error('Base64解码失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的Base64编码
|
||||
* @param str 要编码的字符串
|
||||
* @returns 编码后的Base64字符串
|
||||
*/
|
||||
static encodeBase64URLSafe(str: string): string {
|
||||
return CryptoEXTEND.encodeBase64(str)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* URL安全的Base64解码
|
||||
* @param base64 要解码的Base64字符串
|
||||
* @returns 解码后的原始字符串
|
||||
*/
|
||||
static decodeBase64URLSafe(base64: string): string {
|
||||
let str = base64.replace(/-/g, '+').replace(/_/g, '/')
|
||||
// 补齐等号
|
||||
while (str.length % 4) {
|
||||
str += '='
|
||||
}
|
||||
return CryptoEXTEND.decodeBase64(str)
|
||||
}
|
||||
|
||||
// ========== 实例化编解码方法 ==========
|
||||
|
||||
/**
|
||||
* Base64加密字符串(实例方法)
|
||||
* @param data 需要加密的字符串
|
||||
* @returns 加密后的Base64字符串
|
||||
*/
|
||||
encryptBase64(data: string): string {
|
||||
return CryptoEXTEND.encodeBase64(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64解密字符串(实例方法)
|
||||
* @param data 需要解密的Base64字符串
|
||||
* @returns 解密后的原始字符串
|
||||
*/
|
||||
decryptBase64(data: string): string {
|
||||
return CryptoEXTEND.decodeBase64(data)
|
||||
}
|
||||
|
||||
// ========== 密钥相关编解码 ==========
|
||||
|
||||
/**
|
||||
* 加密字符串(Base64 + 密钥拼接)
|
||||
* @param data 需要加密的字符串
|
||||
* @returns 加密后的字符串
|
||||
*/
|
||||
encrypt(data: string): string {
|
||||
if (!data) return ''
|
||||
try {
|
||||
const encryptStr = data + this.key
|
||||
return btoa(encodeURIComponent(encryptStr))
|
||||
} catch (error) {
|
||||
console.error('加密失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param data 需要解密的字符串
|
||||
* @returns 解密后的原始字符串
|
||||
*/
|
||||
decrypt(data: string): string {
|
||||
if (!data) return ''
|
||||
try {
|
||||
const decryptStr = decodeURIComponent(atob(data))
|
||||
return decryptStr.replace(this.key, '')
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证加密字符串是否有效(匹配当前密钥)
|
||||
* @param data 加密后的字符串
|
||||
* @returns boolean 有效返回true,否则返回false
|
||||
*/
|
||||
validate(data: string): boolean {
|
||||
try {
|
||||
const decryptStr = this.decrypt(data)
|
||||
return decryptStr !== data && decryptStr.length > 0
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 静态工具方法 ==========
|
||||
|
||||
/**
|
||||
* MD5加密(简化版,实际项目建议使用crypto-js)
|
||||
* @param data 需要加密的字符串
|
||||
* @returns MD5哈希值
|
||||
*/
|
||||
static md5(data: string): string {
|
||||
if (!data) return ''
|
||||
// 简化实现,实际项目中建议使用专业的MD5库
|
||||
let hash = 0
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const char = data.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash).toString(16)
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256加密
|
||||
* @param data 需要加密的字符串
|
||||
* @returns SHA256哈希值
|
||||
*/
|
||||
static sha256(data: string): Promise<string> {
|
||||
if (!data) return Promise.resolve('')
|
||||
// 使用Web Crypto API
|
||||
const encoder = new TextEncoder()
|
||||
const dataBuffer = encoder.encode(data)
|
||||
return crypto.subtle.digest('SHA-256', dataBuffer).then(hash => {
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param length 字符串长度
|
||||
* @returns 随机字符串
|
||||
*/
|
||||
static randomString(length: number = 32): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数字
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @returns 随机数字
|
||||
*/
|
||||
static randomNumber(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* URL参数编码
|
||||
* @param data 需要编码的数据对象
|
||||
* @returns 编码后的URL参数字符串
|
||||
*/
|
||||
static urlEncode(data: Record<string, any>): string {
|
||||
const params = new URLSearchParams()
|
||||
for (const key in data) {
|
||||
if (data[key] !== undefined && data[key] !== null) {
|
||||
params.append(key, String(data[key]))
|
||||
}
|
||||
}
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* URL参数解码
|
||||
* @param query URL参数字符串
|
||||
* @returns 解码后的数据对象
|
||||
*/
|
||||
static urlDecode(query: string): Record<string, string> {
|
||||
const params = new URLSearchParams(query)
|
||||
const result: Record<string, string> = {}
|
||||
params.forEach((value, key) => {
|
||||
result[key] = value
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* 日期处理工具类(静态工具)
|
||||
* 提供日期格式化、日期差计算等功能
|
||||
*/
|
||||
export class DateEXTEND {
|
||||
/**
|
||||
* 格式化时间戳为本地时间字符串
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns 格式化后的时间字符串(如:2026-04-09 23:59:59)
|
||||
*/
|
||||
static format(timestamp: number | Date): string {
|
||||
if (!timestamp) return ''
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const second = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期(仅日期部分)
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns 格式化后的日期字符串(如:2026-04-09)
|
||||
*/
|
||||
static formatDate(timestamp: number | Date): string {
|
||||
if (!timestamp) return ''
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间(仅时间部分)
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns 格式化后的时间字符串(如:23:59:59)
|
||||
*/
|
||||
static formatTime(timestamp: number | Date): string {
|
||||
if (!timestamp) return ''
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const second = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳的天数差
|
||||
* @param start 开始时间戳
|
||||
* @param end 结束时间戳
|
||||
* @returns 天数差(正数:end在start之后;负数:end在start之前)
|
||||
*/
|
||||
static diffDay(start: number, end: number): number {
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
return Math.floor((end - start) / oneDay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳的小时差
|
||||
* @param start 开始时间戳
|
||||
* @param end 结束时间戳
|
||||
* @returns 小时差
|
||||
*/
|
||||
static diffHour(start: number, end: number): number {
|
||||
const oneHour = 1000 * 60 * 60
|
||||
return Math.floor((end - start) / oneHour)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳的分钟差
|
||||
* @param start 开始时间戳
|
||||
* @param end 结束时间戳
|
||||
* @returns 分钟差
|
||||
*/
|
||||
static diffMinute(start: number, end: number): number {
|
||||
const oneMinute = 1000 * 60
|
||||
return Math.floor((end - start) / oneMinute)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为今天
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns boolean 是今天返回true,否则返回false
|
||||
*/
|
||||
static isToday(timestamp: number | Date): boolean {
|
||||
const today = new Date()
|
||||
const target = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||
return (
|
||||
today.getFullYear() === target.getFullYear() &&
|
||||
today.getMonth() === target.getMonth() &&
|
||||
today.getDate() === target.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为昨天
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns boolean 是昨天返回true,否则返回false
|
||||
*/
|
||||
static isYesterday(timestamp: number | Date): boolean {
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const target = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||
return (
|
||||
yesterday.getFullYear() === target.getFullYear() &&
|
||||
yesterday.getMonth() === target.getMonth() &&
|
||||
yesterday.getDate() === target.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能时间显示(聊天场景使用)
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns 智能时间字符串
|
||||
*/
|
||||
static smartFormat(timestamp: number | Date): string {
|
||||
const now = Date.now()
|
||||
const target = timestamp instanceof Date ? timestamp.getTime() : timestamp
|
||||
const diff = now - target
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60 * 1000) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`
|
||||
}
|
||||
|
||||
// 小于24小时
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
|
||||
}
|
||||
|
||||
// 今天是显示时间
|
||||
if (this.isToday(target)) {
|
||||
return this.formatTime(target)
|
||||
}
|
||||
|
||||
// 昨天显示昨天 + 时间
|
||||
if (this.isYesterday(target)) {
|
||||
return `昨天 ${this.formatTime(target)}`
|
||||
}
|
||||
|
||||
// 其他显示日期时间
|
||||
return this.format(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳(毫秒)
|
||||
* @returns 当前时间戳
|
||||
*/
|
||||
static now(): number {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天开始的时间戳
|
||||
* @returns 今天开始的时间戳
|
||||
*/
|
||||
static getTodayStart(): number {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
return today.getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今天结束的时间戳
|
||||
* @returns 今天结束的时间戳
|
||||
*/
|
||||
static getTodayEnd(): number {
|
||||
const today = new Date()
|
||||
today.setHours(23, 59, 59, 999)
|
||||
return today.getTime()
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
/**
|
||||
* 表单校验工具类(静态工具)
|
||||
* 提供手机号、邮箱、身份证等常用校验功能
|
||||
* 整合原 utils/regex.ts 正则常量与通用校验方法
|
||||
*/
|
||||
|
||||
// 常用正则表达式常量(原 utils/regex.ts)
|
||||
export const Regexp = {
|
||||
// 手机号(中国大陆)
|
||||
phone: /^1[3-9]\d{9}$/,
|
||||
|
||||
// 邮箱
|
||||
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||
|
||||
// 身份证号(中国大陆)
|
||||
idCard: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$/)/,
|
||||
|
||||
// URL
|
||||
url: /^https?:\/\/([\w.-]+\.)+[\w.-]+(\/[\w.-]*)*(\?[\w=&.-]*)?$/,
|
||||
|
||||
// IP地址
|
||||
ip: /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||
|
||||
// 邮政编码(中国大陆)
|
||||
postalCode: /^[1-9]\d{5}$/,
|
||||
|
||||
// 用户名(字母开头,允许字母数字下划线)
|
||||
username: /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/,
|
||||
|
||||
// 密码(8-20位,包含字母和数字)
|
||||
password: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$/,
|
||||
|
||||
// 强密码(8-20位,包含大小写字母和数字)
|
||||
strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/,
|
||||
|
||||
// 金额(正数,最多两位小数)
|
||||
amount: /^[0-9]+(\.[0-9]{1,2})?$/,
|
||||
|
||||
// 中文名(2-10个中文字符)
|
||||
chineseName: /^[\u4e00-\u9fa5]{2,10}$/,
|
||||
|
||||
// QQ号
|
||||
qq: /^[1-9]\d{4,10}$/,
|
||||
|
||||
// 微信号
|
||||
wechat: /^[a-zA-Z][a-zA-Z0-9_-]{5,19}$/,
|
||||
|
||||
// 银行卡号(16或19位数字)
|
||||
bankCard: /^\d{16}|\d{19}$/,
|
||||
|
||||
// 座机电话(中国大陆)
|
||||
landline: /^(0\d{2,3}-?)?\d{7,8}$/,
|
||||
|
||||
// 车牌号(中国大陆)
|
||||
carPlate: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]$/,
|
||||
|
||||
// 整数
|
||||
integer: /^-?\d+$/,
|
||||
|
||||
// 正整数
|
||||
positiveInteger: /^[1-9]\d*$/,
|
||||
|
||||
// 负整数
|
||||
negativeInteger: /^-[1-9]\d*$/,
|
||||
|
||||
// 浮点数
|
||||
float: /^-?\d+(\.\d+)?$/,
|
||||
|
||||
// 正浮点数
|
||||
positiveFloat: /^[1-9]\d*(\.\d+)?$|^0\.\d+$/,
|
||||
|
||||
// 颜色值(hex)
|
||||
hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||
|
||||
// 日期格式(yyyy-mm-dd)
|
||||
date: /^\d{4}-\d{2}-\d{2}$/,
|
||||
|
||||
// 时间格式(HH:mm:ss)
|
||||
time: /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/,
|
||||
|
||||
// 日期时间格式(yyyy-mm-dd HH:mm:ss)
|
||||
dateTime: /^\d{4}-\d{2}-\d{2} ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/,
|
||||
|
||||
// IPv6地址(简化)
|
||||
ipv6: /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/,
|
||||
|
||||
// Mac地址
|
||||
mac: /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/,
|
||||
|
||||
// 端口号
|
||||
port: /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/,
|
||||
|
||||
// 16进制
|
||||
hex: /^[0-9A-Fa-f]+$/
|
||||
}
|
||||
|
||||
export class ValidateEXTEND {
|
||||
/**
|
||||
* 校验手机号(中国大陆)
|
||||
* @param phone 手机号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isPhone(phone: string): boolean {
|
||||
if (!phone) return false
|
||||
return Regexp.phone.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮箱
|
||||
* @param email 邮箱地址
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isEmail(email: string): boolean {
|
||||
if (!email) return false
|
||||
return Regexp.email.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验身份证号(中国大陆)
|
||||
* @param idCard 身份证号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isIdCard(idCard: string): boolean {
|
||||
if (!idCard) return false
|
||||
return Regexp.idCard.test(idCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验URL
|
||||
* @param url URL地址
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isUrl(url: string): boolean {
|
||||
if (!url) return false
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验IP地址
|
||||
* @param ip IP地址
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isIP(ip: string): boolean {
|
||||
if (!ip) return false
|
||||
return Regexp.ip.test(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮政编码(中国大陆)
|
||||
* @param code 邮政编码
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isPostalCode(code: string): boolean {
|
||||
if (!code) return false
|
||||
return Regexp.postalCode.test(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验车牌号(中国大陆)
|
||||
* @param plate 车牌号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isCarPlate(plate: string): boolean {
|
||||
if (!plate) return false
|
||||
return Regexp.carPlate.test(plate)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户名(字母开头,允许字母数字下划线)
|
||||
* @param username 用户名
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isUsername(username: string): boolean {
|
||||
if (!username) return false
|
||||
return Regexp.username.test(username)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码强度(8-20位,包含字母和数字)
|
||||
* @param password 密码
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isPassword(password: string): boolean {
|
||||
if (!password) return false
|
||||
return Regexp.password.test(password)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验强密码(8-20位,包含大小写字母和数字)
|
||||
* @param password 密码
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isStrongPassword(password: string): boolean {
|
||||
if (!password) return false
|
||||
return Regexp.strongPassword.test(password)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验金额(正数,最多两位小数)
|
||||
* @param amount 金额
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isAmount(amount: string | number): boolean {
|
||||
if (amount === '' || amount === null || amount === undefined) return false
|
||||
return Regexp.amount.test(String(amount))
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验中文名(2-10个中文字符)
|
||||
* @param name 姓名
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isChineseName(name: string): boolean {
|
||||
if (!name) return false
|
||||
return Regexp.chineseName.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验QQ号
|
||||
* @param qq QQ号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isQQ(qq: string): boolean {
|
||||
if (!qq) return false
|
||||
return Regexp.qq.test(qq)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验微信号
|
||||
* @param wechat 微信号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isWeChat(wechat: string): boolean {
|
||||
if (!wechat) return false
|
||||
return Regexp.wechat.test(wechat)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验银行卡号(简单校验Luhn算法)
|
||||
* @param cardNo 银行卡号
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isBankCard(cardNo: string): boolean {
|
||||
if (!cardNo) return false
|
||||
// 去除空格
|
||||
const card = cardNo.replace(/\s/g, '')
|
||||
// 只能是16或19位数字
|
||||
if (!/^\d{16}|\d{19}$/.test(card)) return false
|
||||
// Luhn算法校验
|
||||
let sum = 0
|
||||
let isEven = false
|
||||
for (let i = card.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(card.charAt(i))
|
||||
if (isEven) {
|
||||
digit *= 2
|
||||
if (digit > 9) {
|
||||
digit -= 9
|
||||
}
|
||||
}
|
||||
sum += digit
|
||||
isEven = !isEven
|
||||
}
|
||||
return sum % 10 === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验座机电话(中国大陆)
|
||||
* @param phone 座机电话
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isLandline(phone: string): boolean {
|
||||
if (!phone) return false
|
||||
return Regexp.landline.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验非空
|
||||
* @param value 值
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static isRequired(value: any): boolean {
|
||||
if (value === null || value === undefined) return false
|
||||
if (typeof value === 'string') return value.trim().length > 0
|
||||
if (Array.isArray(value)) return value.length > 0
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验最小长度
|
||||
* @param value 值
|
||||
* @param min 最小长度
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static minLength(value: string, min: number): boolean {
|
||||
if (!value) return false
|
||||
return value.trim().length >= min
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验最大长度
|
||||
* @param value 值
|
||||
* @param max 最大长度
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static maxLength(value: string, max: number): boolean {
|
||||
if (!value) return false
|
||||
return value.trim().length <= max
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验范围
|
||||
* @param value 值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static inRange(value: number, min: number, max: number): boolean {
|
||||
if (value === null || value === undefined) return false
|
||||
return value >= min && value <= max
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验数组长度范围
|
||||
* @param value 数组
|
||||
* @param min 最小长度
|
||||
* @param max 最大长度
|
||||
* @returns boolean 是否有效
|
||||
*/
|
||||
static arrayLengthInRange(value: any[], min: number, max: number): boolean {
|
||||
if (!Array.isArray(value)) return false
|
||||
return value.length >= min && value.length <= max
|
||||
}
|
||||
|
||||
// ========== 以下为原 utils/regex.ts 通用方法 ==========
|
||||
|
||||
/**
|
||||
* 校验字符串是否符合指定正则表达式
|
||||
* @param value 要校验的值
|
||||
* @param pattern 正则表达式
|
||||
* @returns boolean 是否匹配
|
||||
*/
|
||||
static test(value: string, pattern: RegExp): boolean {
|
||||
if (!value) return false
|
||||
return pattern.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字符串中提取匹配的内容
|
||||
* @param str 要匹配的字符串
|
||||
* @param pattern 正则表达式
|
||||
* @param group 分组索引(可选)
|
||||
* @returns 匹配的结果数组
|
||||
*/
|
||||
static match(str: string, pattern: RegExp, group?: number): string | string[] | null {
|
||||
const result = str.match(pattern)
|
||||
if (!result) return null
|
||||
if (group !== undefined) {
|
||||
return result[group] || null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换字符串中匹配的内容
|
||||
* @param str 要处理的字符串
|
||||
* @param pattern 正则表达式
|
||||
* @param replacement 替换内容
|
||||
* @returns 替换后的字符串
|
||||
*/
|
||||
static replace(str: string, pattern: RegExp, replacement: string): string {
|
||||
return str.replace(pattern, replacement)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割字符串
|
||||
* @param str 要处理的字符串
|
||||
* @param pattern 正则表达式或分隔符
|
||||
* @returns 分割后的字符串数组
|
||||
*/
|
||||
static split(str: string, pattern: RegExp | string): string[] {
|
||||
return str.split(pattern)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IUserInfo } from '~/types/user'
|
||||
|
||||
definePageMeta({
|
||||
layout: layout.empty
|
||||
})
|
||||
@@ -121,6 +123,8 @@ definePageMeta({
|
||||
type ErrorField = 'username' | 'password' | 'agreement'
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const { emit } = useEventBus()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
@@ -194,6 +198,23 @@ const handleSubmit = async () => {
|
||||
try {
|
||||
isSubmitting.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 900))
|
||||
|
||||
const loginAt = new Date().toISOString()
|
||||
const mockUserInfo: IUserInfo = {
|
||||
id: Date.now(),
|
||||
username: form.username.trim(),
|
||||
nickname: form.username.trim(),
|
||||
role: 'user'
|
||||
}
|
||||
|
||||
userStore.setUserInfo(mockUserInfo, `demo-token-${mockUserInfo.id}`)
|
||||
emit('auth:login', {
|
||||
userId: mockUserInfo.id || 0,
|
||||
username: mockUserInfo.username || '',
|
||||
nickname: mockUserInfo.nickname || '',
|
||||
loginAt
|
||||
})
|
||||
|
||||
showSuccessToast('登录成功')
|
||||
await navigateTo('/home')
|
||||
} finally {
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
<p class="eyebrow">SignalR IM Demo</p>
|
||||
<h1>即时通讯测试页</h1>
|
||||
<p class="hero-text">
|
||||
当前已对接服务端 `ChatHub`,默认调用 `SendMessage(user, message)`,监听 `ReceiveMessage`、
|
||||
`UserConnected` 和 `UserDisconnected`。
|
||||
当前页面会连接服务端 `ChatHub`,默认调用 `SendMessage(user, message)`,并监听 `ReceiveMessage`、`UserConnected`、`UserDisconnected`。收到聊天消息时会同步发出全局事件 `chat:received`。
|
||||
</p>
|
||||
</div>
|
||||
<div class="status-box">
|
||||
<span class="status-label">连接状态</span>
|
||||
<strong :class="['status-pill', `is-${status}`]">{{ statusText }}</strong>
|
||||
<span v-if="connectionId" class="connection-id">连接ID:{{ connectionId }}</span>
|
||||
<span v-if="connectionId" class="connection-id">连接 ID:{{ connectionId }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -34,7 +33,7 @@
|
||||
<textarea
|
||||
v-model.trim="accessToken"
|
||||
rows="3"
|
||||
placeholder="如后端开启鉴权,可在这里粘贴 token;为空则匿名连接。"
|
||||
placeholder="如果服务端开启鉴权,可以在这里粘贴 token;为空则匿名连接。"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -48,7 +47,7 @@
|
||||
<section class="panel">
|
||||
<div class="panel-title">消息面板</div>
|
||||
<div ref="messageListRef" class="message-list">
|
||||
<div v-if="!messages.length" class="empty-state">还没有消息,先连接然后发送一条试试。</div>
|
||||
<div v-if="!messages.length" class="empty-state">还没有消息,先连接再发一条试试。</div>
|
||||
<article
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
@@ -113,6 +112,7 @@ interface LogItem {
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { emit } = useEventBus()
|
||||
|
||||
const defaultHubUrl = `${BaseConfig.BaseUrl.replace(/\/$/, '')}/chatHub`
|
||||
|
||||
@@ -193,7 +193,16 @@ const resolveHubUrl = (value: string) => {
|
||||
|
||||
const registerConnectionEvents = (hub: HubConnection) => {
|
||||
hub.on('ReceiveMessage', (user: string, message: string) => {
|
||||
addMessage(user || '匿名用户', message || '')
|
||||
const sender = user || '匿名用户'
|
||||
const text = message || ''
|
||||
|
||||
addMessage(sender, text)
|
||||
emit('chat:received', {
|
||||
user: sender,
|
||||
message: text,
|
||||
receivedAt: new Date().toISOString(),
|
||||
type: 'chat'
|
||||
})
|
||||
})
|
||||
|
||||
hub.on('UserConnected', (id: string) => {
|
||||
@@ -214,7 +223,7 @@ const registerConnectionEvents = (hub: HubConnection) => {
|
||||
hub.onreconnected((id) => {
|
||||
status.value = 'connected'
|
||||
connectionId.value = id || hub.connectionId || ''
|
||||
addLog(`重连成功:${connectionId.value || '无连接ID'}`)
|
||||
addLog(`重连成功:${connectionId.value || '无连接 ID'}`)
|
||||
})
|
||||
|
||||
hub.onclose((error) => {
|
||||
@@ -265,7 +274,7 @@ const connectHub = async () => {
|
||||
connectionId.value = hub.connectionId || ''
|
||||
status.value = 'connected'
|
||||
|
||||
addLog(`连接成功:${connectionId.value || '未返回连接ID'}`)
|
||||
addLog(`连接成功:${connectionId.value || '未返回连接 ID'}`)
|
||||
addMessage('系统', 'SignalR 已连接,可以开始发送消息。', 'system')
|
||||
} catch (error) {
|
||||
status.value = 'error'
|
||||
@@ -389,6 +398,7 @@ h1 {
|
||||
max-width: 640px;
|
||||
color: #665246;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.status-box {
|
||||
|
||||
@@ -2,14 +2,44 @@
|
||||
<div class="page-home">
|
||||
<section class="home-card">
|
||||
<p class="page-tag">Home</p>
|
||||
<h1>前端调试入口</h1>
|
||||
<h1>页面事件总线</h1>
|
||||
<p class="page-desc">
|
||||
这里先放一个即时通讯测试页入口,方便直接验证 SignalR 连接、收发消息和连接状态。
|
||||
这一版只保留最小能力:页面自己定义事件名,自己决定 payload 结构,通过 `on / emit / off / clear` 完成页面间监听和订阅。
|
||||
</p>
|
||||
|
||||
<NuxtLink class="entry-link" to="/home/im">
|
||||
进入 IM Demo
|
||||
</NuxtLink>
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<strong>自定义事件名</strong>
|
||||
<span>例如 `auth:login`、`chat:received`、`user:refresh`</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<strong>自定义数据</strong>
|
||||
<span>payload 不做预设,页面按业务自己约定</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<strong>自动解绑</strong>
|
||||
<span>`useEventBus().on()` 在页面销毁时会自动取消监听</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-card">
|
||||
<pre><code>const { on, emit } = useEventBus()
|
||||
|
||||
on('user:refresh', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
|
||||
emit('user:refresh', { id: 1 })</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="entry-row">
|
||||
<NuxtLink class="entry-link" to="/auth/login">
|
||||
前往登录页
|
||||
</NuxtLink>
|
||||
<NuxtLink class="entry-link secondary" to="/home/im">
|
||||
进入 IM Demo
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +48,26 @@
|
||||
definePageMeta({
|
||||
layout: layout.default
|
||||
})
|
||||
|
||||
interface IUserRefreshPayload {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const { on, emit } = useEventBus()
|
||||
|
||||
// EventBus 使用示例:订阅自定义事件
|
||||
on<IUserRefreshPayload>('user:refresh', (payload) => {
|
||||
console.log('收到 user:refresh 事件', payload)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// EventBus 使用示例:派发自定义事件
|
||||
emit<IUserRefreshPayload>('user:refresh', {
|
||||
id: 1,
|
||||
name: 'captain'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -25,7 +75,11 @@ definePageMeta({
|
||||
.page-home div,
|
||||
.page-home section,
|
||||
.page-home p,
|
||||
.page-home h1 {
|
||||
.page-home h1,
|
||||
.page-home strong,
|
||||
.page-home span,
|
||||
.page-home pre,
|
||||
.page-home code {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -38,7 +92,7 @@ definePageMeta({
|
||||
}
|
||||
|
||||
.home-card {
|
||||
width: min(720px, 100%);
|
||||
width: min(760px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
border-radius: 24px;
|
||||
@@ -62,8 +116,54 @@ h1 {
|
||||
|
||||
.page-desc {
|
||||
margin-top: 12px;
|
||||
color: #665246;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: #665246;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.feature-item,
|
||||
.code-card {
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(98, 77, 65, 0.12);
|
||||
}
|
||||
|
||||
.feature-item strong {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
color: #2a221d;
|
||||
}
|
||||
|
||||
.feature-item span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
color: #665246;
|
||||
}
|
||||
|
||||
.code-card {
|
||||
margin-top: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-card code {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #3f3129;
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.entry-link {
|
||||
@@ -71,12 +171,19 @@ h1 {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 140px;
|
||||
margin-top: 20px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.entry-link {
|
||||
background: linear-gradient(135deg, #fe5e08 0%, #d9534f 100%);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.entry-link.secondary {
|
||||
background: rgba(89, 70, 59, 0.08);
|
||||
color: #4a3a31;
|
||||
}
|
||||
|
||||
.entry-link:hover,
|
||||
@@ -85,4 +192,10 @@ h1 {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #db5208 0%, #bf4744 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,16 +91,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
// MessageExtend.showToast('success', '更新成功!')
|
||||
MessageExtend.notifyList('primary', ['获取装备'])
|
||||
|
||||
|
||||
//MessageExtend.ShowToast('success', '更新成功!')
|
||||
//MessageExtend.NotifyList("success", ['获取装备',"获取物品"])
|
||||
definePageMeta({
|
||||
layout: layout.empty
|
||||
})
|
||||
showNotify({ message: '提示' });
|
||||
// await navigateTo('/auth/login', { replace: true })
|
||||
onMounted(() => {
|
||||
req();
|
||||
alert(1);
|
||||
onMounted(async () => {
|
||||
|
||||
const test = await LoginService.Test("dddd","dddd2");
|
||||
console.log(test);
|
||||
|
||||
//alert(1);
|
||||
})
|
||||
</script>
|
||||
|
||||
23
Web/src/services/Index/TestService.ts
Normal file
23
Web/src/services/Index/TestService.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export class LoginService {
|
||||
/**
|
||||
* 登录接口
|
||||
* POST /Login/Login
|
||||
* 请求体: 登录请求参数
|
||||
* @param name 登录名/手机号
|
||||
* @param pwd 密码
|
||||
* @param code 验证码
|
||||
*/
|
||||
static async Login(name: string, pwd: string, code: string) {
|
||||
return ApiService.Request("post", "/Login/Login", { name, pwd, code });
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试接口
|
||||
* GET /Login/Test
|
||||
* @param name 测试名
|
||||
* @param ttt 测试2
|
||||
*/
|
||||
static async Test(name: string, ttt: string) {
|
||||
return ApiService.Request("get", "/Login/Test", { name, ttt });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface IUserInfo {
|
||||
id?: number
|
||||
username?: string
|
||||
nickname?: string
|
||||
avatar?: string
|
||||
role?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
Reference in New Issue
Block a user