Browse Source

-init:初始化项目

Diamond_ore 2 weeks ago
commit
5e26db4cff

+ 2 - 0
.env.development

@@ -0,0 +1,2 @@
+VITE_API_BASE_URL=http://localhost:8000
+VITE_APP_TITLE=SSO认证中心

+ 605 - 0
.gitattributes

@@ -0,0 +1,605 @@
+# auto generated by UGit 
+*.exe filter=lfs diff=lfs merge=binary -text
+
+# 视频,video
+# oggtheora
+*.[oO][gG][gG][tT][hH][eE][oO][rR][aA] filter=lfs diff=lfs merge=binary -text
+# m2t
+*.[mM]2[tT] filter=lfs diff=lfs merge=binary -text
+# mts
+*.[mM][tT][sS] filter=lfs diff=lfs merge=binary -text
+# mp4
+*.[mM][pP]4 filter=lfs diff=lfs merge=binary -text
+# avi
+*.[aA][vV][iI] filter=lfs diff=lfs merge=binary -text
+# mkv
+*.[mM][kK][vV] filter=lfs diff=lfs merge=binary -text
+# wmv
+*.[wW][mM][vV] filter=lfs diff=lfs merge=binary -text
+# asf
+*.[aA][sS][fF] filter=lfs diff=lfs merge=binary -text
+# asx
+*.[aA][sS][xX] filter=lfs diff=lfs merge=binary -text
+# rm
+*.[rR][mM] filter=lfs diff=lfs merge=binary -text
+# rmvb
+*.[rR][mM][vV][bB] filter=lfs diff=lfs merge=binary -text
+# 3gp
+*.3[gG][pP] filter=lfs diff=lfs merge=binary -text
+# 3gpp
+*.3[gG][pP][pP] filter=lfs diff=lfs merge=binary -text
+# 3gpp2
+*.3[gG][pP][pP]2 filter=lfs diff=lfs merge=binary -text
+# mov
+*.[mM][oO][vV] filter=lfs diff=lfs merge=binary -text
+# m4v
+*.[mM]4[vV] filter=lfs diff=lfs merge=binary -text
+# dat
+*.[dD][aA][tT] filter=lfs diff=lfs merge=binary -text
+# vob
+*.[vV][oO][bB] filter=lfs diff=lfs merge=binary -text
+# dv
+*.[dD][vV] filter=lfs diff=lfs merge=binary -text
+# mpeg
+*.[mM][pP][eE][gG] filter=lfs diff=lfs merge=binary -text
+# mpg
+*.[mM][pP][gG] filter=lfs diff=lfs merge=binary -text
+# mpe
+*.[mM][pP][eE] filter=lfs diff=lfs merge=binary -text
+# m2v
+*.[mM]2[vV] filter=lfs diff=lfs merge=binary -text
+# webm
+*.[wW][eE][bB][mM] filter=lfs diff=lfs merge=binary -text
+# flv
+*.[fF][lL][vV] filter=lfs diff=lfs merge=binary -text
+# swf
+*.[sS][wW][fF] filter=lfs diff=lfs merge=binary -text
+# avc
+*.[aA][vV][cC] filter=lfs diff=lfs merge=binary -text
+# arf
+*.[aA][rR][fF] filter=lfs diff=lfs merge=binary -text
+# vcr
+*.[vV][cC][rR] filter=lfs diff=lfs merge=binary -text
+# ogv
+*.[oO][gG][vV] filter=lfs diff=lfs merge=binary -text
+
+# 音频, audio
+# ape
+*.[aA][pP][eE] filter=lfs diff=lfs merge=binary -text
+# wav
+*.[wW][aA][vV] filter=lfs diff=lfs merge=binary -text
+# m4a
+*.[mM]4[aA] filter=lfs diff=lfs merge=binary -text
+# mp3
+*.[mM][pP]3 filter=lfs diff=lfs merge=binary -text
+# flac
+*.[fF][lL][aA][cC] filter=lfs diff=lfs merge=binary -text
+# aif
+*.[aA][iI][fF] filter=lfs diff=lfs merge=binary -text
+# aiff
+*.[aA][iI][fF][fF] filter=lfs diff=lfs merge=binary -text
+# aac
+*.[aA][aA][cC] filter=lfs diff=lfs merge=binary -text
+# cpa
+*.[cC][pP][aA] filter=lfs diff=lfs merge=binary -text
+# swa
+*.[sS][wW][aA] filter=lfs diff=lfs merge=binary -text
+# sesx
+*.[sS][eE][sS][xX] filter=lfs diff=lfs merge=binary -text
+# ses
+*.[sS][eE][sS] filter=lfs diff=lfs merge=binary -text
+# bnk, Wwise audio
+*.[bB][nN][kK] filter=lfs diff=lfs merge=binary -text
+# wem, Wwise
+*.[wW][eE][mM] filter=lfs diff=lfs merge=binary -text
+# pca
+*.[pP][cC][aA] filter=lfs diff=lfs merge=binary -text
+
+# 库文件,Library
+# a
+*.[aA] filter=lfs diff=lfs merge=binary -text
+# o
+*.[oO] filter=lfs diff=lfs merge=binary -text
+# so
+*.[sS][oO] filter=lfs diff=lfs merge=binary -text
+# lib
+*.[lL][iI][bB] filter=lfs diff=lfs merge=binary -text
+# dll
+*.[dD][lL][lL] filter=lfs diff=lfs merge=binary -text
+# lbr
+*.[lL][bB][rR] filter=lfs diff=lfs merge=binary -text
+# tlb
+*.[tT][lL][bB] filter=lfs diff=lfs merge=binary -text
+# cab
+*.[cC][aA][bB] filter=lfs diff=lfs merge=binary -text
+# dylib
+*.[dD][yY][lL][iI][bB] filter=lfs diff=lfs merge=binary -text
+# dsym
+*.[dD][sS][yY][mM] filter=lfs diff=lfs merge=binary -text
+# app
+*.[aA][pP][pP] filter=lfs diff=lfs merge=binary -text
+# ipa
+*.[iI][pP][aA] filter=lfs diff=lfs merge=binary -text
+# dmg
+*.[dD][mM][gG] filter=lfs diff=lfs merge=binary -text
+# exe
+*.[eE][xX][eE] filter=lfs diff=lfs merge=binary -text
+# pdb
+*.[pP][dD][bB] filter=lfs diff=lfs merge=binary -text
+# dbg
+*.[dD][bB][gG] filter=lfs diff=lfs merge=binary -text
+# run
+*.[rR][uU][nN] filter=lfs diff=lfs merge=binary -text
+# pyd
+*.[pP][yY][dD] filter=lfs diff=lfs merge=binary -text
+# pyc
+*.[pP][yY][cC] filter=lfs diff=lfs merge=binary -text
+# nupkg, NuGet package
+*.[nN][uU][pP][kK][gG] filter=lfs diff=lfs merge=binary -text
+# pch
+*.[pP][cC][hH] filter=lfs diff=lfs merge=binary -text
+# ilk
+*.[iI][lL][kK] filter=lfs diff=lfs merge=binary -text
+# debug
+*.[dD][eE][bB][uU][gG] filter=lfs diff=lfs merge=binary -text
+# obj
+*.[oO][bB][jJ] filter=lfs diff=lfs merge=binary -text
+# stub
+*.[sS][tT][uU][bB] filter=lfs diff=lfs merge=binary -text
+# ddp
+*.[dD][dD][pP] filter=lfs diff=lfs merge=binary -text
+# sym
+*.[sS][yY][mM] filter=lfs diff=lfs merge=binary -text
+# lld
+*.[lL][lL][dD] filter=lfs diff=lfs merge=binary -text
+# res
+*.[rR][eE][sS] filter=lfs diff=lfs merge=binary -text
+# locres
+*.[lL][oO][cC][rR][eE][sS] filter=lfs diff=lfs merge=binary -text
+# aar
+*.[aA][aA][rR] filter=lfs diff=lfs merge=binary -text
+# udd
+*.[uU][dD][dD] filter=lfs diff=lfs merge=binary -text
+# mdb
+*.[mM][dD][bB] filter=lfs diff=lfs merge=binary -text
+# ddc
+*.[dD][dD][cC] filter=lfs diff=lfs merge=binary -text
+# udn
+*.[uU][dD][nN] filter=lfs diff=lfs merge=binary -text
+# h5
+*.[hH]5 filter=lfs diff=lfs merge=binary -text
+
+
+
+# 压缩包,Archive format
+# =Archiving only=
+# ar
+*.[aA][rR] filter=lfs diff=lfs merge=binary -text
+# cpio
+*.[cC][pP][iI][oO] filter=lfs diff=lfs merge=binary -text
+# shar
+*.[sS][hH][aA][rR] filter=lfs diff=lfs merge=binary -text
+# tar
+*.[tT][aA][rR] filter=lfs diff=lfs merge=binary -text
+# lbr
+*.[lL][bB][rR] filter=lfs diff=lfs merge=binary -text
+# =Compression only=
+# Brotli
+*.[bB][rR][oO][tT][lL][iI] filter=lfs diff=lfs merge=binary -text
+# zip
+*.[zZ][iI][pP] filter=lfs diff=lfs merge=binary -text
+# bzip2
+*.[bB][zZ][iI][pP]2 filter=lfs diff=lfs merge=binary -text
+# compress
+*.[cC][oO][mM][pP][rR][eE][sS][sS] filter=lfs diff=lfs merge=binary -text
+# gzip
+*.[gG][zZ][iI][pP] filter=lfs diff=lfs merge=binary -text
+# zopfli
+*.[zZ][oO][pP][fF][lL][iI] filter=lfs diff=lfs merge=binary -text
+# LZMA
+*.[lL][zZ][mM][aA] filter=lfs diff=lfs merge=binary -text
+# LZ4
+*.[lL][zZ]4 filter=lfs diff=lfs merge=binary -text
+# lzip
+*.[lL][zZ][iI][pP] filter=lfs diff=lfs merge=binary -text
+# lzop
+*.[lL][zZ][oO][pP] filter=lfs diff=lfs merge=binary -text
+# SQ
+*.[sS][qQ] filter=lfs diff=lfs merge=binary -text
+# xz
+*.[xX][zZ] filter=lfs diff=lfs merge=binary -text
+# Zstandard
+*.[zZ][sS][tT][aA][nN][dD][aA][rR][dD] filter=lfs diff=lfs merge=binary -text
+# =Archiving and compression=
+# 7z
+*.7[zZ] filter=lfs diff=lfs merge=binary -text
+# ace
+*.[aA][cC][eE] filter=lfs diff=lfs merge=binary -text
+# arc
+*.[aA][rR][cC] filter=lfs diff=lfs merge=binary -text
+# arj
+*.[aA][rR][jJ] filter=lfs diff=lfs merge=binary -text
+# b1
+*.[bB]1 filter=lfs diff=lfs merge=binary -text
+# cabinet
+*.[cC][aA][bB][iI][nN][eE][tT] filter=lfs diff=lfs merge=binary -text
+# cfs
+*.[cC][fF][sS] filter=lfs diff=lfs merge=binary -text
+# cpt
+*.[cC][pP][tT] filter=lfs diff=lfs merge=binary -text
+# dar
+*.[dD][aA][rR] filter=lfs diff=lfs merge=binary -text
+# dgca
+*.[dD][gG][cC][aA] filter=lfs diff=lfs merge=binary -text
+# dmg
+*.[dD][mM][gG] filter=lfs diff=lfs merge=binary -text
+# egg
+*.[eE][gG][gG] filter=lfs diff=lfs merge=binary -text
+# kgb
+*.[kK][gG][bB] filter=lfs diff=lfs merge=binary -text
+# lha
+*.[lL][hH][aA] filter=lfs diff=lfs merge=binary -text
+# lzx
+*.[lL][zZ][xX] filter=lfs diff=lfs merge=binary -text
+# mpq
+*.[mM][pP][qQ] filter=lfs diff=lfs merge=binary -text
+# pea
+*.[pP][eE][aA] filter=lfs diff=lfs merge=binary -text
+# rar
+*.[rR][aA][rR] filter=lfs diff=lfs merge=binary -text
+# rzip
+*.[rR][zZ][iI][pP] filter=lfs diff=lfs merge=binary -text
+# sit
+*.[sS][iI][tT] filter=lfs diff=lfs merge=binary -text
+# sitx
+*.[sS][iI][tT][xX] filter=lfs diff=lfs merge=binary -text
+# sqx
+*.[sS][qQ][xX] filter=lfs diff=lfs merge=binary -text
+# uda
+*.[uU][dD][aA] filter=lfs diff=lfs merge=binary -text
+# xar
+*.[xX][aA][rR] filter=lfs diff=lfs merge=binary -text
+# zoo
+*.[zZ][oO][oO] filter=lfs diff=lfs merge=binary -text
+# zpaq
+*.[zZ][pP][aA][qQ] filter=lfs diff=lfs merge=binary -text
+# =Software packaging and distribution=
+# apk
+*.[aA][pP][kK] filter=lfs diff=lfs merge=binary -text
+# appx
+*.[aA][pP][pP][xX] filter=lfs diff=lfs merge=binary -text
+# deb
+*.[dD][eE][bB] filter=lfs diff=lfs merge=binary -text
+# rpm
+*.[rR][pP][mM] filter=lfs diff=lfs merge=binary -text
+# msi
+*.[mM][sS][iI] filter=lfs diff=lfs merge=binary -text
+# ipa
+*.[iI][pP][aA] filter=lfs diff=lfs merge=binary -text
+# jar
+*.[jJ][aA][rR] filter=lfs diff=lfs merge=binary -text
+# war
+*.[wW][aA][rR] filter=lfs diff=lfs merge=binary -text
+# ear
+*.[eE][aA][rR] filter=lfs diff=lfs merge=binary -text
+# xap
+*.[xX][aA][pP] filter=lfs diff=lfs merge=binary -text
+# xbap
+*.[xX][bB][aA][pP] filter=lfs diff=lfs merge=binary -text
+# hap
+*.[hH][aA][pP] filter=lfs diff=lfs merge=binary -text
+# app
+*.[aA][pP][pP] filter=lfs diff=lfs merge=binary -text
+# gz
+*.[gG][zZ] filter=lfs diff=lfs merge=binary -text
+# tgz
+*.[tT][gG][zZ] filter=lfs diff=lfs merge=binary -text
+# bz2
+*.[bB][zZ]2 filter=lfs diff=lfs merge=binary -text
+# z
+*.[zZ] filter=lfs diff=lfs merge=binary -text
+# pak
+*.[pP][aA][kK] filter=lfs diff=lfs merge=binary -text
+# archive
+*.[aA][rR][cC][hH][iI][vV][eE] filter=lfs diff=lfs merge=binary -text
+# vsix
+*.[vV][sS][iI][xX] filter=lfs diff=lfs merge=binary -text
+# disk image
+# iso
+*.[iI][sS][oO] filter=lfs diff=lfs merge=binary -text
+# bin
+*.[bB][iI][nN] filter=lfs diff=lfs merge=binary -text
+# cue
+*.[cC][uU][eE] filter=lfs diff=lfs merge=binary -text
+# raw
+*.[rR][aA][wW] filter=lfs diff=lfs merge=binary -text
+
+# Adobe
+# Photoshop
+# psd
+*.[pP][sS][dD] filter=lfs diff=lfs merge=binary -text
+# Illustrator
+# ai
+*.[aA][iI] filter=lfs diff=lfs merge=binary -text
+# eps
+*.[eE][pP][sS] filter=lfs diff=lfs merge=binary -text
+# pdf
+*.[pP][dD][fF] filter=lfs diff=lfs merge=binary -text
+
+# 原始图片,Raw image
+# cr2
+*.[cC][rR]2 filter=lfs diff=lfs merge=binary -text
+# crw
+*.[cC][rR][wW] filter=lfs diff=lfs merge=binary -text
+# nef
+*.[nN][eE][fF] filter=lfs diff=lfs merge=binary -text
+# nrw
+*.[nN][rR][wW] filter=lfs diff=lfs merge=binary -text
+# sr2
+*.[sS][rR]2 filter=lfs diff=lfs merge=binary -text
+# dng
+*.[dD][nN][gG] filter=lfs diff=lfs merge=binary -text
+# arw
+*.[aA][rR][wW] filter=lfs diff=lfs merge=binary -text
+# ort
+*.[oO][rR][fF] filter=lfs diff=lfs merge=binary -text
+# fbx
+*.[fF][bB][xX] filter=lfs diff=lfs merge=binary -text
+# 3ds
+*.3[dD][sS] filter=lfs diff=lfs merge=binary -text
+# xcf
+*.[xX][cC][fF] filter=lfs diff=lfs merge=binary -text
+# hdr
+*.[hH][dD][rR] filter=lfs diff=lfs merge=binary -text
+# duf
+*.[dD][uU][fF] filter=lfs diff=lfs merge=binary -text
+# mb, maya
+*.[mM][bB] filter=lfs diff=lfs merge=binary -text
+# cubemap,unity 贴图
+*.[cC][uU][bB][eE][mM][aA][pP] filter=lfs diff=lfs merge=binary -text
+# navmesh,unity
+*.[nN][aA][vV][mM][eE][sS][hH] filter=lfs diff=lfs merge=binary -text
+# osm,地理数据
+*.[oO][sS][mM] filter=lfs diff=lfs merge=binary -text
+# hip, houdini
+*.[hH][iI][pP] filter=lfs diff=lfs merge=binary -text
+# cdr
+*.[cC][dD][rR] filter=lfs diff=lfs merge=binary -text
+# raw
+*.[rR][aA][wW] filter=lfs diff=lfs merge=binary -text
+# dae
+*.[dD][aA][eE] filter=lfs diff=lfs merge=binary -text
+# hda, houdini
+*.[hH][dD][aA] filter=lfs diff=lfs merge=binary -text
+# geo, houdini
+*.[gG][eE][oO] filter=lfs diff=lfs merge=binary -text
+# bgeo, houdini
+*.[bB][gG][eE][oO] filter=lfs diff=lfs merge=binary -text
+# ma, 3dmax
+*.[mM][aA] filter=lfs diff=lfs merge=binary -text
+# max, 3dmax
+*.[mM][aA][xX] filter=lfs diff=lfs merge=binary -text
+# 3dm, 3d模型
+*.3[dD][mM] filter=lfs diff=lfs merge=binary -text
+# blend
+*.[bB][lL][eE][nN][dD] filter=lfs diff=lfs merge=binary -text
+# c4d
+*.[cC]4[dD] filter=lfs diff=lfs merge=binary -text
+# collada
+*.[cC][oO][lL][lL][aA][dD][aA] filter=lfs diff=lfs merge=binary -text
+# dxf
+*.[dD][xX][fF] filter=lfs diff=lfs merge=binary -text
+# jas
+*.[jJ][aA][sS] filter=lfs diff=lfs merge=binary -text
+# lws
+*.[lL][wW][sS] filter=lfs diff=lfs merge=binary -text
+# lxo
+*.[lL][xX][oO] filter=lfs diff=lfs merge=binary -text
+# ply
+*.[pP][lL][yY] filter=lfs diff=lfs merge=binary -text
+# skp
+*.[sS][kK][pP] filter=lfs diff=lfs merge=binary -text
+# stl
+*.[sS][tT][lL] filter=lfs diff=lfs merge=binary -text
+# ztl
+*.[zZ][tT][lL] filter=lfs diff=lfs merge=binary -text
+# it
+*.[iI][tT] filter=lfs diff=lfs merge=binary -text
+# mod
+*.[mM][oO][dD] filter=lfs diff=lfs merge=binary -text
+# ogg
+*.[oO][gG][gG] filter=lfs diff=lfs merge=binary -text
+# s3m
+*.[sS]3[mM] filter=lfs diff=lfs merge=binary -text
+# xm
+*.[xX][mM] filter=lfs diff=lfs merge=binary -text
+# glb
+*.[gG][lL][bB] filter=lfs diff=lfs merge=binary -text
+# gltf
+*.[gG][lL][tT][fF] filter=lfs diff=lfs merge=binary -text
+# off
+*.[oO][fF][fF] filter=lfs diff=lfs merge=binary -text
+# wrl
+*.[wW][rR][lL] filter=lfs diff=lfs merge=binary -text
+# 3mf
+*.3[mM][fF] filter=lfs diff=lfs merge=binary -text
+# amf
+*.[aA][mM][fF] filter=lfs diff=lfs merge=binary -text
+# ifc
+*.[iI][fF][cC] filter=lfs diff=lfs merge=binary -text
+# brep
+*.[bB][rR][eE][pP] filter=lfs diff=lfs merge=binary -text
+# step
+*.[sS][tT][eE][pP] filter=lfs diff=lfs merge=binary -text
+# fcstd
+*.[fF][cC][sS][tT][dD] filter=lfs diff=lfs merge=binary -text
+# bim
+*.[bB][iI][mM] filter=lfs diff=lfs merge=binary -text
+
+# 图像,Image
+# jpg
+*.[jJ][pP][gG] filter=lfs diff=lfs merge=binary -text
+# jpeg
+*.[jJ][pP][eE][gG] filter=lfs diff=lfs merge=binary -text
+# tiff
+*.[tT][iI][fF][fF] filter=lfs diff=lfs merge=binary -text
+# gif
+*.[gG][iI][fF] filter=lfs diff=lfs merge=binary -text
+# svg
+*.[sS][vV][gG] filter=lfs diff=lfs merge=binary -text
+# svgz
+*.[sS][vV][gG][zZ] filter=lfs diff=lfs merge=binary -text
+# bmp
+*.[bB][mM][pP] filter=lfs diff=lfs merge=binary -text
+# png
+*.[pP][nN][gG] filter=lfs diff=lfs merge=binary -text
+# tif
+*.[tT][iI][fF] filter=lfs diff=lfs merge=binary -text
+# tga
+*.[tT][gG][aA] filter=lfs diff=lfs merge=binary -text
+# prj
+*.[pP][rR][jJ] filter=lfs diff=lfs merge=binary -text
+# dwg
+*.[dD][wW][gG] filter=lfs diff=lfs merge=binary -text
+# flt
+*.[fF][lL][tT] filter=lfs diff=lfs merge=binary -text
+# htr
+*.[hH][tT][rR] filter=lfs diff=lfs merge=binary -text
+# iges
+*.[iI][gG][eE][sS] filter=lfs diff=lfs merge=binary -text
+# igs
+*.[iI][gG][sS] filter=lfs diff=lfs merge=binary -text
+# ige
+*.[iI][gG][eE] filter=lfs diff=lfs merge=binary -text
+# ipt
+*.[iI][pP][tT] filter=lfs diff=lfs merge=binary -text
+# iam
+*.[iI][aA][mM] filter=lfs diff=lfs merge=binary -text
+# lp
+*.[lL][pP] filter=lfs diff=lfs merge=binary -text
+# ls
+*.[lL][sS] filter=lfs diff=lfs merge=binary -text
+# shp
+*.[sS][hH][pP] filter=lfs diff=lfs merge=binary -text
+# aep
+*.[aA][eE][pP] filter=lfs diff=lfs merge=binary -text
+# psb
+*.[pP][sS][bB] filter=lfs diff=lfs merge=binary -text
+# edx
+*.[eE][dD][xX] filter=lfs diff=lfs merge=binary -text
+# cds
+*.[cC][dD][sS] filter=lfs diff=lfs merge=binary -text
+# exr
+*.[eE][xX][rR] filter=lfs diff=lfs merge=binary -text
+# bc
+*.[bB][cC] filter=lfs diff=lfs merge=binary -text
+
+
+# 文档,Document
+# Microsoft Excel
+# xls
+*.[xX][lL][sS] filter=lfs diff=lfs merge=binary -text
+# xlsx
+*.[xX][lL][sS][xX] filter=lfs diff=lfs merge=binary -text
+# xslsm
+*.[xX][sS][lL][sS][mM] filter=lfs diff=lfs merge=binary -text
+# xlt
+*.[xX][lL][tT] filter=lfs diff=lfs merge=binary -text
+# xltx
+*.[xX][lL][tT][xX] filter=lfs diff=lfs merge=binary -text
+# xltm
+*.[xX][lL][tT][mM] filter=lfs diff=lfs merge=binary -text
+# Microsoft powperpoint
+# ppt
+*.[pP][pP][tT] filter=lfs diff=lfs merge=binary -text
+# pptx
+*.[pP][pP][tT][xX] filter=lfs diff=lfs merge=binary -text
+# pps
+*.[pP][pP][sS] filter=lfs diff=lfs merge=binary -text
+# ppsx
+*.[pP][pP][sS][xX] filter=lfs diff=lfs merge=binary -text
+# ppsm
+*.[pP][pP][sS][mM] filter=lfs diff=lfs merge=binary -text
+# pptm
+*.[pP][pP][tT][mM] filter=lfs diff=lfs merge=binary -text
+# pot
+*.[pP][oO][tT] filter=lfs diff=lfs merge=binary -text
+# potm
+*.[pP][oO][tT][mM] filter=lfs diff=lfs merge=binary -text
+# Microsoft word
+# doc
+*.[dD][oO][cC] filter=lfs diff=lfs merge=binary -text
+# docx
+*.[dD][oO][cC][xX] filter=lfs diff=lfs merge=binary -text
+# docm
+*.[dD][oO][cC][mM] filter=lfs diff=lfs merge=binary -text
+# dot
+*.[dD][oO][tT] filter=lfs diff=lfs merge=binary -text
+# dotx
+*.[dD][oO][tT][xX] filter=lfs diff=lfs merge=binary -text
+# dotm
+*.[dD][oO][tT][mM] filter=lfs diff=lfs merge=binary -text
+# Apple keynotes
+# key
+*.[kK][eE][yY] filter=lfs diff=lfs merge=binary -text
+# Apple pages
+# pages, apple
+*.[pP][aA][gG][eE][sS] filter=lfs diff=lfs merge=binary -text
+# Apple numbers
+# numbers, apple
+*.[nN][uU][mM][bB][eE][rR][sS] filter=lfs diff=lfs merge=binary -text
+
+# 电子书,Book
+# chm
+*.[cC][hH][mM] filter=lfs diff=lfs merge=binary -text
+# mobi
+*.[mM][oO][bB][iI] filter=lfs diff=lfs merge=binary -text
+# epub
+*.[eE][pP][uU][bB] filter=lfs diff=lfs merge=binary -text
+# azw
+*.[aA][zZ][wW] filter=lfs diff=lfs merge=binary -text
+# azw3
+*.[aA][zZ][wW]3 filter=lfs diff=lfs merge=binary -text
+# iba
+*.[iI][bB][aA] filter=lfs diff=lfs merge=binary -text
+# lrs
+*.[lL][rR][sS] filter=lfs diff=lfs merge=binary -text
+# lrf
+*.[lL][rR][fF] filter=lfs diff=lfs merge=binary -text
+# lrx
+*.[lL][rR][xX] filter=lfs diff=lfs merge=binary -text
+# djvu
+*.[dD][jJ][vV][uU] filter=lfs diff=lfs merge=binary -text
+# lit
+*.[lL][iI][tT] filter=lfs diff=lfs merge=binary -text
+# rft
+*.[rR][fF][tT] filter=lfs diff=lfs merge=binary -text
+# cbr
+*.[cC][bB][rR] filter=lfs diff=lfs merge=binary -text
+# cbz
+*.[cC][bB][zZ] filter=lfs diff=lfs merge=binary -text
+# cb7
+*.[cC][bB]7 filter=lfs diff=lfs merge=binary -text
+# cbt
+*.[cC][bB][tT] filter=lfs diff=lfs merge=binary -text
+# cba
+*.[cC][bB][aA] filter=lfs diff=lfs merge=binary -text
+# pdb
+*.[pP][dD][bB] filter=lfs diff=lfs merge=binary -text
+
+# 字体,font
+# ttf
+*.[tT][tT][fF] filter=lfs diff=lfs merge=binary -text
+# otf
+*.[oO][tT][fF] filter=lfs diff=lfs merge=binary -text
+# woff
+*.[wW][oO][fF][fF] filter=lfs diff=lfs merge=binary -text
+# woff2
+*.[wW][oO][fF][fF]2 filter=lfs diff=lfs merge=binary -text
+
+# 翻译,translate
+# po
+*.[pP][oO] filter=lfs diff=lfs merge=binary -text
+
+# auto generated by UGit 
+

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SSO认证中心</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 3530 - 0
package-lock.json

