This commit is contained in:
Putoo
2026-04-26 18:53:56 +08:00
parent 2c6c62e88c
commit 4463f9e810
20 changed files with 461 additions and 1035 deletions

View File

@@ -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; }
}
}

View File

@@ -25,11 +25,16 @@ namespace Application.Web.Controllers.Login
[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", "系统");

View File

@@ -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; }
}
}

View File

@@ -26,8 +26,8 @@ export default defineNuxtConfig({
'stores',
'composables',
'extends',
'services',
"model"
'services/**',
"model/**"
]
},

View File

@@ -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>

View File

@@ -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 = {}

View File

@@ -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
}
}

View 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
}
}

View 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)
}
}

View File

@@ -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

View File

@@ -0,0 +1,5 @@
export class PageExtend {
public static Redirect(route: string) {
navigateTo(route, { replace: true })
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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">
<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) =&gt; {
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>

View File

@@ -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>

View 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 });
}
}

View File

@@ -1,8 +0,0 @@
export interface IUserInfo {
id?: number
username?: string
nickname?: string
avatar?: string
role?: string
[key: string]: unknown
}