@@ -0,0 +1,3530 @@
+{
+  "name": "sso-frontend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "sso-frontend",
+      "version": "1.0.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.1.0",
+        "axios": "^1.6.0",
+        "dayjs": "^1.11.10",
+        "element-plus": "^2.4.2",
+        "js-cookie": "^3.0.5",
+        "pinia": "^2.1.7",
+        "vue": "^3.3.8",
+        "vue-router": "^4.2.5"
+      },
+      "devDependencies": {
+        "@types/js-cookie": "^3.0.6",
+        "@types/node": "^20.8.10",
+        "@vitejs/plugin-vue": "^4.4.1",
+        "@vue/eslint-config-prettier": "^8.0.0",
+        "@vue/eslint-config-typescript": "^12.0.0",
+        "@vue/tsconfig": "^0.4.0",
+        "eslint": "^8.53.0",
+        "eslint-plugin-vue": "^9.18.1",
+        "prettier": "^3.0.3",
+        "typescript": "~5.2.0",
+        "vite": "^4.5.0",
+        "vue-tsc": "^1.8.22"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.5"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+      "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+      "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+      "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.3",
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+      "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+      "license": "MIT"
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+      "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+      "deprecated": "Use @eslint/config-array instead",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^2.0.3",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+      "deprecated": "Use @eslint/object-schema instead",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgr/core": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+      "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/pkgr"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@types/js-cookie": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+      "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.19.27",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
+      "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+      "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/type-utils": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+      "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+      "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@ungap/structured-clone": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+      "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+      "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.0.0 || ^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
+      "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "1.11.1"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
+      "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "muggle-string": "^0.3.1"
+      }
+    },
+    "node_modules/@volar/typescript": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
+      "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "1.11.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
+      "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/shared": "3.5.26",
+        "entities": "^7.0.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
+      "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
+      "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/compiler-core": "3.5.26",
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
+      "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/eslint-config-prettier": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-8.0.0.tgz",
+      "integrity": "sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-config-prettier": "^8.8.0",
+        "eslint-plugin-prettier": "^5.0.0"
+      },
+      "peerDependencies": {
+        "eslint": ">= 8.0.0",
+        "prettier": ">= 3.0.0"
+      }
+    },
+    "node_modules/@vue/eslint-config-typescript": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
+      "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "^6.7.0",
+        "@typescript-eslint/parser": "^6.7.0",
+        "vue-eslint-parser": "^9.3.1"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0",
+        "eslint-plugin-vue": "^9.0.0",
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
+      "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "~1.11.1",
+        "@volar/source-map": "~1.11.1",
+        "@vue/compiler-dom": "^3.3.0",
+        "@vue/shared": "^3.3.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.3.1",
+        "path-browserify": "^1.0.1",
+        "vue-template-compiler": "^2.7.14"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
+      "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
+      "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
+      "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/runtime-core": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
+      "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "vue": "3.5.26"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
+      "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.4.0.tgz",
+      "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+      "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.11.1",
+        "@vueuse/shared": "10.11.1",
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+      "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+      "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
+      "integrity": "sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.1",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.17.20",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "^10.11.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.19",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
+      "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+      "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+      "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.57.1",
+        "@humanwhocodes/config-array": "^0.13.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-config-prettier": {
+      "version": "8.10.2",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz",
+      "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "5.5.4",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
+      "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0",
+        "synckit": "^0.11.7"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-plugin-prettier"
+      },
+      "peerDependencies": {
+        "@types/eslint": ">=8.0.0",
+        "eslint": ">=8.0.0",
+        "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+        "prettier": ">=3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/eslint": {
+          "optional": true
+        },
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-plugin-vue": {
+      "version": "9.33.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
+      "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.3",
+        "vue-eslint-parser": "^9.4.3",
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-diff": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+      "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.22",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+      "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
+      "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+      "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.4.4",
+        "vue": "^2.7.0 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.7.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
+      "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "3.29.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
+      "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=14.18.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/synckit": {
+      "version": "0.11.11",
+      "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
+      "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@pkgr/core": "^0.2.9"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/synckit"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+      "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+      "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "4.5.14",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+      "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.18.10",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
+      "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-sfc": "3.5.26",
+        "@vue/runtime-dom": "3.5.26",
+        "@vue/server-renderer": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-eslint-parser": {
+      "version": "9.4.3",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+      "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=6.0.0"
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-template-compiler": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
+      "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
+      "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~1.11.1",
+        "@vue/language-core": "1.8.27",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}

+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "sso-frontend",
+  "version": "1.0.0",
+  "description": "SSO认证中心前端应用",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "vue": "^3.3.8",
+    "vue-router": "^4.2.5",
+    "pinia": "^2.1.7",
+    "axios": "^1.6.0",
+    "element-plus": "^2.4.2",
+    "@element-plus/icons-vue": "^2.1.0",
+    "js-cookie": "^3.0.5",
+    "dayjs": "^1.11.10"
+  },
+  "devDependencies": {
+    "@types/node": "^20.8.10",
+    "@types/js-cookie": "^3.0.6",
+    "@vitejs/plugin-vue": "^4.4.1",
+    "@vue/eslint-config-prettier": "^8.0.0",
+    "@vue/eslint-config-typescript": "^12.0.0",
+    "@vue/tsconfig": "^0.4.0",
+    "eslint": "^8.53.0",
+    "eslint-plugin-vue": "^9.18.1",
+    "prettier": "^3.0.3",
+    "typescript": "~5.2.0",
+    "vite": "^4.5.0",
+    "vue-tsc": "^1.8.22"
+  }
+}

+ 38 - 0
src/App.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import { useAuthStore } from '@/stores/auth'
+
+const authStore = useAuthStore()
+
+onMounted(() => {
+  // 应用启动时检查登录状态
+  authStore.checkAuth()
+})
+</script>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #2c3e50;
+  height: 100vh;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  background-color: #f5f5f5;
+}
+</style>

+ 287 - 0
src/api/admin.ts

@@ -0,0 +1,287 @@
+import request from './request'
+import type { ApiResponse } from '@/types/auth'
+
+export interface SystemStats {
+  total_users: number
+  active_users: number
+  new_users_today: number
+  total_apps: number
+  active_apps: number
+  new_apps_today: number
+  active_tokens: number
+  today_requests: number
+  requests_change_percent: number
+  uptime: string
+  cpu_usage: number
+  memory_usage: number
+}
+
+export interface UserManagement {
+  id: string
+  username: string
+  email: string
+  phone?: string
+  is_active: boolean
+  is_superuser: boolean
+  last_login_at?: string
+  created_at: string
+  login_count: number
+  app_count: number
+}
+
+export interface AppManagement {
+  id: string
+  name: string
+  app_key: string
+  description?: string
+  is_active: boolean
+  is_trusted: boolean
+  created_by: string
+  creator_username: string
+  created_at: string
+  user_count: number
+  request_count: number
+}
+
+export interface SystemLog {
+  id: string
+  level: string
+  message: string
+  module: string
+  user_id?: string
+  username?: string
+  ip_address?: string
+  created_at: string
+  extra_data?: Record<string, any>
+}
+
+export interface SecurityEvent {
+  id: string
+  event_type: string
+  severity: string
+  title: string
+  description: string
+  user_id?: string
+  username?: string
+  ip_address: string
+  user_agent?: string
+  created_at: string
+  handled: boolean
+}
+
+export interface SystemConfig {
+  // 基本设置
+  site_name: string
+  site_description: string
+  site_logo?: string
+  
+  // 安全设置
+  password_min_length: number
+  password_require_uppercase: boolean
+  password_require_lowercase: boolean
+  password_require_numbers: boolean
+  password_require_symbols: boolean
+  login_max_attempts: number
+  login_lockout_duration: number
+  
+  // 令牌设置
+  default_access_token_expires: number
+  default_refresh_token_expires: number
+  max_token_lifetime: number
+  
+  // 邮件设置
+  email_enabled: boolean
+  smtp_host?: string
+  smtp_port?: number
+  smtp_username?: string
+  smtp_password?: string
+  smtp_use_tls: boolean
+  
+  // 其他设置
+  registration_enabled: boolean
+  email_verification_required: boolean
+  admin_approval_required: boolean
+}
+
+export const adminApi = {
+  // 获取系统统计
+  getSystemStats(): Promise<ApiResponse<SystemStats>> {
+    return request.get('/api/v1/admin/stats')
+  },
+
+  // 获取用户列表(管理员视图)
+  getUsers(params?: {
+    page?: number
+    page_size?: number
+    keyword?: string
+    status?: string
+    role?: string
+    start_date?: string
+    end_date?: string
+  }): Promise<ApiResponse<{
+    items: UserManagement[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/admin/users', { params })
+  },
+
+  // 创建用户
+  createUser(data: {
+    username: string
+    email: string
+    password: string
+    phone?: string
+    is_superuser?: boolean
+  }): Promise<ApiResponse<UserManagement>> {
+    return request.post('/api/v1/admin/users', data)
+  },
+
+  // 更新用户
+  updateUser(userId: string, data: {
+    email?: string
+    phone?: string
+    is_active?: boolean
+    is_superuser?: boolean
+  }): Promise<ApiResponse<UserManagement>> {
+    return request.put(`/api/v1/admin/users/${userId}`, data)
+  },
+
+  // 删除用户
+  deleteUser(userId: string): Promise<ApiResponse> {
+    return request.delete(`/api/v1/admin/users/${userId}`)
+  },
+
+  // 重置用户密码
+  resetUserPassword(userId: string, newPassword: string): Promise<ApiResponse> {
+    return request.put(`/api/v1/admin/users/${userId}/password`, {
+      new_password: newPassword
+    })
+  },
+
+  // 强制用户下线
+  forceUserLogout(userId: string): Promise<ApiResponse> {
+    return request.post(`/api/v1/admin/users/${userId}/force-logout`)
+  },
+
+  // 获取应用列表(管理员视图)
+  getApps(params?: {
+    page?: number
+    page_size?: number
+    keyword?: string
+    status?: string
+    creator?: string
+    start_date?: string
+    end_date?: string
+  }): Promise<ApiResponse<{
+    items: AppManagement[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/admin/apps', { params })
+  },
+
+  // 审核应用
+  approveApp(appId: string, approved: boolean, reason?: string): Promise<ApiResponse> {
+    return request.put(`/api/v1/admin/apps/${appId}/approve`, {
+      approved,
+      reason
+    })
+  },
+
+  // 获取系统日志
+  getSystemLogs(params?: {
+    page?: number
+    page_size?: number
+    level?: string
+    module?: string
+    start_time?: string
+    end_time?: string
+    keyword?: string
+  }): Promise<ApiResponse<{
+    items: SystemLog[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/admin/logs', { params })
+  },
+
+  // 获取安全事件
+  getSecurityEvents(params?: {
+    page?: number
+    page_size?: number
+    event_type?: string
+    severity?: string
+    handled?: boolean
+    start_time?: string
+    end_time?: string
+  }): Promise<ApiResponse<{
+    items: SecurityEvent[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/admin/security/events', { params })
+  },
+
+  // 处理安全事件
+  handleSecurityEvent(eventId: string, action: string, note?: string): Promise<ApiResponse> {
+    return request.put(`/api/v1/admin/security/events/${eventId}`, {
+      action,
+      note
+    })
+  },
+
+  // 获取系统配置
+  getSystemConfig(): Promise<ApiResponse<SystemConfig>> {
+    return request.get('/api/v1/admin/config')
+  },
+
+  // 更新系统配置
+  updateSystemConfig(config: Partial<SystemConfig>): Promise<ApiResponse> {
+    return request.put('/api/v1/admin/config', config)
+  },
+
+  // 系统备份
+  createBackup(description?: string): Promise<ApiResponse<{ backup_id: string }>> {
+    return request.post('/api/v1/admin/backup', { description })
+  },
+
+  // 获取备份列表
+  getBackups(): Promise<ApiResponse<Array<{
+    id: string
+    description: string
+    size: number
+    created_at: string
+  }>>> {
+    return request.get('/api/v1/admin/backups')
+  },
+
+  // 下载备份
+  downloadBackup(backupId: string): Promise<Blob> {
+    return request.get(`/api/v1/admin/backups/${backupId}/download`, {
+      responseType: 'blob'
+    })
+  },
+
+  // 数据导出
+  exportData(type: 'users' | 'apps' | 'logs', format: 'csv' | 'json', filters?: Record<string, any>): Promise<Blob> {
+    return request.post(`/api/v1/admin/export/${type}`, {
+      format,
+      filters
+    }, {
+      responseType: 'blob'
+    })
+  },
+
+  // 清理过期数据
+  cleanupExpiredData(types: string[]): Promise<ApiResponse<{
+    cleaned_count: number
+    types: string[]
+  }>> {
+    return request.post('/api/v1/admin/cleanup', { types })
+  }
+}

+ 156 - 0
src/api/app.ts

@@ -0,0 +1,156 @@
+import request from './request'
+import type { ApiResponse } from '@/types/auth'
+
+export interface App {
+  id: string
+  name: string
+  app_key: string
+  app_secret?: string
+  description?: string
+  icon_url?: string
+  redirect_uris: string[]
+  scope: string[]
+  is_active: boolean
+  is_trusted: boolean
+  access_token_expires: number
+  refresh_token_expires: number
+  created_by: string
+  created_at: string
+  updated_at: string
+  // 统计数据
+  today_requests?: number
+  active_users?: number
+  total_requests?: number
+}
+
+export interface CreateAppRequest {
+  name: string
+  description?: string
+  icon_url?: string
+  redirect_uris: string[]
+  scope: string[]
+  access_token_expires?: number
+  refresh_token_expires?: number
+  is_trusted?: boolean
+}
+
+export interface UpdateAppRequest {
+  name?: string
+  description?: string
+  icon_url?: string
+  redirect_uris?: string[]
+  scope?: string[]
+  access_token_expires?: number
+  refresh_token_expires?: number
+  is_trusted?: boolean
+}
+
+export interface AppStats {
+  total_requests: number
+  today_requests: number
+  active_users: number
+  error_rate: number
+  avg_response_time: number
+}
+
+export interface AppLog {
+  id: string
+  app_id: string
+  user_id?: string
+  username?: string
+  action: string
+  ip_address: string
+  user_agent: string
+  success: boolean
+  error_message?: string
+  created_at: string
+}
+
+export const appApi = {
+  // 获取应用列表
+  getApps(params?: {
+    page?: number
+    page_size?: number
+    keyword?: string
+    status?: string
+    trusted?: string
+  }): Promise<ApiResponse<{
+    items: App[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/apps', { params })
+  },
+
+  // 获取应用详情
+  getApp(appId: string): Promise<ApiResponse<App>> {
+    return request.get(`/api/v1/apps/${appId}`)
+  },
+
+  // 创建应用
+  createApp(data: CreateAppRequest): Promise<ApiResponse<App>> {
+    return request.post('/api/v1/apps', data)
+  },
+
+  // 更新应用
+  updateApp(appId: string, data: UpdateAppRequest): Promise<ApiResponse<App>> {
+    return request.put(`/api/v1/apps/${appId}`, data)
+  },
+
+  // 删除应用
+  deleteApp(appId: string): Promise<ApiResponse> {
+    return request.delete(`/api/v1/apps/${appId}`)
+  },
+
+  // 启用/禁用应用
+  toggleAppStatus(appId: string, isActive: boolean): Promise<ApiResponse> {
+    return request.put(`/api/v1/apps/${appId}/status`, { is_active: isActive })
+  },
+
+  // 重置应用密钥
+  resetAppSecret(appId: string): Promise<ApiResponse<{ app_secret: string }>> {
+    return request.post(`/api/v1/apps/${appId}/reset-secret`)
+  },
+
+  // 获取应用密钥
+  getAppSecret(appId: string): Promise<ApiResponse<{ app_secret: string }>> {
+    return request.get(`/api/v1/apps/${appId}/secret`)
+  },
+
+  // 上传应用图标
+  uploadIcon(file: File): Promise<ApiResponse<{ url: string }>> {
+    const formData = new FormData()
+    formData.append('icon', file)
+    return request.post('/api/v1/upload/icon', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+  },
+
+  // 获取应用统计
+  getAppStats(appId: string, params?: {
+    start_date?: string
+    end_date?: string
+  }): Promise<ApiResponse<AppStats>> {
+    return request.get(`/api/v1/apps/${appId}/stats`, { params })
+  },
+
+  // 获取应用日志
+  getAppLogs(appId: string, params?: {
+    page?: number
+    page_size?: number
+    start_time?: string
+    end_time?: string
+    action?: string
+    success?: boolean
+  }): Promise<ApiResponse<{
+    items: AppLog[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get(`/api/v1/apps/${appId}/logs`, { params })
+  }
+}

+ 47 - 0
src/api/auth.ts

@@ -0,0 +1,47 @@
+import request from './request'
+import type { LoginForm, RegisterForm, User, TokenResponse, ApiResponse } from '@/types/auth'
+
+export const authApi = {
+  // 用户登录
+  login(data: LoginForm): Promise<ApiResponse<TokenResponse>> {
+    return request.post('/api/v1/auth/login', data)
+  },
+
+  // 用户注册
+  register(data: RegisterForm): Promise<ApiResponse> {
+    return request.post('/api/v1/auth/register', data)
+  },
+
+  // 用户登出
+  logout(): Promise<ApiResponse> {
+    return request.post('/api/v1/auth/logout')
+  },
+
+  // 刷新token
+  refreshToken(): Promise<ApiResponse<TokenResponse>> {
+    return request.post('/api/v1/auth/refresh')
+  },
+
+  // 获取用户信息
+  getUserInfo(): Promise<ApiResponse<User>> {
+    return request.get('/api/v1/auth/userinfo')
+  },
+
+  // 获取验证码
+  getCaptcha(): Promise<ApiResponse<{ captcha_id: string; captcha_image: string }>> {
+    return request.get('/api/v1/auth/captcha')
+  },
+
+  // 忘记密码
+  forgotPassword(email: string): Promise<ApiResponse> {
+    return request.post('/api/v1/auth/forgot-password', { email })
+  },
+
+  // 重置密码
+  resetPassword(token: string, newPassword: string): Promise<ApiResponse> {
+    return request.post('/api/v1/auth/reset-password', {
+      token,
+      new_password: newPassword
+    })
+  }
+}

+ 110 - 0
src/api/request.ts

@@ -0,0 +1,110 @@
+import axios from 'axios'
+import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
+import { ElMessage } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+import { getToken, getRefreshToken } from '@/utils/auth'
+import router from '@/router'
+
+// 创建axios实例
+const request: AxiosInstance = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000',
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+})
+
+// 请求拦截器
+request.interceptors.request.use(
+  (config: AxiosRequestConfig) => {
+    const token = getToken()
+    if (token && config.headers) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+
+// 响应拦截器
+request.interceptors.response.use(
+  (response: AxiosResponse) => {
+    const { code, message, data } = response.data
+    
+    // 成功响应
+    if (code === 0) {
+      return response.data
+    }
+    
+    // 业务错误
+    ElMessage.error(message || '请求失败')
+    return Promise.reject(new Error(message || '请求失败'))
+  },
+  async (error) => {
+    const { response } = error
+    
+    if (response) {
+      const { status, data } = response
+      
+      switch (status) {
+        case 401:
+          // 未授权,尝试刷新token
+          const authStore = useAuthStore()
+          const refreshToken = getRefreshToken()
+          
+          if (refreshToken && !error.config._retry) {
+            error.config._retry = true
+            
+            try {
+              const success = await authStore.refreshToken()
+              if (success) {
+                // 重新发送原请求
+                const token = getToken()
+                if (token) {
+                  error.config.headers.Authorization = `Bearer ${token}`
+                }
+                return request(error.config)
+              }
+            } catch (refreshError) {
+              console.error('刷新token失败:', refreshError)
+            }
+          }
+          
+          // 刷新失败或没有refresh token,跳转到登录页
+          await authStore.logout()
+          router.push('/login')
+          ElMessage.error('登录已过期,请重新登录')
+          break
+          
+        case 403:
+          ElMessage.error('权限不足')
+          router.push('/unauthorized')
+          break
+          
+        case 404:
+          ElMessage.error('请求的资源不存在')
+          break
+          
+        case 429:
+          ElMessage.error('请求过于频繁,请稍后再试')
+          break
+          
+        case 500:
+          ElMessage.error('服务器内部错误')
+          break
+          
+        default:
+          ElMessage.error(data?.message || '请求失败')
+      }
+    } else {
+      // 网络错误
+      ElMessage.error('网络连接失败,请检查网络设置')
+    }
+    
+    return Promise.reject(error)
+  }
+)
+
+export default request

+ 120 - 0
src/api/user.ts

@@ -0,0 +1,120 @@
+import request from './request'
+import type { ApiResponse } from '@/types/auth'
+
+export interface UserProfile {
+  id: string
+  username: string
+  email: string
+  phone?: string
+  real_name?: string
+  company?: string
+  department?: string
+  position?: string
+  avatar_url?: string
+  is_active: boolean
+  is_superuser: boolean
+  created_at: string
+  updated_at: string
+  last_login_at?: string
+}
+
+export interface UpdateProfileRequest {
+  email: string
+  phone?: string
+  real_name?: string
+  company?: string
+  department?: string
+  position?: string
+}
+
+export interface ChangePasswordRequest {
+  old_password: string
+  new_password: string
+}
+
+export interface LoginLog {
+  id: string
+  username: string
+  login_type: string
+  ip_address: string
+  user_agent: string
+  success: boolean
+  failure_reason?: string
+  login_at: string
+}
+
+export interface UserToken {
+  id: string
+  app_name: string
+  token_type: string
+  scope: string
+  expires_at: string
+  created_at: string
+  last_used_at?: string
+}
+
+export const userApi = {
+  // 获取用户信息
+  getProfile(): Promise<ApiResponse<UserProfile>> {
+    return request.get('/api/v1/users/profile')
+  },
+
+  // 更新用户信息
+  updateProfile(data: UpdateProfileRequest): Promise<ApiResponse> {
+    return request.put('/api/v1/users/profile', data)
+  },
+
+  // 修改密码
+  changePassword(data: ChangePasswordRequest): Promise<ApiResponse> {
+    return request.put('/api/v1/users/password', data)
+  },
+
+  // 上传头像
+  uploadAvatar(file: File): Promise<ApiResponse<{ url: string }>> {
+    const formData = new FormData()
+    formData.append('avatar', file)
+    return request.post('/api/v1/users/avatar', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+  },
+
+  // 获取登录日志
+  getLoginLogs(params?: {
+    page?: number
+    page_size?: number
+    start_time?: string
+    end_time?: string
+  }): Promise<ApiResponse<{
+    items: LoginLog[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/users/login-logs', { params })
+  },
+
+  // 获取用户令牌
+  getTokens(params?: {
+    page?: number
+    page_size?: number
+  }): Promise<ApiResponse<{
+    items: UserToken[]
+    total: number
+    page: number
+    page_size: number
+  }>> {
+    return request.get('/api/v1/users/tokens', { params })
+  },
+
+  // 撤销令牌
+  revokeToken(tokenId: string): Promise<ApiResponse> {
+    return request.delete(`/api/v1/users/tokens/${tokenId}`)
+  },
+
+  // 强制下线(撤销所有令牌)
+  forceLogout(): Promise<ApiResponse> {
+    return request.post('/api/v1/users/force-logout')
+  }
+}

+ 16 - 0
src/env.d.ts

@@ -0,0 +1,16 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL: string
+  readonly VITE_APP_TITLE: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 204 - 0
src/layouts/MainLayout.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="main-layout">
+    <el-container>
+      <!-- 头部 -->
+      <el-header class="header">
+        <div class="header-left">
+          <h1>SSO 认证中心</h1>
+        </div>
+        <div class="header-right">
+          <el-dropdown @command="handleCommand">
+            <span class="user-info">
+              <el-avatar :src="authStore.user?.avatar_url" :size="32">
+                {{ authStore.user?.username?.charAt(0).toUpperCase() }}
+              </el-avatar>
+              <span class="username">{{ authStore.user?.username }}</span>
+              <el-icon><ArrowDown /></el-icon>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="profile">
+                  <el-icon><User /></el-icon>
+                  个人资料
+                </el-dropdown-item>
+                <el-dropdown-item command="settings">
+                  <el-icon><Setting /></el-icon>
+                  设置
+                </el-dropdown-item>
+                <el-dropdown-item divided command="logout">
+                  <el-icon><SwitchButton /></el-icon>
+                  退出登录
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </el-header>
+
+      <el-container>
+        <!-- 侧边栏 -->
+        <el-aside width="200px" class="sidebar">
+          <el-menu
+            :default-active="activeMenu"
+            class="sidebar-menu"
+            router
+          >
+            <el-menu-item index="/dashboard">
+              <el-icon><House /></el-icon>
+              <span>仪表盘</span>
+            </el-menu-item>
+            <el-menu-item index="/profile">
+              <el-icon><User /></el-icon>
+              <span>个人资料</span>
+            </el-menu-item>
+            <el-menu-item index="/apps">
+              <el-icon><Grid /></el-icon>
+              <span>我的应用</span>
+            </el-menu-item>
+            <el-sub-menu v-if="authStore.isAdmin" index="/admin">
+              <template #title>
+                <el-icon><Setting /></el-icon>
+                <span>系统管理</span>
+              </template>
+              <el-menu-item index="/admin/dashboard">
+                <el-icon><Monitor /></el-icon>
+                <span>管理概览</span>
+              </el-menu-item>
+              <el-menu-item index="/admin/users">
+                <el-icon><UserFilled /></el-icon>
+                <span>用户管理</span>
+              </el-menu-item>
+              <el-menu-item index="/admin/apps">
+                <el-icon><Grid /></el-icon>
+                <span>应用管理</span>
+              </el-menu-item>
+              <el-menu-item index="/admin/logs">
+                <el-icon><Document /></el-icon>
+                <span>系统日志</span>
+              </el-menu-item>
+              <el-menu-item index="/admin/settings">
+                <el-icon><Tools /></el-icon>
+                <span>系统设置</span>
+              </el-menu-item>
+            </el-sub-menu>
+          </el-menu>
+        </el-aside>
+
+        <!-- 主内容区 -->
+        <el-main class="main-content">
+          <router-view />
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+
+const activeMenu = computed(() => {
+  // 处理子路由的菜单激活状态
+  const path = route.path
+  if (path.startsWith('/admin')) {
+    return path
+  }
+  if (path.startsWith('/apps')) {
+    return '/apps'
+  }
+  return path
+})
+
+// 处理下拉菜单命令
+const handleCommand = async (command: string) => {
+  switch (command) {
+    case 'profile':
+      router.push('/profile')
+      break
+    case 'settings':
+      router.push('/settings')
+      break
+    case 'logout':
+      try {
+        await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        
+        await authStore.logout()
+        ElMessage.success('已退出登录')
+        router.push('/login')
+      } catch (error) {
+        // 用户取消
+      }
+      break
+  }
+}
+</script>
+
+<style scoped>
+.main-layout {
+  height: 100vh;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: #fff;
+  border-bottom: 1px solid #e6e6e6;
+  padding: 0 20px;
+}
+
+.header-left h1 {
+  margin: 0;
+  color: #333;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  padding: 8px;
+  border-radius: 4px;
+  transition: background-color 0.3s;
+}
+
+.user-info:hover {
+  background-color: #f5f5f5;
+}
+
+.username {
+  font-size: 14px;
+  color: #333;
+}
+
+.sidebar {
+  background: #fff;
+  border-right: 1px solid #e6e6e6;
+}
+
+.sidebar-menu {
+  border-right: none;
+}
+
+.main-content {
+  background: #f5f5f5;
+  padding: 0;
+}
+</style>

+ 22 - 0
src/main.ts

@@ -0,0 +1,22 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+// 注册Element Plus图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+
+app.mount('#app')

+ 137 - 0
src/router/index.ts

@@ -0,0 +1,137 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+import { useAuthStore } from '@/stores/auth'
+import MainLayout from '@/layouts/MainLayout.vue'
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/',
+    redirect: '/dashboard'
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/auth/Login.vue'),
+    meta: { requiresGuest: true }
+  },
+  {
+    path: '/register',
+    name: 'Register', 
+    component: () => import('@/views/auth/Register.vue'),
+    meta: { requiresGuest: true }
+  },
+  {
+    path: '/oauth/callback',
+    name: 'OAuthCallback',
+    component: () => import('@/views/auth/OAuthCallback.vue')
+  },
+  {
+    path: '/',
+    component: MainLayout,
+    meta: { requiresAuth: true },
+    children: [
+      {
+        path: 'dashboard',
+        name: 'Dashboard',
+        component: () => import('@/views/dashboard/Index.vue')
+      },
+      {
+        path: 'profile',
+        name: 'Profile',
+        component: () => import('@/views/user/Profile.vue')
+      },
+      {
+        path: 'apps',
+        name: 'Apps',
+        component: () => import('@/views/apps/Index.vue')
+      },
+      {
+        path: 'apps/create',
+        name: 'CreateApp',
+        component: () => import('@/views/apps/Create.vue')
+      },
+      {
+        path: 'admin',
+        redirect: '/admin/dashboard'
+      },
+      {
+        path: 'admin/dashboard',
+        name: 'AdminDashboard',
+        component: () => import('@/views/admin/Dashboard.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/users',
+        name: 'AdminUsers',
+        component: () => import('@/views/admin/Users.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/apps',
+        name: 'AdminApps',
+        component: () => import('@/views/admin/Apps.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/logs',
+        name: 'AdminLogs',
+        component: () => import('@/views/admin/Logs.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/settings',
+        name: 'AdminSettings',
+        component: () => import('@/views/admin/Settings.vue'),
+        meta: { requiresAdmin: true }
+      }
+    ]
+  },
+  {
+    path: '/unauthorized',
+    name: 'Unauthorized',
+    component: () => import('@/views/error/403.vue')
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'NotFound',
+    component: () => import('@/views/error/404.vue')
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+// 路由守卫
+router.beforeEach(async (to, from, next) => {
+  const authStore = useAuthStore()
+  
+  // 检查是否需要认证
+  if (to.meta.requiresAuth) {
+    if (!authStore.isAuthenticated) {
+      // 未登录,重定向到登录页
+      next({
+        name: 'Login',
+        query: { redirect: to.fullPath }
+      })
+      return
+    }
+    
+    // 检查是否需要管理员权限
+    if (to.meta.requiresAdmin && !authStore.isAdmin) {
+      next({ name: 'Unauthorized' })
+      return
+    }
+  }
+  
+  // 检查是否需要访客状态(未登录)
+  if (to.meta.requiresGuest && authStore.isAuthenticated) {
+    next({ name: 'Dashboard' })
+    return
+  }
+  
+  next()
+})
+
+export default router

+ 120 - 0
src/stores/auth.ts

@@ -0,0 +1,120 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import type { User, LoginForm, TokenResponse } from '@/types/auth'
+import { authApi } from '@/api/auth'
+import { userApi, type UserProfile } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+
+export const useAuthStore = defineStore('auth', () => {
+  const user = ref<UserProfile | null>(null)
+  const token = ref<string | null>(getToken())
+  const loading = ref(false)
+
+  // 计算属性
+  const isAuthenticated = computed(() => !!token.value && !!user.value)
+  const isAdmin = computed(() => user.value?.is_superuser || false)
+
+  // 登录
+  const login = async (loginForm: LoginForm): Promise<void> => {
+    loading.value = true
+    try {
+      const response = await authApi.login(loginForm)
+      const tokenData: TokenResponse = response.data
+      
+      // 保存token
+      token.value = tokenData.access_token
+      setToken(tokenData.access_token, tokenData.refresh_token)
+      
+      // 获取用户信息
+      await fetchUserInfo()
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 登出
+  const logout = async (): Promise<void> => {
+    try {
+      if (token.value) {
+        await authApi.logout()
+      }
+    } catch (error) {
+      console.error('登出失败:', error)
+    } finally {
+      // 清除本地数据
+      user.value = null
+      token.value = null
+      removeToken()
+    }
+  }
+
+  // 获取用户信息
+  const fetchUserInfo = async (): Promise<void> => {
+    if (!token.value) return
+    
+    try {
+      const response = await userApi.getProfile()
+      user.value = response.data
+    } catch (error) {
+      console.error('获取用户信息失败:', error)
+      // 如果获取用户信息失败,可能token已过期,清除登录状态
+      await logout()
+      throw error
+    }
+  }
+
+  // 检查认证状态
+  const checkAuth = async (): Promise<void> => {
+    if (token.value && !user.value) {
+      try {
+        await fetchUserInfo()
+      } catch (error) {
+        // 认证失败,清除token
+        await logout()
+      }
+    }
+  }
+
+  // 刷新token
+  const refreshToken = async (): Promise<boolean> => {
+    try {
+      const response = await authApi.refreshToken()
+      const tokenData: TokenResponse = response.data
+      
+      token.value = tokenData.access_token
+      setToken(tokenData.access_token, tokenData.refresh_token)
+      
+      return true
+    } catch (error) {
+      console.error('刷新token失败:', error)
+      await logout()
+      return false
+    }
+  }
+
+  // 更新用户信息
+  const updateProfile = async (profileData: any): Promise<void> => {
+    try {
+      await userApi.updateProfile(profileData)
+      // 重新获取用户信息
+      await fetchUserInfo()
+    } catch (error) {
+      console.error('更新用户信息失败:', error)
+      throw error
+    }
+  }
+
+  return {
+    user,
+    token,
+    loading,
+    isAuthenticated,
+    isAdmin,
+    login,
+    logout,
+    fetchUserInfo,
+    checkAuth,
+    refreshToken,
+    updateProfile
+  }
+})

+ 41 - 0
src/types/auth.ts

@@ -0,0 +1,41 @@
+export interface User {
+  id: string
+  username: string
+  email: string
+  phone?: string
+  avatar_url?: string
+  is_active: boolean
+  is_superuser: boolean
+  roles: string[]
+  permissions: string[]
+}
+
+export interface LoginForm {
+  username: string
+  password: string
+  remember_me?: boolean
+  captcha?: string
+}
+
+export interface RegisterForm {
+  username: string
+  email: string
+  password: string
+  phone?: string
+  captcha: string
+}
+
+export interface TokenResponse {
+  access_token: string
+  refresh_token?: string
+  token_type: string
+  expires_in: number
+  scope?: string
+}
+
+export interface ApiResponse<T = any> {
+  code: number
+  message: string
+  data: T
+  timestamp: string
+}

+ 44 - 0
src/utils/auth.ts

@@ -0,0 +1,44 @@
+import Cookies from 'js-cookie'
+
+const TOKEN_KEY = 'sso_access_token'
+const REFRESH_TOKEN_KEY = 'sso_refresh_token'
+
+// 获取token
+export function getToken(): string | null {
+  return Cookies.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY)
+}
+
+// 获取refresh token
+export function getRefreshToken(): string | null {
+  return Cookies.get(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY)
+}
+
+// 设置token
+export function setToken(token: string, refreshToken?: string, rememberMe = false): void {
+  if (rememberMe) {
+    // 记住我,使用cookie存储,有效期30天
+    Cookies.set(TOKEN_KEY, token, { expires: 30 })
+    if (refreshToken) {
+      Cookies.set(REFRESH_TOKEN_KEY, refreshToken, { expires: 30 })
+    }
+  } else {
+    // 不记住我,使用sessionStorage存储
+    localStorage.setItem(TOKEN_KEY, token)
+    if (refreshToken) {
+      localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
+    }
+  }
+}
+
+// 移除token
+export function removeToken(): void {
+  Cookies.remove(TOKEN_KEY)
+  Cookies.remove(REFRESH_TOKEN_KEY)
+  localStorage.removeItem(TOKEN_KEY)
+  localStorage.removeItem(REFRESH_TOKEN_KEY)
+}
+
+// 检查token是否存在
+export function hasToken(): boolean {
+  return !!getToken()
+}

+ 61 - 0
src/views/admin/Apps.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="admin-apps">
+    <div class="page-header">
+      <h2>应用管理</h2>
+      <p>管理所有OAuth2应用和API配置</p>
+    </div>
+
+    <el-card>
+      <div class="coming-soon">
+        <el-icon size="64" color="#67C23A"><Grid /></el-icon>
+        <h3>应用管理功能</h3>
+        <p>此功能正在开发中,敬请期待...</p>
+        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 应用管理页面
+</script>
+
+<style scoped>
+.admin-apps {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.coming-soon {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.coming-soon h3 {
+  margin: 16px 0 8px 0;
+  color: #333;
+  font-size: 20px;
+}
+
+.coming-soon p {
+  margin: 0 0 24px 0;
+  color: #666;
+  font-size: 14px;
+}
+</style>

+ 332 - 0
src/views/admin/Dashboard.vue

@@ -0,0 +1,332 @@
+<template>
+  <div class="admin-dashboard">
+    <div class="page-header">
+      <h2>系统管理</h2>
+      <p>管理系统用户、应用和配置</p>
+    </div>
+
+    <!-- 系统统计 -->
+    <div class="stats-grid">
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#409EFF"><UserFilled /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ systemStats.userCount }}</div>
+            <div class="stats-label">总用户数</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#67C23A"><Grid /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ systemStats.appCount }}</div>
+            <div class="stats-label">总应用数</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#E6A23C"><Key /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ systemStats.tokenCount }}</div>
+            <div class="stats-label">活跃令牌</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#F56C6C"><Warning /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ systemStats.alertCount }}</div>
+            <div class="stats-label">系统警告</div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+
+    <!-- 快速管理 -->
+    <el-row :gutter="24">
+      <el-col :span="12">
+        <el-card class="management-card">
+          <template #header>
+            <span>用户管理</span>
+          </template>
+          <div class="management-content">
+            <p>管理系统用户账户、权限和状态</p>
+            <div class="management-actions">
+              <el-button type="primary" @click="$router.push('/admin/users')">
+                <el-icon><UserFilled /></el-icon>
+                用户管理
+              </el-button>
+              <el-button @click="createUser">
+                <el-icon><Plus /></el-icon>
+                创建用户
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="12">
+        <el-card class="management-card">
+          <template #header>
+            <span>应用管理</span>
+          </template>
+          <div class="management-content">
+            <p>管理所有OAuth2应用和API配置</p>
+            <div class="management-actions">
+              <el-button type="primary" @click="$router.push('/admin/apps')">
+                <el-icon><Grid /></el-icon>
+                应用管理
+              </el-button>
+              <el-button @click="$router.push('/apps/create')">
+                <el-icon><Plus /></el-icon>
+                创建应用
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="24" style="margin-top: 24px;">
+      <el-col :span="12">
+        <el-card class="management-card">
+          <template #header>
+            <span>系统日志</span>
+          </template>
+          <div class="management-content">
+            <p>查看系统操作日志和安全事件</p>
+            <div class="management-actions">
+              <el-button type="primary" @click="$router.push('/admin/logs')">
+                <el-icon><Document /></el-icon>
+                查看日志
+              </el-button>
+              <el-button @click="exportLogs">
+                <el-icon><Download /></el-icon>
+                导出日志
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="12">
+        <el-card class="management-card">
+          <template #header>
+            <span>系统设置</span>
+          </template>
+          <div class="management-content">
+            <p>配置系统参数和安全策略</p>
+            <div class="management-actions">
+              <el-button type="primary" @click="$router.push('/admin/settings')">
+                <el-icon><Tools /></el-icon>
+                系统设置
+              </el-button>
+              <el-button @click="backupSystem">
+                <el-icon><FolderOpened /></el-icon>
+                系统备份
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 最近活动 -->
+    <el-card class="recent-activity" style="margin-top: 24px;">
+      <template #header>
+        <span>最近系统活动</span>
+      </template>
+      <el-table :data="recentActivities" style="width: 100%">
+        <el-table-column prop="time" label="时间" width="180" />
+        <el-table-column prop="user" label="用户" width="120" />
+        <el-table-column prop="action" label="操作" />
+        <el-table-column prop="status" label="状态" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === 'success' ? 'success' : 'danger'">
+              {{ scope.row.status === 'success' ? '成功' : '失败' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 系统统计数据
+const systemStats = ref({
+  userCount: 156,
+  appCount: 23,
+  tokenCount: 89,
+  alertCount: 2
+})
+
+// 最近活动
+const recentActivities = ref([
+  {
+    time: '2024-01-15 14:30:25',
+    user: 'admin',
+    action: '创建了新用户 "test_user"',
+    status: 'success'
+  },
+  {
+    time: '2024-01-15 14:25:10',
+    user: 'john_doe',
+    action: '更新了应用 "测试应用" 的配置',
+    status: 'success'
+  },
+  {
+    time: '2024-01-15 14:20:05',
+    user: 'system',
+    action: '自动清理过期令牌',
+    status: 'success'
+  },
+  {
+    time: '2024-01-15 14:15:30',
+    user: 'jane_smith',
+    action: '尝试访问未授权资源',
+    status: 'failed'
+  }
+])
+
+// 创建用户
+const createUser = () => {
+  ElMessage.info('创建用户功能开发中...')
+}
+
+// 导出日志
+const exportLogs = () => {
+  ElMessage.info('导出日志功能开发中...')
+}
+
+// 系统备份
+const backupSystem = () => {
+  ElMessage.info('系统备份功能开发中...')
+}
+
+onMounted(() => {
+  // 加载系统统计数据
+  loadSystemStats()
+})
+
+const loadSystemStats = async () => {
+  // TODO: 从API加载真实统计数据
+  console.log('加载系统统计数据')
+}
+</script>
+
+<style scoped>
+.admin-dashboard {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.stats-card {
+  cursor: pointer;
+  transition: transform 0.2s;
+}
+
+.stats-card:hover {
+  transform: translateY(-2px);
+}
+
+.stats-content {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.stats-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f0f9ff;
+}
+
+.stats-info {
+  flex: 1;
+}
+
+.stats-number {
+  font-size: 24px;
+  font-weight: 600;
+  color: #333;
+  line-height: 1;
+}
+
+.stats-label {
+  font-size: 14px;
+  color: #666;
+  margin-top: 4px;
+}
+
+.management-card {
+  height: 100%;
+}
+
+.management-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.management-content p {
+  margin: 0 0 16px 0;
+  color: #666;
+  font-size: 14px;
+  flex: 1;
+}
+
+.management-actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.recent-activity {
+  margin-bottom: 24px;
+}
+</style>

+ 61 - 0
src/views/admin/Logs.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="admin-logs">
+    <div class="page-header">
+      <h2>系统日志</h2>
+      <p>查看系统操作日志和安全事件</p>
+    </div>
+
+    <el-card>
+      <div class="coming-soon">
+        <el-icon size="64" color="#E6A23C"><Document /></el-icon>
+        <h3>系统日志功能</h3>
+        <p>此功能正在开发中,敬请期待...</p>
+        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 系统日志页面
+</script>
+
+<style scoped>
+.admin-logs {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.coming-soon {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.coming-soon h3 {
+  margin: 16px 0 8px 0;
+  color: #333;
+  font-size: 20px;
+}
+
+.coming-soon p {
+  margin: 0 0 24px 0;
+  color: #666;
+  font-size: 14px;
+}
+</style>

+ 61 - 0
src/views/admin/Settings.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="admin-settings">
+    <div class="page-header">
+      <h2>系统设置</h2>
+      <p>配置系统参数和安全策略</p>
+    </div>
+
+    <el-card>
+      <div class="coming-soon">
+        <el-icon size="64" color="#F56C6C"><Tools /></el-icon>
+        <h3>系统设置功能</h3>
+        <p>此功能正在开发中,敬请期待...</p>
+        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 系统设置页面
+</script>
+
+<style scoped>
+.admin-settings {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.coming-soon {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.coming-soon h3 {
+  margin: 16px 0 8px 0;
+  color: #333;
+  font-size: 20px;
+}
+
+.coming-soon p {
+  margin: 0 0 24px 0;
+  color: #666;
+  font-size: 14px;
+}
+</style>

+ 61 - 0
src/views/admin/Users.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="admin-users">
+    <div class="page-header">
+      <h2>用户管理</h2>
+      <p>管理系统用户账户、权限和状态</p>
+    </div>
+
+    <el-card>
+      <div class="coming-soon">
+        <el-icon size="64" color="#409EFF"><UserFilled /></el-icon>
+        <h3>用户管理功能</h3>
+        <p>此功能正在开发中,敬请期待...</p>
+        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 用户管理页面
+</script>
+
+<style scoped>
+.admin-users {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.coming-soon {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.coming-soon h3 {
+  margin: 16px 0 8px 0;
+  color: #333;
+  font-size: 20px;
+}
+
+.coming-soon p {
+  margin: 0 0 24px 0;
+  color: #666;
+  font-size: 14px;
+}
+</style>

+ 645 - 0
src/views/apps/Create.vue

@@ -0,0 +1,645 @@
+<template>
+  <div class="create-app-page">
+    <el-container>
+      <!-- 头部 -->
+      <el-header class="header">
+        <div class="header-left">
+          <h1>创建应用</h1>
+          <p>创建一个新的OAuth2应用来接入SSO认证</p>
+        </div>
+        <div class="header-right">
+          <el-button @click="$router.go(-1)">
+            <el-icon><ArrowLeft /></el-icon>
+            返回
+          </el-button>
+        </div>
+      </el-header>
+
+      <!-- 主内容 -->
+      <el-main class="main-content">
+        <div class="create-container">
+          <el-row :gutter="24">
+            <!-- 左侧:表单 -->
+            <el-col :span="16">
+              <el-card class="form-card">
+                <el-form
+                  ref="appFormRef"
+                  :model="appForm"
+                  :rules="appRules"
+                  label-width="120px"
+                  @submit.prevent="createApp"
+                >
+                  <!-- 基本信息 -->
+                  <div class="form-section">
+                    <h3>基本信息</h3>
+                    
+                    <el-form-item label="应用名称" prop="name">
+                      <el-input
+                        v-model="appForm.name"
+                        placeholder="请输入应用名称"
+                        maxlength="100"
+                        show-word-limit
+                      />
+                      <div class="form-tip">应用的显示名称,用户在授权时会看到</div>
+                    </el-form-item>
+
+                    <el-form-item label="应用描述" prop="description">
+                      <el-input
+                        v-model="appForm.description"
+                        type="textarea"
+                        :rows="3"
+                        placeholder="请输入应用描述"
+                        maxlength="500"
+                        show-word-limit
+                      />
+                      <div class="form-tip">简要描述应用的功能和用途</div>
+                    </el-form-item>
+
+                    <el-form-item label="应用图标" prop="icon_url">
+                      <div class="icon-upload">
+                        <el-upload
+                          class="icon-uploader"
+                          action="/api/v1/upload/icon"
+                          :headers="uploadHeaders"
+                          :show-file-list="false"
+                          :on-success="handleIconSuccess"
+                          :before-upload="beforeIconUpload"
+                        >
+                          <div v-if="appForm.icon_url" class="icon-preview">
+                            <img :src="appForm.icon_url" alt="应用图标" />
+                          </div>
+                          <div v-else class="icon-placeholder">
+                            <el-icon size="32"><Plus /></el-icon>
+                            <div>上传图标</div>
+                          </div>
+                        </el-upload>
+                        <div class="icon-tips">
+                          <p>• 推荐尺寸:128x128 像素</p>
+                          <p>• 支持格式:PNG、JPG</p>
+                          <p>• 文件大小:不超过 1MB</p>
+                        </div>
+                      </div>
+                    </el-form-item>
+                  </div>
+
+                  <!-- OAuth配置 -->
+                  <div class="form-section">
+                    <h3>OAuth配置</h3>
+                    
+                    <el-form-item label="回调URL" prop="redirect_uris">
+                      <div class="redirect-uris">
+                        <div
+                          v-for="(uri, index) in appForm.redirect_uris"
+                          :key="index"
+                          class="uri-item"
+                        >
+                          <el-input
+                            v-model="appForm.redirect_uris[index]"
+                            placeholder="https://example.com/auth/callback"
+                          />
+                          <el-button
+                            type="danger"
+                            size="small"
+                            @click="removeRedirectUri(index)"
+                          >
+                            <el-icon><Delete /></el-icon>
+                          </el-button>
+                        </div>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          @click="addRedirectUri"
+                        >
+                          <el-icon><Plus /></el-icon>
+                          添加回调URL
+                        </el-button>
+                      </div>
+                      <div class="form-tip">OAuth授权完成后的回调地址,必须使用HTTPS(开发环境可使用HTTP)</div>
+                    </el-form-item>
+
+                    <el-form-item label="权限范围" prop="scope">
+                      <el-checkbox-group v-model="appForm.scope">
+                        <el-checkbox label="profile">基本信息</el-checkbox>
+                        <el-checkbox label="email">邮箱地址</el-checkbox>
+                        <el-checkbox label="phone">手机号码</el-checkbox>
+                        <el-checkbox label="roles">用户角色</el-checkbox>
+                      </el-checkbox-group>
+                      <div class="form-tip">应用可以访问的用户信息范围</div>
+                    </el-form-item>
+                  </div>
+
+                  <!-- 安全设置 -->
+                  <div class="form-section">
+                    <h3>安全设置</h3>
+                    
+                    <el-form-item label="令牌有效期">
+                      <el-row :gutter="16">
+                        <el-col :span="12">
+                          <el-input-number
+                            v-model="appForm.access_token_expires"
+                            :min="300"
+                            :max="86400"
+                            :step="300"
+                            controls-position="right"
+                          />
+                          <div class="form-tip">访问令牌有效期(秒),建议2小时</div>
+                        </el-col>
+                        <el-col :span="12">
+                          <el-input-number
+                            v-model="appForm.refresh_token_expires"
+                            :min="86400"
+                            :max="2592000"
+                            :step="86400"
+                            controls-position="right"
+                          />
+                          <div class="form-tip">刷新令牌有效期(秒),建议30天</div>
+                        </el-col>
+                      </el-row>
+                    </el-form-item>
+
+                    <el-form-item label="应用类型">
+                      <el-radio-group v-model="appForm.is_trusted">
+                        <el-radio :label="false">普通应用</el-radio>
+                        <el-radio :label="true">受信任应用</el-radio>
+                      </el-radio-group>
+                      <div class="form-tip">受信任应用可以跳过用户授权确认步骤</div>
+                    </el-form-item>
+                  </div>
+
+                  <!-- 提交按钮 -->
+                  <el-form-item>
+                    <el-button
+                      type="primary"
+                      :loading="loading"
+                      @click="createApp"
+                    >
+                      创建应用
+                    </el-button>
+                    <el-button @click="resetForm">
+                      重置表单
+                    </el-button>
+                    <el-button @click="$router.go(-1)">
+                      取消
+                    </el-button>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+
+            <!-- 右侧:帮助信息 -->
+            <el-col :span="8">
+              <el-card class="help-card">
+                <template #header>
+                  <span>创建指南</span>
+                </template>
+                
+                <div class="help-content">
+                  <div class="help-section">
+                    <h4>1. 应用信息</h4>
+                    <p>填写应用的基本信息,包括名称、描述和图标。这些信息会在用户授权时显示。</p>
+                  </div>
+
+                  <div class="help-section">
+                    <h4>2. 回调URL</h4>
+                    <p>OAuth授权完成后用户会被重定向到这个地址。请确保URL的正确性和安全性。</p>
+                    <ul>
+                      <li>生产环境必须使用HTTPS</li>
+                      <li>可以配置多个回调URL</li>
+                      <li>不支持通配符和动态参数</li>
+                    </ul>
+                  </div>
+
+                  <div class="help-section">
+                    <h4>3. 权限范围</h4>
+                    <p>选择应用需要访问的用户信息类型:</p>
+                    <ul>
+                      <li><strong>profile</strong>: 用户基本信息(用户名、头像等)</li>
+                      <li><strong>email</strong>: 用户邮箱地址</li>
+                      <li><strong>phone</strong>: 用户手机号码</li>
+                      <li><strong>roles</strong>: 用户角色和权限</li>
+                    </ul>
+                  </div>
+
+                  <div class="help-section">
+                    <h4>4. 安全建议</h4>
+                    <ul>
+                      <li>妥善保管应用密钥</li>
+                      <li>定期轮换应用密钥</li>
+                      <li>监控应用的使用情况</li>
+                      <li>及时更新回调URL</li>
+                    </ul>
+                  </div>
+                </div>
+              </el-card>
+
+              <el-card class="example-card">
+                <template #header>
+                  <span>集成示例</span>
+                </template>
+                
+                <div class="example-content">
+                  <el-tabs>
+                    <el-tab-pane label="JavaScript" name="js">
+                      <pre><code>// OAuth授权URL
+const authUrl = 'https://sso.example.com/oauth/authorize?' +
+  'response_type=code&' +
+  'client_id=YOUR_APP_KEY&' +
+  'redirect_uri=YOUR_CALLBACK_URL&' +
+  'scope=profile email&' +
+  'state=random_string';
+
+// 跳转到授权页面
+window.location.href = authUrl;</code></pre>
+                    </el-tab-pane>
+                    <el-tab-pane label="Python" name="python">
+                      <pre><code># 使用requests库获取访问令牌
+import requests
+
+response = requests.post('https://sso.example.com/oauth/token', {
+    'grant_type': 'authorization_code',
+    'code': 'AUTHORIZATION_CODE',
+    'redirect_uri': 'YOUR_CALLBACK_URL',
+    'client_id': 'YOUR_APP_KEY',
+    'client_secret': 'YOUR_APP_SECRET'
+})</code></pre>
+                    </el-tab-pane>
+                  </el-tabs>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </div>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+import { getToken } from '@/utils/auth'
+
+const router = useRouter()
+
+const loading = ref(false)
+const appFormRef = ref<FormInstance>()
+
+// 应用表单数据
+const appForm = reactive({
+  name: '',
+  description: '',
+  icon_url: '',
+  redirect_uris: [''],
+  scope: ['profile'],
+  access_token_expires: 7200, // 2小时
+  refresh_token_expires: 2592000, // 30天
+  is_trusted: false
+})
+
+// 表单验证规则
+const appRules: FormRules = {
+  name: [
+    { required: true, message: '请输入应用名称', trigger: 'blur' },
+    { min: 2, max: 100, message: '应用名称长度在 2 到 100 个字符', trigger: 'blur' }
+  ],
+  description: [
+    { max: 500, message: '描述长度不能超过 500 个字符', trigger: 'blur' }
+  ],
+  redirect_uris: [
+    {
+      validator: (rule, value, callback) => {
+        const validUris = value.filter((uri: string) => uri.trim())
+        if (validUris.length === 0) {
+          callback(new Error('至少需要一个回调URL'))
+        } else {
+          const urlPattern = /^https?:\/\/.+/
+          const invalidUris = validUris.filter((uri: string) => !urlPattern.test(uri))
+          if (invalidUris.length > 0) {
+            callback(new Error('请输入有效的URL格式'))
+          } else {
+            callback()
+          }
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  scope: [
+    {
+      validator: (rule, value, callback) => {
+        if (value.length === 0) {
+          callback(new Error('至少需要选择一个权限范围'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+
+// 上传头部
+const uploadHeaders = {
+  Authorization: `Bearer ${getToken()}`
+}
+
+// 添加回调URL
+const addRedirectUri = () => {
+  appForm.redirect_uris.push('')
+}
+
+// 删除回调URL
+const removeRedirectUri = (index: number) => {
+  if (appForm.redirect_uris.length > 1) {
+    appForm.redirect_uris.splice(index, 1)
+  }
+}
+
+// 图标上传成功
+const handleIconSuccess = (response: any) => {
+  if (response.code === 0) {
+    appForm.icon_url = response.data.url
+    ElMessage.success('图标上传成功')
+  } else {
+    ElMessage.error('图标上传失败')
+  }
+}
+
+// 图标上传前验证
+const beforeIconUpload = (file: File) => {
+  const isImage = file.type === 'image/jpeg' || file.type === 'image/png'
+  const isLt1M = file.size / 1024 / 1024 < 1
+
+  if (!isImage) {
+    ElMessage.error('图标只能是 JPG/PNG 格式!')
+    return false
+  }
+  if (!isLt1M) {
+    ElMessage.error('图标大小不能超过 1MB!')
+    return false
+  }
+  return true
+}
+
+// 创建应用
+const createApp = async () => {
+  if (!appFormRef.value) return
+  
+  try {
+    await appFormRef.value.validate()
+    loading.value = true
+    
+    // 过滤空的回调URL
+    const formData = {
+      ...appForm,
+      redirect_uris: appForm.redirect_uris.filter(uri => uri.trim())
+    }
+    
+    console.log('创建应用:', formData)
+    
+    // 调用API创建应用
+    const response = await fetch('/api/v1/apps', {
+      method: 'POST',
+      headers: {
+        'Authorization': `Bearer ${getToken()}`,
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify(formData)
+    })
+    
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+    }
+    
+    const result = await response.json()
+    console.log('创建应用响应:', result)
+    
+    if (result.code === 0) {
+      ElMessage.success('应用创建成功')
+      
+      // 跳转到应用列表
+      router.push('/apps')
+    } else {
+      throw new Error(result.message || '创建应用失败')
+    }
+    
+  } catch (error) {
+    console.error('创建应用失败:', error)
+    ElMessage.error(`创建应用失败: ${error.message}`)
+    
+    // 如果是认证错误,跳转到登录页
+    if (error.message.includes('401') || error.message.includes('Unauthorized')) {
+      localStorage.removeItem('access_token')
+      localStorage.removeItem('user_info')
+      router.push('/auth/login')
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  if (appFormRef.value) {
+    appFormRef.value.resetFields()
+  }
+  
+  // 重置为默认值
+  Object.assign(appForm, {
+    name: '',
+    description: '',
+    icon_url: '',
+    redirect_uris: [''],
+    scope: ['profile'],
+    access_token_expires: 7200,
+    refresh_token_expires: 2592000,
+    is_trusted: false
+  })
+  
+  ElMessage.info('表单已重置')
+}
+</script>
+
+<style scoped>
+.create-app-page {
+  height: 100vh;
+  background: #f5f5f5;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: #fff;
+  border-bottom: 1px solid #e6e6e6;
+  padding: 0 20px;
+}
+
+.header-left h1 {
+  margin: 0 0 4px 0;
+  color: #333;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.header-left p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.main-content {
+  padding: 20px;
+}
+
+.create-container {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.form-card {
+  margin-bottom: 20px;
+}
+
+.form-section {
+  margin-bottom: 32px;
+  padding-bottom: 24px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.form-section:last-child {
+  border-bottom: none;
+  margin-bottom: 0;
+  padding-bottom: 0;
+}
+
+.form-section h3 {
+  margin: 0 0 16px 0;
+  color: #333;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #999;
+  margin-top: 4px;
+  line-height: 1.4;
+}
+
+.icon-upload {
+  display: flex;
+  gap: 16px;
+  align-items: flex-start;
+}
+
+.icon-uploader {
+  flex-shrink: 0;
+}
+
+.icon-preview,
+.icon-placeholder {
+  width: 80px;
+  height: 80px;
+  border: 2px dashed #d9d9d9;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: border-color 0.3s;
+}
+
+.icon-preview:hover,
+.icon-placeholder:hover {
+  border-color: #409eff;
+}
+
+.icon-preview img {
+  width: 64px;
+  height: 64px;
+  border-radius: 4px;
+}
+
+.icon-placeholder {
+  color: #999;
+  font-size: 12px;
+}
+
+.icon-tips {
+  flex: 1;
+}
+
+.icon-tips p {
+  margin: 0 0 4px 0;
+  font-size: 12px;
+  color: #666;
+}
+
+.redirect-uris {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.uri-item {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.help-card,
+.example-card {
+  margin-bottom: 20px;
+}
+
+.help-content {
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+.help-section {
+  margin-bottom: 20px;
+}
+
+.help-section:last-child {
+  margin-bottom: 0;
+}
+
+.help-section h4 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.help-section p {
+  margin: 0 0 8px 0;
+  color: #666;
+}
+
+.help-section ul {
+  margin: 8px 0;
+  padding-left: 16px;
+  color: #666;
+}
+
+.help-section li {
+  margin-bottom: 4px;
+}
+
+.example-content pre {
+  background: #f8f9fa;
+  padding: 12px;
+  border-radius: 4px;
+  font-size: 12px;
+  line-height: 1.4;
+  overflow-x: auto;
+}
+
+.example-content code {
+  color: #333;
+}
+</style>

+ 993 - 0
src/views/apps/Index.vue

@@ -0,0 +1,993 @@
+<template>
+  <div class="apps-content">
+    <div class="page-header">
+      <div class="header-left">
+        <h2>我的应用</h2>
+        <p>管理您的OAuth2应用和API密钥</p>
+      </div>
+      <div class="header-right">
+        <el-button type="primary" @click="$router.push('/apps/create')">
+          <el-icon><Plus /></el-icon>
+          创建应用
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 搜索和筛选 -->
+    <el-card class="search-card">
+          <el-row :gutter="16">
+            <el-col :span="8">
+              <el-input
+                v-model="searchForm.keyword"
+                placeholder="搜索应用名称或描述"
+                clearable
+                @input="handleSearch"
+              >
+                <template #prefix>
+                  <el-icon><Search /></el-icon>
+                </template>
+              </el-input>
+            </el-col>
+            <el-col :span="6">
+              <el-select
+                v-model="searchForm.status"
+                placeholder="应用状态"
+                clearable
+                @change="handleSearch"
+              >
+                <el-option label="全部" value="" />
+                <el-option label="正常" value="active" />
+                <el-option label="已禁用" value="inactive" />
+              </el-select>
+            </el-col>
+            <el-col :span="6">
+              <el-select
+                v-model="searchForm.trusted"
+                placeholder="信任状态"
+                clearable
+                @change="handleSearch"
+              >
+                <el-option label="全部" value="" />
+                <el-option label="受信任" value="true" />
+                <el-option label="不受信任" value="false" />
+              </el-select>
+            </el-col>
+            <el-col :span="4">
+              <el-button type="primary" @click="handleSearch">
+                <el-icon><Search /></el-icon>
+                搜索
+              </el-button>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 应用列表 -->
+        <div class="apps-grid">
+          <el-card
+            v-for="app in apps"
+            :key="app.id"
+            class="app-card"
+            :class="{ 'app-disabled': !app.is_active }"
+          >
+            <div class="app-header">
+              <div class="app-icon">
+                <img v-if="app.icon_url" :src="app.icon_url" :alt="app.name" />
+                <el-icon v-else size="32"><Grid /></el-icon>
+              </div>
+              <div class="app-info">
+                <h3 class="app-name">{{ app.name }}</h3>
+                <p class="app-description">{{ app.description || '暂无描述' }}</p>
+              </div>
+              <div class="app-status">
+                <el-tag :type="app.is_active ? 'success' : 'danger'">
+                  {{ app.is_active ? '正常' : '已禁用' }}
+                </el-tag>
+                <el-tag v-if="app.is_trusted" type="warning" size="small">
+                  受信任
+                </el-tag>
+              </div>
+            </div>
+
+            <div class="app-stats">
+              <div class="stat-item">
+                <span class="stat-label">创建时间:</span>
+                <span class="stat-value">{{ formatDate(app.created_at) }}</span>
+              </div>
+              <div class="stat-item">
+                <span class="stat-label">更新时间:</span>
+                <span class="stat-value">{{ formatDate(app.updated_at) }}</span>
+              </div>
+              <div class="stat-item">
+                <span class="stat-label">回调URL:</span>
+                <span class="stat-value">{{ app.redirect_uris?.length || 0 }} 个</span>
+              </div>
+            </div>
+
+            <div class="app-actions">
+              <el-button
+                size="small"
+                @click="viewApp(app)"
+              >
+                <el-icon><View /></el-icon>
+                查看
+              </el-button>
+              <el-button
+                size="small"
+                type="primary"
+                @click="editApp(app)"
+              >
+                <el-icon><Edit /></el-icon>
+                编辑
+              </el-button>
+              <el-button
+                size="small"
+                type="warning"
+                @click="showSecret(app)"
+              >
+                <el-icon><Key /></el-icon>
+                密钥
+              </el-button>
+              <el-dropdown @command="(command) => handleAppAction(command, app)">
+                <el-button size="small">
+                  更多<el-icon><ArrowDown /></el-icon>
+                </el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item command="toggle">
+                      {{ app.is_active ? '禁用' : '启用' }}
+                    </el-dropdown-item>
+                    <el-dropdown-item command="reset-secret">
+                      重置密钥
+                    </el-dropdown-item>
+                    <el-dropdown-item command="logs">
+                      查看日志
+                    </el-dropdown-item>
+                    <el-dropdown-item command="delete" divided>
+                      删除应用
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </el-card>
+
+          <!-- 空状态 -->
+          <div v-if="apps.length === 0" class="empty-state">
+            <el-empty description="暂无应用">
+              <el-button type="primary" @click="$router.push('/apps/create')">
+                创建第一个应用
+              </el-button>
+            </el-empty>
+          </div>
+        </div>
+
+        <!-- 分页 -->
+        <div v-if="total > 0" class="pagination">
+          <el-pagination
+            v-model:current-page="currentPage"
+            v-model:page-size="pageSize"
+            :total="total"
+            :page-sizes="[10, 20, 50, 100]"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+          />
+        </div>
+
+    <!-- 应用详情对话框 -->
+    <el-dialog
+      v-model="showAppDetail"
+      :title="selectedApp?.name"
+      width="600px"
+    >
+      <div v-if="selectedApp" class="app-detail">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="应用ID">
+            {{ selectedApp.id }}
+          </el-descriptions-item>
+          <el-descriptions-item label="应用Key">
+            {{ selectedApp.app_key }}
+          </el-descriptions-item>
+          <el-descriptions-item label="应用名称">
+            {{ selectedApp.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="selectedApp.is_active ? 'success' : 'danger'">
+              {{ selectedApp.is_active ? '正常' : '已禁用' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="描述" :span="2">
+            {{ selectedApp.description || '暂无描述' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="回调URL" :span="2">
+            <div v-for="(url, index) in selectedApp.redirect_uris" :key="index">
+              <el-tag size="small">{{ url }}</el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="权限范围" :span="2">
+            <div v-for="(scope, index) in selectedApp.scope" :key="index">
+              <el-tag size="small" type="info">{{ scope }}</el-tag>
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ formatDate(selectedApp.created_at) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ formatDate(selectedApp.updated_at) }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+    </el-dialog>
+
+    <!-- 应用密钥对话框 -->
+    <el-dialog
+      v-model="showAppSecret"
+      title="应用密钥"
+      width="500px"
+    >
+      <div v-if="selectedApp" class="app-secret">
+        <el-alert
+          title="安全提示"
+          type="warning"
+          description="请妥善保管您的应用密钥,不要在客户端代码中暴露密钥信息。"
+          :closable="false"
+          show-icon
+        />
+        
+        <div class="secret-item">
+          <label>App Key:</label>
+          <el-input
+            :value="selectedApp.app_key"
+            readonly
+          >
+            <template #append>
+              <el-button @click="copyToClipboard(selectedApp.app_key)">
+                <el-icon><CopyDocument /></el-icon>
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+
+        <div class="secret-item">
+          <label>App Secret:</label>
+          <el-input
+            :value="showSecretValue ? selectedApp.app_secret : '••••••••••••••••'"
+            readonly
+          >
+            <template #prepend>
+              <el-button @click="showSecretValue = !showSecretValue">
+                <el-icon>{{ showSecretValue ? 'Hide' : 'View' }}</el-icon>
+              </el-button>
+            </template>
+            <template #append>
+              <el-button @click="copyToClipboard(selectedApp.app_secret)">
+                <el-icon><CopyDocument /></el-icon>
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 编辑应用对话框 -->
+    <el-dialog
+      v-model="showEditApp"
+      title="编辑应用"
+      width="600px"
+      @close="resetEditForm"
+    >
+      <el-form
+        ref="editFormRef"
+        :model="editForm"
+        :rules="editRules"
+        label-width="100px"
+      >
+        <el-form-item label="应用名称" prop="name">
+          <el-input
+            v-model="editForm.name"
+            placeholder="请输入应用名称"
+            maxlength="50"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="应用描述" prop="description">
+          <el-input
+            v-model="editForm.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入应用描述"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="应用图标" prop="icon_url">
+          <el-input
+            v-model="editForm.icon_url"
+            placeholder="请输入应用图标URL(可选)"
+          />
+        </el-form-item>
+
+        <el-form-item label="回调URL" prop="redirect_uris">
+          <div class="redirect-uris">
+            <div
+              v-for="(uri, index) in editForm.redirect_uris"
+              :key="index"
+              class="uri-item"
+            >
+              <el-input
+                v-model="editForm.redirect_uris[index]"
+                placeholder="请输入回调URL"
+              >
+                <template #append>
+                  <el-button
+                    @click="removeRedirectUri(index)"
+                    :disabled="editForm.redirect_uris.length <= 1"
+                  >
+                    <el-icon><Delete /></el-icon>
+                  </el-button>
+                </template>
+              </el-input>
+            </div>
+            <el-button
+              type="dashed"
+              @click="addRedirectUri"
+              style="width: 100%; margin-top: 8px;"
+            >
+              <el-icon><Plus /></el-icon>
+              添加回调URL
+            </el-button>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="权限范围" prop="scope">
+          <el-checkbox-group v-model="editForm.scope">
+            <el-checkbox label="profile">用户资料</el-checkbox>
+            <el-checkbox label="email">邮箱地址</el-checkbox>
+            <el-checkbox label="openid">OpenID</el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+
+        <el-form-item label="受信任应用">
+          <el-switch
+            v-model="editForm.is_trusted"
+            active-text="是"
+            inactive-text="否"
+          />
+          <div class="form-tip">
+            受信任应用可以跳过用户授权确认步骤
+          </div>
+        </el-form-item>
+
+        <el-form-item label="Token过期时间">
+          <el-row :gutter="16">
+            <el-col :span="12">
+              <el-input
+                v-model.number="editForm.access_token_expires"
+                type="number"
+                :min="300"
+                :max="86400"
+              >
+                <template #append>秒</template>
+              </el-input>
+              <div class="form-tip">访问令牌过期时间(300-86400秒)</div>
+            </el-col>
+            <el-col :span="12">
+              <el-input
+                v-model.number="editForm.refresh_token_expires"
+                type="number"
+                :min="86400"
+                :max="2592000"
+              >
+                <template #append>秒</template>
+              </el-input>
+              <div class="form-tip">刷新令牌过期时间(1-30天)</div>
+            </el-col>
+          </el-row>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showEditApp = false">取消</el-button>
+          <el-button type="primary" @click="submitEditForm" :loading="editLoading">
+            保存
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import request from '@/api/request'
+
+// 响应式数据
+const loading = ref(false)
+const apps = ref<any[]>([])
+const total = ref(0)
+const currentPage = ref(1)
+const pageSize = ref(20)
+
+const showAppDetail = ref(false)
+const showAppSecret = ref(false)
+const showEditApp = ref(false)
+const showSecretValue = ref(false)
+const selectedApp = ref<any>(null)
+const editLoading = ref(false)
+const editFormRef = ref()
+
+// 编辑表单
+const editForm = reactive({
+  name: '',
+  description: '',
+  icon_url: '',
+  redirect_uris: [''],
+  scope: ['profile', 'email'],
+  is_trusted: false,
+  access_token_expires: 7200,
+  refresh_token_expires: 2592000
+})
+
+// 编辑表单验证规则
+const editRules = {
+  name: [
+    { required: true, message: '请输入应用名称', trigger: 'blur' },
+    { min: 2, max: 50, message: '应用名称长度在 2 到 50 个字符', trigger: 'blur' }
+  ],
+  description: [
+    { max: 200, message: '描述不能超过 200 个字符', trigger: 'blur' }
+  ],
+  redirect_uris: [
+    { 
+      validator: (rule: any, value: string[], callback: Function) => {
+        if (!value || value.length === 0) {
+          callback(new Error('至少需要一个回调URL'))
+        } else if (value.some(uri => !uri.trim())) {
+          callback(new Error('回调URL不能为空'))
+        } else if (value.some(uri => !uri.startsWith('http://') && !uri.startsWith('https://'))) {
+          callback(new Error('回调URL必须以 http:// 或 https:// 开头'))
+        } else {
+          callback()
+        }
+      }, 
+      trigger: 'blur' 
+    }
+  ],
+  scope: [
+    { 
+      validator: (rule: any, value: string[], callback: Function) => {
+        if (!value || value.length === 0) {
+          callback(new Error('至少需要选择一个权限范围'))
+        } else {
+          callback()
+        }
+      }, 
+      trigger: 'change' 
+    }
+  ]
+}
+
+// 搜索表单
+const searchForm = reactive({
+  keyword: '',
+  status: '',
+  trusted: ''
+})
+
+// 初始化数据
+onMounted(() => {
+  loadApps()
+})
+
+// 加载应用列表
+const loadApps = async () => {
+  loading.value = true
+  try {
+    // 调用真实API
+    const params = {
+      page: currentPage.value,
+      page_size: pageSize.value,
+      keyword: searchForm.keyword || undefined,
+      status: searchForm.status || undefined,
+      trusted: searchForm.trusted || undefined
+    }
+    
+    // 移除空值参数
+    Object.keys(params).forEach(key => {
+      if (params[key] === undefined || params[key] === '') {
+        delete params[key]
+      }
+    })
+    
+    console.log('加载应用列表,参数:', params)
+    
+    const result = await request.get('/api/v1/apps', { params })
+    console.log('API响应:', result)
+    
+    if (result.code === 0) {
+      apps.value = result.data.items || []
+      total.value = result.data.total || 0
+      console.log(`加载了 ${apps.value.length} 个应用,总计 ${total.value} 个`)
+    } else {
+      throw new Error(result.message || '获取应用列表失败')
+    }
+    
+  } catch (error) {
+    console.error('加载应用列表失败:', error)
+    ElMessage.error(`加载应用列表失败: ${error.message}`)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadApps()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  loadApps()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadApps()
+}
+
+// 查看应用详情
+const viewApp = (app: any) => {
+  selectedApp.value = app
+  showAppDetail.value = true
+}
+
+// 编辑应用
+const editApp = (app: any) => {
+  selectedApp.value = app
+  
+  // 填充编辑表单
+  editForm.name = app.name
+  editForm.description = app.description || ''
+  editForm.icon_url = app.icon_url || ''
+  editForm.redirect_uris = [...(app.redirect_uris || [''])]
+  editForm.scope = [...(app.scope || ['profile', 'email'])]
+  editForm.is_trusted = app.is_trusted || false
+  editForm.access_token_expires = app.access_token_expires || 7200
+  editForm.refresh_token_expires = app.refresh_token_expires || 2592000
+  
+  // 确保至少有一个回调URL输入框
+  if (editForm.redirect_uris.length === 0) {
+    editForm.redirect_uris = ['']
+  }
+  
+  showEditApp.value = true
+}
+
+// 显示应用密钥
+const showSecret = async (app: any) => {
+  try {
+    // 获取包含密钥的完整应用信息
+    const result = await request.get(`/api/v1/apps/${app.id}`)
+    
+    if (result.code === 0) {
+      selectedApp.value = result.data
+      showSecretValue.value = false
+      showAppSecret.value = true
+    } else {
+      throw new Error(result.message || '获取应用详情失败')
+    }
+  } catch (error) {
+    console.error('获取应用详情失败:', error)
+    ElMessage.error(`获取应用详情失败: ${error.message}`)
+  }
+}
+
+// 应用操作处理
+const handleAppAction = async (command: string, app: any) => {
+  switch (command) {
+    case 'toggle':
+      await toggleAppStatus(app)
+      break
+    case 'reset-secret':
+      await resetAppSecret(app)
+      break
+    case 'logs':
+      viewAppLogs(app)
+      break
+    case 'delete':
+      await deleteApp(app)
+      break
+  }
+}
+
+// 切换应用状态
+const toggleAppStatus = async (app: any) => {
+  try {
+    const action = app.is_active ? '禁用' : '启用'
+    await ElMessageBox.confirm(`确定要${action}应用 "${app.name}" 吗?`, '确认操作', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    
+    // 调用API切换状态
+    const result = await request.put(`/api/v1/apps/${app.id}/status`, {
+      is_active: !app.is_active
+    })
+    
+    if (result.code === 0) {
+      app.is_active = !app.is_active
+      ElMessage.success(`应用已${action}`)
+    } else {
+      throw new Error(result.message || `${action}应用失败`)
+    }
+    
+  } catch (error) {
+    if (error.name !== 'cancel') {
+      console.error('切换应用状态失败:', error)
+      ElMessage.error(`操作失败: ${error.message}`)
+    }
+  }
+}
+
+// 重置应用密钥
+const resetAppSecret = async (app: any) => {
+  try {
+    await ElMessageBox.confirm(
+      '重置密钥后,使用旧密钥的应用将无法正常工作,确定要继续吗?',
+      '重置应用密钥',
+      {
+        confirmButtonText: '确定重置',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    // 调用API重置密钥
+    const result = await request.post(`/api/v1/apps/${app.id}/reset-secret`)
+    
+    if (result.code === 0) {
+      // 更新应用的密钥
+      app.app_secret = result.data.app_secret
+      ElMessage.success('应用密钥已重置')
+    } else {
+      throw new Error(result.message || '重置密钥失败')
+    }
+    
+  } catch (error) {
+    if (error.name !== 'cancel') {
+      console.error('重置密钥失败:', error)
+      ElMessage.error(`重置失败: ${error.message}`)
+    }
+  }
+}
+
+// 查看应用日志
+const viewAppLogs = (app: any) => {
+  // TODO: 跳转到日志页面
+  ElMessage.info('日志查看功能开发中...')
+}
+
+// 删除应用
+const deleteApp = async (app: any) => {
+  try {
+    await ElMessageBox.confirm(
+      `删除应用 "${app.name}" 后将无法恢复,确定要删除吗?`,
+      '删除应用',
+      {
+        confirmButtonText: '确定删除',
+        cancelButtonText: '取消',
+        type: 'error'
+      }
+    )
+    
+    // 调用API删除应用
+    const result = await request.delete(`/api/v1/apps/${app.id}`)
+    
+    if (result.code === 0) {
+      ElMessage.success('应用已删除')
+      loadApps() // 重新加载列表
+    } else {
+      throw new Error(result.message || '删除应用失败')
+    }
+    
+  } catch (error) {
+    if (error.name !== 'cancel') {
+      console.error('删除应用失败:', error)
+      ElMessage.error(`删除失败: ${error.message}`)
+    }
+  }
+}
+
+// 复制到剪贴板
+const copyToClipboard = async (text: string) => {
+  try {
+    await navigator.clipboard.writeText(text)
+    ElMessage.success('已复制到剪贴板')
+  } catch (error) {
+    ElMessage.error('复制失败')
+  }
+}
+
+// 格式化日期
+const formatDate = (date: string) => {
+  return new Date(date).toLocaleString('zh-CN')
+}
+
+// 重置编辑表单
+const resetEditForm = () => {
+  editForm.name = ''
+  editForm.description = ''
+  editForm.icon_url = ''
+  editForm.redirect_uris = ['']
+  editForm.scope = ['profile', 'email']
+  editForm.is_trusted = false
+  editForm.access_token_expires = 7200
+  editForm.refresh_token_expires = 2592000
+  
+  // 清除表单验证
+  if (editFormRef.value) {
+    editFormRef.value.clearValidate()
+  }
+}
+
+// 添加回调URL
+const addRedirectUri = () => {
+  editForm.redirect_uris.push('')
+}
+
+// 移除回调URL
+const removeRedirectUri = (index: number) => {
+  if (editForm.redirect_uris.length > 1) {
+    editForm.redirect_uris.splice(index, 1)
+  }
+}
+
+// 提交编辑表单
+const submitEditForm = async () => {
+  if (!editFormRef.value) return
+  
+  try {
+    await editFormRef.value.validate()
+    
+    editLoading.value = true
+    
+    // 过滤空的回调URL
+    const filteredUris = editForm.redirect_uris.filter(uri => uri.trim())
+    
+    const updateData = {
+      name: editForm.name.trim(),
+      description: editForm.description.trim(),
+      icon_url: editForm.icon_url.trim(),
+      redirect_uris: filteredUris,
+      scope: editForm.scope,
+      is_trusted: editForm.is_trusted,
+      access_token_expires: editForm.access_token_expires,
+      refresh_token_expires: editForm.refresh_token_expires
+    }
+    
+    const result = await request.put(`/api/v1/apps/${selectedApp.value.id}`, updateData)
+    
+    if (result.code === 0) {
+      ElMessage.success('应用更新成功')
+      showEditApp.value = false
+      
+      // 更新列表中的应用数据
+      const index = apps.value.findIndex(app => app.id === selectedApp.value.id)
+      if (index !== -1) {
+        apps.value[index] = { ...apps.value[index], ...result.data }
+      }
+      
+      // 如果当前选中的应用是被编辑的应用,也更新选中的应用数据
+      if (selectedApp.value && selectedApp.value.id === result.data.id) {
+        selectedApp.value = { ...selectedApp.value, ...result.data }
+      }
+    } else {
+      throw new Error(result.message || '更新应用失败')
+    }
+    
+  } catch (error) {
+    console.error('更新应用失败:', error)
+    ElMessage.error(`更新失败: ${error.message}`)
+  } finally {
+    editLoading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.apps-content {
+  padding: 20px;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 20px;
+}
+
+.header-left h2 {
+  margin: 0 0 4px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.header-left p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.search-card {
+  margin-bottom: 20px;
+}
+
+.apps-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+  gap: 20px;
+  margin-bottom: 20px;
+}
+
+.app-card {
+  transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.app-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.app-disabled {
+  opacity: 0.6;
+}
+
+.app-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.app-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 8px;
+  background: #f0f9ff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.app-icon img {
+  width: 32px;
+  height: 32px;
+  border-radius: 4px;
+}
+
+.app-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.app-name {
+  margin: 0 0 4px 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.app-description {
+  margin: 0;
+  font-size: 14px;
+  color: #666;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.app-status {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  align-items: flex-end;
+}
+
+.app-stats {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+  margin-bottom: 16px;
+  padding: 12px;
+  background: #f8f9fa;
+  border-radius: 6px;
+}
+
+.stat-item {
+  text-align: center;
+}
+
+.stat-label {
+  display: block;
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  display: block;
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.app-actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.empty-state {
+  grid-column: 1 / -1;
+  text-align: center;
+  padding: 40px;
+}
+
+.pagination {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+}
+
+.app-detail {
+  margin-top: 16px;
+}
+
+.app-secret {
+  margin-top: 16px;
+}
+
+.secret-item {
+  margin-bottom: 16px;
+}
+
+.secret-item label {
+  display: block;
+  margin-bottom: 8px;
+  font-weight: 600;
+  color: #333;
+}
+
+.redirect-uris {
+  width: 100%;
+}
+
+.uri-item {
+  margin-bottom: 8px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #666;
+  margin-top: 4px;
+  line-height: 1.4;
+}
+
+.dialog-footer {
+  text-align: right;
+}
+</style>

+ 252 - 0
src/views/auth/Login.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="login-container">
+    <div class="login-box">
+      <div class="login-header">
+        <h1>SSO 统一认证中心</h1>
+        <p>请登录您的账户</p>
+      </div>
+      
+      <el-form
+        ref="loginFormRef"
+        :model="loginForm"
+        :rules="loginRules"
+        class="login-form"
+        @submit.prevent="handleLogin"
+      >
+        <el-form-item prop="username">
+          <el-input
+            v-model="loginForm.username"
+            placeholder="用户名或邮箱"
+            size="large"
+            prefix-icon="User"
+            clearable
+          />
+        </el-form-item>
+        
+        <el-form-item prop="password">
+          <el-input
+            v-model="loginForm.password"
+            type="password"
+            placeholder="密码"
+            size="large"
+            prefix-icon="Lock"
+            show-password
+            clearable
+            @keyup.enter="handleLogin"
+          />
+        </el-form-item>
+        
+        <el-form-item v-if="showCaptcha" prop="captcha">
+          <div class="captcha-container">
+            <el-input
+              v-model="loginForm.captcha"
+              placeholder="验证码"
+              size="large"
+              style="flex: 1"
+            />
+            <img
+              :src="captchaImage"
+              class="captcha-image"
+              @click="refreshCaptcha"
+              alt="验证码"
+            />
+          </div>
+        </el-form-item>
+        
+        <el-form-item>
+          <div class="login-options">
+            <el-checkbox v-model="loginForm.remember_me">
+              记住我
+            </el-checkbox>
+            <el-link type="primary" @click="$router.push('/forgot-password')">
+              忘记密码?
+            </el-link>
+          </div>
+        </el-form-item>
+        
+        <el-form-item>
+          <el-button
+            type="primary"
+            size="large"
+            :loading="loading"
+            class="login-button"
+            @click="handleLogin"
+          >
+            登录
+          </el-button>
+        </el-form-item>
+        
+        <el-form-item>
+          <div class="register-link">
+            还没有账户?
+            <el-link type="primary" @click="$router.push('/register')">
+              立即注册
+            </el-link>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+import { authApi } from '@/api/auth'
+import type { LoginForm } from '@/types/auth'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+
+const loginFormRef = ref<FormInstance>()
+const loading = ref(false)
+const showCaptcha = ref(false)
+const captchaImage = ref('')
+
+const loginForm = reactive<LoginForm>({
+  username: '',
+  password: '',
+  remember_me: false,
+  captcha: ''
+})
+
+const loginRules: FormRules = {
+  username: [
+    { required: true, message: '请输入用户名或邮箱', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ],
+  captcha: [
+    { required: true, message: '请输入验证码', trigger: 'blur' }
+  ]
+}
+
+// 获取验证码
+const getCaptcha = async () => {
+  try {
+    const response = await authApi.getCaptcha()
+    captchaImage.value = response.data.captcha_image
+    showCaptcha.value = true
+  } catch (error) {
+    console.error('获取验证码失败:', error)
+  }
+}
+
+// 刷新验证码
+const refreshCaptcha = () => {
+  getCaptcha()
+}
+
+// 处理登录
+const handleLogin = async () => {
+  if (!loginFormRef.value) return
+  
+  try {
+    await loginFormRef.value.validate()
+    loading.value = true
+    
+    await authStore.login(loginForm)
+    
+    ElMessage.success('登录成功')
+    
+    // 重定向到原来要访问的页面或默认页面
+    const redirect = route.query.redirect as string
+    router.push(redirect || '/dashboard')
+    
+  } catch (error: any) {
+    console.error('登录失败:', error)
+    
+    // 如果是验证码错误或需要验证码,显示验证码
+    if (error.message?.includes('验证码') || !showCaptcha.value) {
+      await getCaptcha()
+    }
+    
+    ElMessage.error(error.message || '登录失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  // 如果已经登录,重定向到仪表盘
+  if (authStore.isAuthenticated) {
+    router.push('/dashboard')
+  }
+})
+</script>
+
+<style scoped>
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.login-box {
+  width: 400px;
+  padding: 40px;
+  background: white;
+  border-radius: 10px;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 30px;
+}
+
+.login-header h1 {
+  color: #333;
+  margin-bottom: 10px;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.login-header p {
+  color: #666;
+  font-size: 14px;
+}
+
+.login-form {
+  width: 100%;
+}
+
+.captcha-container {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.captcha-image {
+  width: 100px;
+  height: 40px;
+  cursor: pointer;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+
+.login-options {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+}
+
+.login-button {
+  width: 100%;
+}
+
+.register-link {
+  text-align: center;
+  width: 100%;
+  color: #666;
+  font-size: 14px;
+}
+</style>

+ 18 - 0
src/views/auth/OAuthCallback.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="callback-container">
+    <el-card>
+      <h2>OAuth回调处理中...</h2>
+      <el-icon class="is-loading"><Loading /></el-icon>
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+.callback-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  text-align: center;
+}
+</style>

+ 27 - 0
src/views/auth/Register.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="register-container">
+    <el-card class="register-box">
+      <h2>用户注册</h2>
+      <p>注册功能开发中...</p>
+      <el-button type="primary" @click="$router.push('/login')">
+        返回登录
+      </el-button>
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+.register-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.register-box {
+  width: 400px;
+  text-align: center;
+  padding: 40px;
+}
+</style>

+ 232 - 0
src/views/dashboard/Index.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="dashboard-content">
+    <div class="welcome-section">
+      <h2>欢迎回来,{{ authStore.user?.username }}!</h2>
+      <p>这是您的SSO认证中心控制台</p>
+    </div>
+
+    <!-- 统计卡片 -->
+    <div class="stats-grid">
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#409EFF"><Grid /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ stats.appCount }}</div>
+            <div class="stats-label">我的应用</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#67C23A"><Key /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ stats.tokenCount }}</div>
+            <div class="stats-label">活跃令牌</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#E6A23C"><Clock /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ stats.loginCount }}</div>
+            <div class="stats-label">今日登录</div>
+          </div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-icon">
+            <el-icon size="24" color="#F56C6C"><Warning /></el-icon>
+          </div>
+          <div class="stats-info">
+            <div class="stats-number">{{ stats.alertCount }}</div>
+            <div class="stats-label">安全警告</div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+
+    <!-- 快速操作 -->
+    <el-card class="quick-actions">
+      <template #header>
+        <span>快速操作</span>
+      </template>
+      <div class="actions-grid">
+        <el-button type="primary" @click="$router.push('/apps/create')">
+          <el-icon><Plus /></el-icon>
+          创建应用
+        </el-button>
+        <el-button @click="$router.push('/profile')">
+          <el-icon><User /></el-icon>
+          编辑资料
+        </el-button>
+        <el-button @click="$router.push('/apps')">
+          <el-icon><Grid /></el-icon>
+          管理应用
+        </el-button>
+      </div>
+    </el-card>
+
+    <!-- 最近活动 -->
+    <el-card class="recent-activity">
+      <template #header>
+        <span>最近活动</span>
+      </template>
+      <el-timeline>
+        <el-timeline-item
+          v-for="activity in recentActivities"
+          :key="activity.id"
+          :timestamp="activity.timestamp"
+          :type="activity.type"
+        >
+          {{ activity.description }}
+        </el-timeline-item>
+      </el-timeline>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { useAuthStore } from '@/stores/auth'
+
+const router = useRouter()
+const authStore = useAuthStore()
+
+// 统计数据
+const stats = ref({
+  appCount: 3,
+  tokenCount: 12,
+  loginCount: 8,
+  alertCount: 0
+})
+
+// 最近活动
+const recentActivities = ref([
+  {
+    id: 1,
+    description: '创建了新应用 "测试应用"',
+    timestamp: '2024-01-15 14:30',
+    type: 'primary'
+  },
+  {
+    id: 2,
+    description: '更新了个人资料',
+    timestamp: '2024-01-15 10:20',
+    type: 'success'
+  },
+  {
+    id: 3,
+    description: '登录系统',
+    timestamp: '2024-01-15 09:00',
+    type: 'info'
+  }
+])
+
+onMounted(() => {
+  // 加载统计数据
+  loadStats()
+})
+
+const loadStats = async () => {
+  // TODO: 从API加载真实统计数据
+  console.log('加载统计数据')
+}
+</script>
+
+<style scoped>
+.dashboard-content {
+  padding: 20px;
+}
+
+.welcome-section {
+  margin-bottom: 24px;
+}
+
+.welcome-section h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.welcome-section p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.stats-card {
+  cursor: pointer;
+  transition: transform 0.2s;
+}
+
+.stats-card:hover {
+  transform: translateY(-2px);
+}
+
+.stats-content {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.stats-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f0f9ff;
+}
+
+.stats-info {
+  flex: 1;
+}
+
+.stats-number {
+  font-size: 24px;
+  font-weight: 600;
+  color: #333;
+  line-height: 1;
+}
+
+.stats-label {
+  font-size: 14px;
+  color: #666;
+  margin-top: 4px;
+}
+
+.quick-actions {
+  margin-bottom: 24px;
+}
+
+.actions-grid {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.recent-activity {
+  margin-bottom: 24px;
+}
+</style>

+ 18 - 0
src/views/error/403.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="error-page">
+    <el-result icon="warning" title="403" sub-title="抱歉,您没有权限访问此页面">
+      <template #extra>
+        <el-button type="primary" @click="$router.push('/')">返回首页</el-button>
+      </template>
+    </el-result>
+  </div>
+</template>
+
+<style scoped>
+.error-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+}
+</style>

+ 18 - 0
src/views/error/404.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="error-page">
+    <el-result icon="warning" title="404" sub-title="抱歉,您访问的页面不存在">
+      <template #extra>
+        <el-button type="primary" @click="$router.push('/')">返回首页</el-button>
+      </template>
+    </el-result>
+  </div>
+</template>
+
+<style scoped>
+.error-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+}
+</style>

+ 525 - 0
src/views/user/Profile.vue

@@ -0,0 +1,525 @@
+<template>
+  <div class="profile-content">
+    <div class="page-header">
+      <h2>个人资料</h2>
+      <p>管理您的个人信息和账户设置</p>
+    </div>
+
+    <div class="profile-container">
+      <el-row :gutter="24">
+        <!-- 左侧:基本信息 -->
+        <el-col :span="16">
+          <el-card class="profile-card">
+            <template #header>
+              <span>基本信息</span>
+            </template>
+            
+            <el-form
+              ref="profileFormRef"
+              :model="profileForm"
+              :rules="profileRules"
+              label-width="100px"
+              @submit.prevent="updateProfile"
+            >
+                  <el-form-item label="用户名" prop="username">
+                    <el-input
+                      v-model="profileForm.username"
+                      placeholder="请输入用户名"
+                      :disabled="true"
+                    />
+                    <div class="form-tip">用户名不可修改</div>
+                  </el-form-item>
+
+                  <el-form-item label="邮箱" prop="email">
+                    <el-input
+                      v-model="profileForm.email"
+                      placeholder="请输入邮箱"
+                      type="email"
+                    />
+                  </el-form-item>
+
+                  <el-form-item label="手机号" prop="phone">
+                    <el-input
+                      v-model="profileForm.phone"
+                      placeholder="请输入手机号"
+                    />
+                  </el-form-item>
+
+                  <el-form-item label="真实姓名" prop="real_name">
+                    <el-input
+                      v-model="profileForm.real_name"
+                      placeholder="请输入真实姓名"
+                    />
+                  </el-form-item>
+
+                  <el-form-item label="公司" prop="company">
+                    <el-input
+                      v-model="profileForm.company"
+                      placeholder="请输入公司名称"
+                    />
+                  </el-form-item>
+
+                  <el-form-item label="部门" prop="department">
+                    <el-input
+                      v-model="profileForm.department"
+                      placeholder="请输入部门"
+                    />
+                  </el-form-item>
+
+                  <el-form-item label="职位" prop="position">
+                    <el-input
+                      v-model="profileForm.position"
+                      placeholder="请输入职位"
+                    />
+                  </el-form-item>
+
+                  <el-form-item>
+                    <el-button
+                      type="primary"
+                      :loading="loading"
+                      @click="updateProfile"
+                    >
+                      保存修改
+                    </el-button>
+                    <el-button @click="resetForm">
+                      重置
+                    </el-button>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+
+            <!-- 右侧:头像和安全设置 -->
+            <el-col :span="8">
+              <!-- 头像设置 -->
+              <el-card class="avatar-card">
+                <template #header>
+                  <span>头像设置</span>
+                </template>
+                
+                <div class="avatar-section">
+                  <el-avatar
+                    :src="profileForm.avatar_url"
+                    :size="120"
+                    class="user-avatar"
+                  >
+                    {{ profileForm.username?.charAt(0).toUpperCase() }}
+                  </el-avatar>
+                  
+                  <el-upload
+                    class="avatar-uploader"
+                    action="/api/v1/upload/avatar"
+                    :headers="uploadHeaders"
+                    :show-file-list="false"
+                    :on-success="handleAvatarSuccess"
+                    :before-upload="beforeAvatarUpload"
+                  >
+                    <el-button type="primary" size="small">
+                      <el-icon><Upload /></el-icon>
+                      更换头像
+                    </el-button>
+                  </el-upload>
+                </div>
+              </el-card>
+
+              <!-- 账户信息 -->
+              <el-card class="account-info">
+                <template #header>
+                  <span>账户信息</span>
+                </template>
+                
+                <div class="info-item">
+                  <span class="label">用户ID:</span>
+                  <span class="value">{{ userStore.user?.id }}</span>
+                </div>
+                
+                <div class="info-item">
+                  <span class="label">注册时间:</span>
+                  <span class="value">{{ formatDate(userStore.user?.created_at) }}</span>
+                </div>
+                
+                <div class="info-item">
+                  <span class="label">最后登录:</span>
+                  <span class="value">{{ formatDate(userStore.user?.last_login_at) }}</span>
+                </div>
+                
+                <div class="info-item">
+                  <span class="label">账户状态:</span>
+                  <el-tag :type="userStore.user?.is_active ? 'success' : 'danger'">
+                    {{ userStore.user?.is_active ? '正常' : '已禁用' }}
+                  </el-tag>
+                </div>
+              </el-card>
+
+              <!-- 安全设置 -->
+              <el-card class="security-card">
+                <template #header>
+                  <span>安全设置</span>
+                </template>
+                
+                <div class="security-actions">
+                  <el-button
+                    type="warning"
+                    size="small"
+                    @click="showChangePassword = true"
+                  >
+                    <el-icon><Lock /></el-icon>
+                    修改密码
+                  </el-button>
+                  
+                  <el-button
+                    type="info"
+                    size="small"
+                    @click="$router.push('/user/tokens')"
+                  >
+                    <el-icon><Key /></el-icon>
+                    令牌管理
+                  </el-button>
+                  
+                  <el-button
+                    type="success"
+                    size="small"
+                    @click="$router.push('/user/activity')"
+                  >
+                    <el-icon><Clock /></el-icon>
+                    登录日志
+                  </el-button>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </div>
+
+    <!-- 修改密码对话框 -->
+    <el-dialog
+      v-model="showChangePassword"
+      title="修改密码"
+      width="400px"
+    >
+      <el-form
+        ref="passwordFormRef"
+        :model="passwordForm"
+        :rules="passwordRules"
+        label-width="100px"
+      >
+        <el-form-item label="当前密码" prop="old_password">
+          <el-input
+            v-model="passwordForm.old_password"
+            type="password"
+            placeholder="请输入当前密码"
+            show-password
+          />
+        </el-form-item>
+
+        <el-form-item label="新密码" prop="new_password">
+          <el-input
+            v-model="passwordForm.new_password"
+            type="password"
+            placeholder="请输入新密码"
+            show-password
+          />
+        </el-form-item>
+
+        <el-form-item label="确认密码" prop="confirm_password">
+          <el-input
+            v-model="passwordForm.confirm_password"
+            type="password"
+            placeholder="请再次输入新密码"
+            show-password
+          />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="showChangePassword = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="passwordLoading"
+          @click="changePassword"
+        >
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+import { getToken } from '@/utils/auth'
+
+const userStore = useAuthStore()
+
+const loading = ref(false)
+const passwordLoading = ref(false)
+const showChangePassword = ref(false)
+
+const profileFormRef = ref<FormInstance>()
+const passwordFormRef = ref<FormInstance>()
+
+// 个人资料表单
+const profileForm = reactive({
+  username: '',
+  email: '',
+  phone: '',
+  real_name: '',
+  company: '',
+  department: '',
+  position: '',
+  avatar_url: ''
+})
+
+// 密码修改表单
+const passwordForm = reactive({
+  old_password: '',
+  new_password: '',
+  confirm_password: ''
+})
+
+// 表单验证规则
+const profileRules: FormRules = {
+  email: [
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
+  ],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+  ]
+}
+
+const passwordRules: FormRules = {
+  old_password: [
+    { required: true, message: '请输入当前密码', trigger: 'blur' }
+  ],
+  new_password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ],
+  confirm_password: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    {
+      validator: (rule, value, callback) => {
+        if (value !== passwordForm.new_password) {
+          callback(new Error('两次输入的密码不一致'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+}
+
+// 上传头像的请求头
+const uploadHeaders = {
+  Authorization: `Bearer ${getToken()}`
+}
+
+// 初始化表单数据
+const initForm = () => {
+  if (userStore.user) {
+    Object.assign(profileForm, {
+      username: userStore.user.username,
+      email: userStore.user.email,
+      phone: userStore.user.phone || '',
+      real_name: userStore.user.real_name || '',
+      company: userStore.user.company || '',
+      department: userStore.user.department || '',
+      position: userStore.user.position || '',
+      avatar_url: userStore.user.avatar_url || ''
+    })
+  }
+}
+
+// 更新个人资料
+const updateProfile = async () => {
+  if (!profileFormRef.value) return
+  
+  try {
+    await profileFormRef.value.validate()
+    loading.value = true
+    
+    // TODO: 调用API更新用户信息
+    await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟API调用
+    
+    ElMessage.success('个人资料更新成功')
+    
+    // 更新store中的用户信息
+    await userStore.fetchUserInfo()
+    
+  } catch (error) {
+    console.error('更新个人资料失败:', error)
+    ElMessage.error('更新失败,请重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  initForm()
+  ElMessage.info('表单已重置')
+}
+
+// 修改密码
+const changePassword = async () => {
+  if (!passwordFormRef.value) return
+  
+  try {
+    await passwordFormRef.value.validate()
+    passwordLoading.value = true
+    
+    // TODO: 调用API修改密码
+    await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟API调用
+    
+    ElMessage.success('密码修改成功')
+    showChangePassword.value = false
+    
+    // 重置密码表单
+    Object.assign(passwordForm, {
+      old_password: '',
+      new_password: '',
+      confirm_password: ''
+    })
+    
+  } catch (error) {
+    console.error('修改密码失败:', error)
+    ElMessage.error('修改密码失败,请重试')
+  } finally {
+    passwordLoading.value = false
+  }
+}
+
+// 头像上传成功回调
+const handleAvatarSuccess = (response: any) => {
+  if (response.code === 0) {
+    profileForm.avatar_url = response.data.url
+    ElMessage.success('头像上传成功')
+  } else {
+    ElMessage.error('头像上传失败')
+  }
+}
+
+// 头像上传前验证
+const beforeAvatarUpload = (file: File) => {
+  const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
+  const isLt2M = file.size / 1024 / 1024 < 2
+
+  if (!isJPG) {
+    ElMessage.error('头像只能是 JPG/PNG 格式!')
+    return false
+  }
+  if (!isLt2M) {
+    ElMessage.error('头像大小不能超过 2MB!')
+    return false
+  }
+  return true
+}
+
+// 格式化日期
+const formatDate = (date: string | undefined) => {
+  if (!date) return '-'
+  return new Date(date).toLocaleString('zh-CN')
+}
+
+onMounted(() => {
+  initForm()
+})
+</script>
+
+<style scoped>
+.profile-content {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 24px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.profile-container {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.profile-card {
+  margin-bottom: 20px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #999;
+  margin-top: 4px;
+}
+
+.avatar-card {
+  margin-bottom: 20px;
+}
+
+.avatar-section {
+  text-align: center;
+}
+
+.user-avatar {
+  margin-bottom: 16px;
+}
+
+.avatar-uploader {
+  display: block;
+}
+
+.account-info {
+  margin-bottom: 20px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.info-item:last-child {
+  border-bottom: none;
+}
+
+.label {
+  font-size: 14px;
+  color: #666;
+}
+
+.value {
+  font-size: 14px;
+  color: #333;
+  word-break: break-all;
+}
+
+.security-card {
+  margin-bottom: 20px;
+}
+
+.security-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.security-actions .el-button {
+  justify-content: flex-start;
+}
+</style>

+ 13 - 0
tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "composite": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+    "types": ["node"]
+  }
+}

+ 39 - 0
vite.config.ts

@@ -0,0 +1,39 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    port: 3000,
+    host: true,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8000',
+        changeOrigin: true,
+      },
+      '/oauth': {
+        target: 'http://localhost:8000',
+        changeOrigin: true,
+      },
+    },
+  },
+  build: {
+    outDir: 'dist',
+    sourcemap: false,
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          vue: ['vue', 'vue-router', 'pinia'],
+          element: ['element-plus', '@element-plus/icons-vue'],
+        },
+      },
+    },
+  },
+})