feat:初始化代码
commit
0c8fcefaee
@ -0,0 +1,4 @@
|
||||
# 开发环境
|
||||
VITE_APP_TITLE=教室智能人脸考勤系统(开发)
|
||||
VITE_API_BASE_URL=http://localhost:8080/api
|
||||
VITE_WS_URL=ws://localhost:8080/ws
|
||||
@ -0,0 +1,4 @@
|
||||
# 生产环境
|
||||
VITE_APP_TITLE=教室智能人脸考勤系统
|
||||
VITE_API_BASE_URL=https://api.yourschool.com/api
|
||||
VITE_WS_URL=wss://api.yourschool.com/ws
|
||||
@ -0,0 +1,28 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# cache
|
||||
.cache/
|
||||
@ -0,0 +1,593 @@
|
||||
import {
|
||||
add_location_default,
|
||||
aim_default,
|
||||
alarm_clock_default,
|
||||
apple_default,
|
||||
arrow_down_bold_default,
|
||||
arrow_down_default,
|
||||
arrow_left_bold_default,
|
||||
arrow_left_default,
|
||||
arrow_right_bold_default,
|
||||
arrow_right_default,
|
||||
arrow_up_bold_default,
|
||||
arrow_up_default,
|
||||
avatar_default,
|
||||
back_default,
|
||||
baseball_default,
|
||||
basketball_default,
|
||||
bell_default,
|
||||
bell_filled_default,
|
||||
bicycle_default,
|
||||
bottom_default,
|
||||
bottom_left_default,
|
||||
bottom_right_default,
|
||||
bowl_default,
|
||||
box_default,
|
||||
briefcase_default,
|
||||
brush_default,
|
||||
brush_filled_default,
|
||||
burger_default,
|
||||
calendar_default,
|
||||
camera_default,
|
||||
camera_filled_default,
|
||||
caret_bottom_default,
|
||||
caret_left_default,
|
||||
caret_right_default,
|
||||
caret_top_default,
|
||||
cellphone_default,
|
||||
chat_dot_round_default,
|
||||
chat_dot_square_default,
|
||||
chat_line_round_default,
|
||||
chat_line_square_default,
|
||||
chat_round_default,
|
||||
chat_square_default,
|
||||
check_default,
|
||||
checked_default,
|
||||
cherry_default,
|
||||
chicken_default,
|
||||
chrome_filled_default,
|
||||
circle_check_default,
|
||||
circle_check_filled_default,
|
||||
circle_close_default,
|
||||
circle_close_filled_default,
|
||||
circle_plus_default,
|
||||
circle_plus_filled_default,
|
||||
clock_default,
|
||||
close_bold_default,
|
||||
close_default,
|
||||
cloudy_default,
|
||||
coffee_cup_default,
|
||||
coffee_default,
|
||||
coin_default,
|
||||
cold_drink_default,
|
||||
collection_default,
|
||||
collection_tag_default,
|
||||
comment_default,
|
||||
compass_default,
|
||||
connection_default,
|
||||
coordinate_default,
|
||||
copy_document_default,
|
||||
cpu_default,
|
||||
credit_card_default,
|
||||
crop_default,
|
||||
d_arrow_left_default,
|
||||
d_arrow_right_default,
|
||||
d_caret_default,
|
||||
data_analysis_default,
|
||||
data_board_default,
|
||||
data_line_default,
|
||||
delete_default,
|
||||
delete_filled_default,
|
||||
delete_location_default,
|
||||
dessert_default,
|
||||
discount_default,
|
||||
dish_default,
|
||||
dish_dot_default,
|
||||
document_add_default,
|
||||
document_checked_default,
|
||||
document_copy_default,
|
||||
document_default,
|
||||
document_delete_default,
|
||||
document_remove_default,
|
||||
download_default,
|
||||
drizzling_default,
|
||||
edit_default,
|
||||
edit_pen_default,
|
||||
eleme_default,
|
||||
eleme_filled_default,
|
||||
element_plus_default,
|
||||
expand_default,
|
||||
failed_default,
|
||||
female_default,
|
||||
files_default,
|
||||
film_default,
|
||||
filter_default,
|
||||
finished_default,
|
||||
first_aid_kit_default,
|
||||
flag_default,
|
||||
fold_default,
|
||||
folder_add_default,
|
||||
folder_checked_default,
|
||||
folder_default,
|
||||
folder_delete_default,
|
||||
folder_opened_default,
|
||||
folder_remove_default,
|
||||
food_default,
|
||||
football_default,
|
||||
fork_spoon_default,
|
||||
fries_default,
|
||||
full_screen_default,
|
||||
goblet_default,
|
||||
goblet_full_default,
|
||||
goblet_square_default,
|
||||
goblet_square_full_default,
|
||||
gold_medal_default,
|
||||
goods_default,
|
||||
goods_filled_default,
|
||||
grape_default,
|
||||
grid_default,
|
||||
guide_default,
|
||||
handbag_default,
|
||||
headset_default,
|
||||
help_default,
|
||||
help_filled_default,
|
||||
hide_default,
|
||||
histogram_default,
|
||||
home_filled_default,
|
||||
hot_water_default,
|
||||
house_default,
|
||||
ice_cream_default,
|
||||
ice_cream_round_default,
|
||||
ice_cream_square_default,
|
||||
ice_drink_default,
|
||||
ice_tea_default,
|
||||
info_filled_default,
|
||||
iphone_default,
|
||||
key_default,
|
||||
knife_fork_default,
|
||||
lightning_default,
|
||||
link_default,
|
||||
list_default,
|
||||
loading_default,
|
||||
location_default,
|
||||
location_filled_default,
|
||||
location_information_default,
|
||||
lock_default,
|
||||
lollipop_default,
|
||||
magic_stick_default,
|
||||
magnet_default,
|
||||
male_default,
|
||||
management_default,
|
||||
map_location_default,
|
||||
medal_default,
|
||||
memo_default,
|
||||
menu_default,
|
||||
message_box_default,
|
||||
message_default,
|
||||
mic_default,
|
||||
microphone_default,
|
||||
milk_tea_default,
|
||||
minus_default,
|
||||
money_default,
|
||||
monitor_default,
|
||||
moon_default,
|
||||
moon_night_default,
|
||||
more_default,
|
||||
more_filled_default,
|
||||
mostly_cloudy_default,
|
||||
mouse_default,
|
||||
mug_default,
|
||||
mute_default,
|
||||
mute_notification_default,
|
||||
no_smoking_default,
|
||||
notebook_default,
|
||||
notification_default,
|
||||
odometer_default,
|
||||
office_building_default,
|
||||
open_default,
|
||||
operation_default,
|
||||
opportunity_default,
|
||||
orange_default,
|
||||
paperclip_default,
|
||||
partly_cloudy_default,
|
||||
pear_default,
|
||||
phone_default,
|
||||
phone_filled_default,
|
||||
picture_default,
|
||||
picture_filled_default,
|
||||
picture_rounded_default,
|
||||
pie_chart_default,
|
||||
place_default,
|
||||
platform_default,
|
||||
plus_default,
|
||||
pointer_default,
|
||||
position_default,
|
||||
postcard_default,
|
||||
pouring_default,
|
||||
present_default,
|
||||
price_tag_default,
|
||||
printer_default,
|
||||
promotion_default,
|
||||
quartz_watch_default,
|
||||
question_filled_default,
|
||||
rank_default,
|
||||
reading_default,
|
||||
reading_lamp_default,
|
||||
refresh_default,
|
||||
refresh_left_default,
|
||||
refresh_right_default,
|
||||
refrigerator_default,
|
||||
remove_default,
|
||||
remove_filled_default,
|
||||
right_default,
|
||||
scale_to_original_default,
|
||||
school_default,
|
||||
scissor_default,
|
||||
search_default,
|
||||
select_default,
|
||||
sell_default,
|
||||
semi_select_default,
|
||||
service_default,
|
||||
set_up_default,
|
||||
setting_default,
|
||||
share_default,
|
||||
ship_default,
|
||||
shop_default,
|
||||
shopping_bag_default,
|
||||
shopping_cart_default,
|
||||
shopping_cart_full_default,
|
||||
shopping_trolley_default,
|
||||
smoking_default,
|
||||
soccer_default,
|
||||
sold_out_default,
|
||||
sort_default,
|
||||
sort_down_default,
|
||||
sort_up_default,
|
||||
stamp_default,
|
||||
star_default,
|
||||
star_filled_default,
|
||||
stopwatch_default,
|
||||
success_filled_default,
|
||||
sugar_default,
|
||||
suitcase_default,
|
||||
suitcase_line_default,
|
||||
sunny_default,
|
||||
sunrise_default,
|
||||
sunset_default,
|
||||
switch_button_default,
|
||||
switch_default,
|
||||
switch_filled_default,
|
||||
takeaway_box_default,
|
||||
ticket_default,
|
||||
tickets_default,
|
||||
timer_default,
|
||||
toilet_paper_default,
|
||||
tools_default,
|
||||
top_default,
|
||||
top_left_default,
|
||||
top_right_default,
|
||||
trend_charts_default,
|
||||
trophy_base_default,
|
||||
trophy_default,
|
||||
turn_off_default,
|
||||
umbrella_default,
|
||||
unlock_default,
|
||||
upload_default,
|
||||
upload_filled_default,
|
||||
user_default,
|
||||
user_filled_default,
|
||||
van_default,
|
||||
video_camera_default,
|
||||
video_camera_filled_default,
|
||||
video_pause_default,
|
||||
video_play_default,
|
||||
view_default,
|
||||
wallet_default,
|
||||
wallet_filled_default,
|
||||
warn_triangle_filled_default,
|
||||
warning_default,
|
||||
warning_filled_default,
|
||||
watch_default,
|
||||
watermelon_default,
|
||||
wind_power_default,
|
||||
zoom_in_default,
|
||||
zoom_out_default
|
||||
} from "./chunk-ETC2NX3T.js";
|
||||
import "./chunk-J44PGZX2.js";
|
||||
import "./chunk-5WRI5ZAA.js";
|
||||
export {
|
||||
add_location_default as AddLocation,
|
||||
aim_default as Aim,
|
||||
alarm_clock_default as AlarmClock,
|
||||
apple_default as Apple,
|
||||
arrow_down_default as ArrowDown,
|
||||
arrow_down_bold_default as ArrowDownBold,
|
||||
arrow_left_default as ArrowLeft,
|
||||
arrow_left_bold_default as ArrowLeftBold,
|
||||
arrow_right_default as ArrowRight,
|
||||
arrow_right_bold_default as ArrowRightBold,
|
||||
arrow_up_default as ArrowUp,
|
||||
arrow_up_bold_default as ArrowUpBold,
|
||||
avatar_default as Avatar,
|
||||
back_default as Back,
|
||||
baseball_default as Baseball,
|
||||
basketball_default as Basketball,
|
||||
bell_default as Bell,
|
||||
bell_filled_default as BellFilled,
|
||||
bicycle_default as Bicycle,
|
||||
bottom_default as Bottom,
|
||||
bottom_left_default as BottomLeft,
|
||||
bottom_right_default as BottomRight,
|
||||
bowl_default as Bowl,
|
||||
box_default as Box,
|
||||
briefcase_default as Briefcase,
|
||||
brush_default as Brush,
|
||||
brush_filled_default as BrushFilled,
|
||||
burger_default as Burger,
|
||||
calendar_default as Calendar,
|
||||
camera_default as Camera,
|
||||
camera_filled_default as CameraFilled,
|
||||
caret_bottom_default as CaretBottom,
|
||||
caret_left_default as CaretLeft,
|
||||
caret_right_default as CaretRight,
|
||||
caret_top_default as CaretTop,
|
||||
cellphone_default as Cellphone,
|
||||
chat_dot_round_default as ChatDotRound,
|
||||
chat_dot_square_default as ChatDotSquare,
|
||||
chat_line_round_default as ChatLineRound,
|
||||
chat_line_square_default as ChatLineSquare,
|
||||
chat_round_default as ChatRound,
|
||||
chat_square_default as ChatSquare,
|
||||
check_default as Check,
|
||||
checked_default as Checked,
|
||||
cherry_default as Cherry,
|
||||
chicken_default as Chicken,
|
||||
chrome_filled_default as ChromeFilled,
|
||||
circle_check_default as CircleCheck,
|
||||
circle_check_filled_default as CircleCheckFilled,
|
||||
circle_close_default as CircleClose,
|
||||
circle_close_filled_default as CircleCloseFilled,
|
||||
circle_plus_default as CirclePlus,
|
||||
circle_plus_filled_default as CirclePlusFilled,
|
||||
clock_default as Clock,
|
||||
close_default as Close,
|
||||
close_bold_default as CloseBold,
|
||||
cloudy_default as Cloudy,
|
||||
coffee_default as Coffee,
|
||||
coffee_cup_default as CoffeeCup,
|
||||
coin_default as Coin,
|
||||
cold_drink_default as ColdDrink,
|
||||
collection_default as Collection,
|
||||
collection_tag_default as CollectionTag,
|
||||
comment_default as Comment,
|
||||
compass_default as Compass,
|
||||
connection_default as Connection,
|
||||
coordinate_default as Coordinate,
|
||||
copy_document_default as CopyDocument,
|
||||
cpu_default as Cpu,
|
||||
credit_card_default as CreditCard,
|
||||
crop_default as Crop,
|
||||
d_arrow_left_default as DArrowLeft,
|
||||
d_arrow_right_default as DArrowRight,
|
||||
d_caret_default as DCaret,
|
||||
data_analysis_default as DataAnalysis,
|
||||
data_board_default as DataBoard,
|
||||
data_line_default as DataLine,
|
||||
delete_default as Delete,
|
||||
delete_filled_default as DeleteFilled,
|
||||
delete_location_default as DeleteLocation,
|
||||
dessert_default as Dessert,
|
||||
discount_default as Discount,
|
||||
dish_default as Dish,
|
||||
dish_dot_default as DishDot,
|
||||
document_default as Document,
|
||||
document_add_default as DocumentAdd,
|
||||
document_checked_default as DocumentChecked,
|
||||
document_copy_default as DocumentCopy,
|
||||
document_delete_default as DocumentDelete,
|
||||
document_remove_default as DocumentRemove,
|
||||
download_default as Download,
|
||||
drizzling_default as Drizzling,
|
||||
edit_default as Edit,
|
||||
edit_pen_default as EditPen,
|
||||
eleme_default as Eleme,
|
||||
eleme_filled_default as ElemeFilled,
|
||||
element_plus_default as ElementPlus,
|
||||
expand_default as Expand,
|
||||
failed_default as Failed,
|
||||
female_default as Female,
|
||||
files_default as Files,
|
||||
film_default as Film,
|
||||
filter_default as Filter,
|
||||
finished_default as Finished,
|
||||
first_aid_kit_default as FirstAidKit,
|
||||
flag_default as Flag,
|
||||
fold_default as Fold,
|
||||
folder_default as Folder,
|
||||
folder_add_default as FolderAdd,
|
||||
folder_checked_default as FolderChecked,
|
||||
folder_delete_default as FolderDelete,
|
||||
folder_opened_default as FolderOpened,
|
||||
folder_remove_default as FolderRemove,
|
||||
food_default as Food,
|
||||
football_default as Football,
|
||||
fork_spoon_default as ForkSpoon,
|
||||
fries_default as Fries,
|
||||
full_screen_default as FullScreen,
|
||||
goblet_default as Goblet,
|
||||
goblet_full_default as GobletFull,
|
||||
goblet_square_default as GobletSquare,
|
||||
goblet_square_full_default as GobletSquareFull,
|
||||
gold_medal_default as GoldMedal,
|
||||
goods_default as Goods,
|
||||
goods_filled_default as GoodsFilled,
|
||||
grape_default as Grape,
|
||||
grid_default as Grid,
|
||||
guide_default as Guide,
|
||||
handbag_default as Handbag,
|
||||
headset_default as Headset,
|
||||
help_default as Help,
|
||||
help_filled_default as HelpFilled,
|
||||
hide_default as Hide,
|
||||
histogram_default as Histogram,
|
||||
home_filled_default as HomeFilled,
|
||||
hot_water_default as HotWater,
|
||||
house_default as House,
|
||||
ice_cream_default as IceCream,
|
||||
ice_cream_round_default as IceCreamRound,
|
||||
ice_cream_square_default as IceCreamSquare,
|
||||
ice_drink_default as IceDrink,
|
||||
ice_tea_default as IceTea,
|
||||
info_filled_default as InfoFilled,
|
||||
iphone_default as Iphone,
|
||||
key_default as Key,
|
||||
knife_fork_default as KnifeFork,
|
||||
lightning_default as Lightning,
|
||||
link_default as Link,
|
||||
list_default as List,
|
||||
loading_default as Loading,
|
||||
location_default as Location,
|
||||
location_filled_default as LocationFilled,
|
||||
location_information_default as LocationInformation,
|
||||
lock_default as Lock,
|
||||
lollipop_default as Lollipop,
|
||||
magic_stick_default as MagicStick,
|
||||
magnet_default as Magnet,
|
||||
male_default as Male,
|
||||
management_default as Management,
|
||||
map_location_default as MapLocation,
|
||||
medal_default as Medal,
|
||||
memo_default as Memo,
|
||||
menu_default as Menu,
|
||||
message_default as Message,
|
||||
message_box_default as MessageBox,
|
||||
mic_default as Mic,
|
||||
microphone_default as Microphone,
|
||||
milk_tea_default as MilkTea,
|
||||
minus_default as Minus,
|
||||
money_default as Money,
|
||||
monitor_default as Monitor,
|
||||
moon_default as Moon,
|
||||
moon_night_default as MoonNight,
|
||||
more_default as More,
|
||||
more_filled_default as MoreFilled,
|
||||
mostly_cloudy_default as MostlyCloudy,
|
||||
mouse_default as Mouse,
|
||||
mug_default as Mug,
|
||||
mute_default as Mute,
|
||||
mute_notification_default as MuteNotification,
|
||||
no_smoking_default as NoSmoking,
|
||||
notebook_default as Notebook,
|
||||
notification_default as Notification,
|
||||
odometer_default as Odometer,
|
||||
office_building_default as OfficeBuilding,
|
||||
open_default as Open,
|
||||
operation_default as Operation,
|
||||
opportunity_default as Opportunity,
|
||||
orange_default as Orange,
|
||||
paperclip_default as Paperclip,
|
||||
partly_cloudy_default as PartlyCloudy,
|
||||
pear_default as Pear,
|
||||
phone_default as Phone,
|
||||
phone_filled_default as PhoneFilled,
|
||||
picture_default as Picture,
|
||||
picture_filled_default as PictureFilled,
|
||||
picture_rounded_default as PictureRounded,
|
||||
pie_chart_default as PieChart,
|
||||
place_default as Place,
|
||||
platform_default as Platform,
|
||||
plus_default as Plus,
|
||||
pointer_default as Pointer,
|
||||
position_default as Position,
|
||||
postcard_default as Postcard,
|
||||
pouring_default as Pouring,
|
||||
present_default as Present,
|
||||
price_tag_default as PriceTag,
|
||||
printer_default as Printer,
|
||||
promotion_default as Promotion,
|
||||
quartz_watch_default as QuartzWatch,
|
||||
question_filled_default as QuestionFilled,
|
||||
rank_default as Rank,
|
||||
reading_default as Reading,
|
||||
reading_lamp_default as ReadingLamp,
|
||||
refresh_default as Refresh,
|
||||
refresh_left_default as RefreshLeft,
|
||||
refresh_right_default as RefreshRight,
|
||||
refrigerator_default as Refrigerator,
|
||||
remove_default as Remove,
|
||||
remove_filled_default as RemoveFilled,
|
||||
right_default as Right,
|
||||
scale_to_original_default as ScaleToOriginal,
|
||||
school_default as School,
|
||||
scissor_default as Scissor,
|
||||
search_default as Search,
|
||||
select_default as Select,
|
||||
sell_default as Sell,
|
||||
semi_select_default as SemiSelect,
|
||||
service_default as Service,
|
||||
set_up_default as SetUp,
|
||||
setting_default as Setting,
|
||||
share_default as Share,
|
||||
ship_default as Ship,
|
||||
shop_default as Shop,
|
||||
shopping_bag_default as ShoppingBag,
|
||||
shopping_cart_default as ShoppingCart,
|
||||
shopping_cart_full_default as ShoppingCartFull,
|
||||
shopping_trolley_default as ShoppingTrolley,
|
||||
smoking_default as Smoking,
|
||||
soccer_default as Soccer,
|
||||
sold_out_default as SoldOut,
|
||||
sort_default as Sort,
|
||||
sort_down_default as SortDown,
|
||||
sort_up_default as SortUp,
|
||||
stamp_default as Stamp,
|
||||
star_default as Star,
|
||||
star_filled_default as StarFilled,
|
||||
stopwatch_default as Stopwatch,
|
||||
success_filled_default as SuccessFilled,
|
||||
sugar_default as Sugar,
|
||||
suitcase_default as Suitcase,
|
||||
suitcase_line_default as SuitcaseLine,
|
||||
sunny_default as Sunny,
|
||||
sunrise_default as Sunrise,
|
||||
sunset_default as Sunset,
|
||||
switch_default as Switch,
|
||||
switch_button_default as SwitchButton,
|
||||
switch_filled_default as SwitchFilled,
|
||||
takeaway_box_default as TakeawayBox,
|
||||
ticket_default as Ticket,
|
||||
tickets_default as Tickets,
|
||||
timer_default as Timer,
|
||||
toilet_paper_default as ToiletPaper,
|
||||
tools_default as Tools,
|
||||
top_default as Top,
|
||||
top_left_default as TopLeft,
|
||||
top_right_default as TopRight,
|
||||
trend_charts_default as TrendCharts,
|
||||
trophy_default as Trophy,
|
||||
trophy_base_default as TrophyBase,
|
||||
turn_off_default as TurnOff,
|
||||
umbrella_default as Umbrella,
|
||||
unlock_default as Unlock,
|
||||
upload_default as Upload,
|
||||
upload_filled_default as UploadFilled,
|
||||
user_default as User,
|
||||
user_filled_default as UserFilled,
|
||||
van_default as Van,
|
||||
video_camera_default as VideoCamera,
|
||||
video_camera_filled_default as VideoCameraFilled,
|
||||
video_pause_default as VideoPause,
|
||||
video_play_default as VideoPlay,
|
||||
view_default as View,
|
||||
wallet_default as Wallet,
|
||||
wallet_filled_default as WalletFilled,
|
||||
warn_triangle_filled_default as WarnTriangleFilled,
|
||||
warning_default as Warning,
|
||||
warning_filled_default as WarningFilled,
|
||||
watch_default as Watch,
|
||||
watermelon_default as Watermelon,
|
||||
wind_power_default as WindPower,
|
||||
zoom_in_default as ZoomIn,
|
||||
zoom_out_default as ZoomOut
|
||||
};
|
||||
//# sourceMappingURL=@element-plus_icons-vue.js.map
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
{
|
||||
"hash": "6565f0a1",
|
||||
"configHash": "eb1835b8",
|
||||
"lockfileHash": "9f1499ac",
|
||||
"browserHash": "7b79acab",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "4a20bd0d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"pinia": {
|
||||
"src": "../../node_modules/pinia/dist/pinia.mjs",
|
||||
"file": "pinia.js",
|
||||
"fileHash": "e3724164",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus": {
|
||||
"src": "../../node_modules/element-plus/es/index.mjs",
|
||||
"file": "element-plus.js",
|
||||
"fileHash": "6327058f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"element-plus/dist/locale/zh-cn.mjs": {
|
||||
"src": "../../node_modules/element-plus/dist/locale/zh-cn.mjs",
|
||||
"file": "element-plus_dist_locale_zh-cn__mjs.js",
|
||||
"fileHash": "085f6406",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@element-plus/icons-vue": {
|
||||
"src": "../../node_modules/@element-plus/icons-vue/dist/index.js",
|
||||
"file": "@element-plus_icons-vue.js",
|
||||
"fileHash": "88f96e20",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue-router": {
|
||||
"src": "../../node_modules/vue-router/dist/vue-router.mjs",
|
||||
"file": "vue-router.js",
|
||||
"fileHash": "3270b841",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-ETC2NX3T": {
|
||||
"file": "chunk-ETC2NX3T.js"
|
||||
},
|
||||
"chunk-AYVSL3LM": {
|
||||
"file": "chunk-AYVSL3LM.js"
|
||||
},
|
||||
"chunk-J44PGZX2": {
|
||||
"file": "chunk-J44PGZX2.js"
|
||||
},
|
||||
"chunk-5WRI5ZAA": {
|
||||
"file": "chunk-5WRI5ZAA.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
|
||||
export {
|
||||
__commonJS,
|
||||
__toESM
|
||||
};
|
||||
//# sourceMappingURL=chunk-5WRI5ZAA.js.map
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
// node_modules/@vue/devtools-api/lib/esm/env.js
|
||||
function getDevtoolsGlobalHook() {
|
||||
return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
||||
}
|
||||
function getTarget() {
|
||||
return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : {};
|
||||
}
|
||||
var isProxyAvailable = typeof Proxy === "function";
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/const.js
|
||||
var HOOK_SETUP = "devtools-plugin:setup";
|
||||
var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set";
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/time.js
|
||||
var supported;
|
||||
var perf;
|
||||
function isPerformanceSupported() {
|
||||
var _a;
|
||||
if (supported !== void 0) {
|
||||
return supported;
|
||||
}
|
||||
if (typeof window !== "undefined" && window.performance) {
|
||||
supported = true;
|
||||
perf = window.performance;
|
||||
} else if (typeof globalThis !== "undefined" && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
|
||||
supported = true;
|
||||
perf = globalThis.perf_hooks.performance;
|
||||
} else {
|
||||
supported = false;
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
function now() {
|
||||
return isPerformanceSupported() ? perf.now() : Date.now();
|
||||
}
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/proxy.js
|
||||
var ApiProxy = class {
|
||||
constructor(plugin, hook) {
|
||||
this.target = null;
|
||||
this.targetQueue = [];
|
||||
this.onQueue = [];
|
||||
this.plugin = plugin;
|
||||
this.hook = hook;
|
||||
const defaultSettings = {};
|
||||
if (plugin.settings) {
|
||||
for (const id in plugin.settings) {
|
||||
const item = plugin.settings[id];
|
||||
defaultSettings[id] = item.defaultValue;
|
||||
}
|
||||
}
|
||||
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
|
||||
let currentSettings = Object.assign({}, defaultSettings);
|
||||
try {
|
||||
const raw = localStorage.getItem(localSettingsSaveId);
|
||||
const data = JSON.parse(raw);
|
||||
Object.assign(currentSettings, data);
|
||||
} catch (e) {
|
||||
}
|
||||
this.fallbacks = {
|
||||
getSettings() {
|
||||
return currentSettings;
|
||||
},
|
||||
setSettings(value) {
|
||||
try {
|
||||
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
}
|
||||
currentSettings = value;
|
||||
},
|
||||
now() {
|
||||
return now();
|
||||
}
|
||||
};
|
||||
if (hook) {
|
||||
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
|
||||
if (pluginId === this.plugin.id) {
|
||||
this.fallbacks.setSettings(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.proxiedOn = new Proxy({}, {
|
||||
get: (_target, prop) => {
|
||||
if (this.target) {
|
||||
return this.target.on[prop];
|
||||
} else {
|
||||
return (...args) => {
|
||||
this.onQueue.push({
|
||||
method: prop,
|
||||
args
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
this.proxiedTarget = new Proxy({}, {
|
||||
get: (_target, prop) => {
|
||||
if (this.target) {
|
||||
return this.target[prop];
|
||||
} else if (prop === "on") {
|
||||
return this.proxiedOn;
|
||||
} else if (Object.keys(this.fallbacks).includes(prop)) {
|
||||
return (...args) => {
|
||||
this.targetQueue.push({
|
||||
method: prop,
|
||||
args,
|
||||
resolve: () => {
|
||||
}
|
||||
});
|
||||
return this.fallbacks[prop](...args);
|
||||
};
|
||||
} else {
|
||||
return (...args) => {
|
||||
return new Promise((resolve) => {
|
||||
this.targetQueue.push({
|
||||
method: prop,
|
||||
args,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
async setRealTarget(target) {
|
||||
this.target = target;
|
||||
for (const item of this.onQueue) {
|
||||
this.target.on[item.method](...item.args);
|
||||
}
|
||||
for (const item of this.targetQueue) {
|
||||
item.resolve(await this.target[item.method](...item.args));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/@vue/devtools-api/lib/esm/index.js
|
||||
function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
|
||||
const descriptor = pluginDescriptor;
|
||||
const target = getTarget();
|
||||
const hook = getDevtoolsGlobalHook();
|
||||
const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
|
||||
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
|
||||
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
|
||||
} else {
|
||||
const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
|
||||
const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
|
||||
list.push({
|
||||
pluginDescriptor: descriptor,
|
||||
setupFn,
|
||||
proxy
|
||||
});
|
||||
if (proxy) {
|
||||
setupFn(proxy.proxiedTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setupDevtoolsPlugin
|
||||
};
|
||||
//# sourceMappingURL=chunk-AYVSL3LM.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,192 @@
|
||||
import "./chunk-5WRI5ZAA.js";
|
||||
|
||||
// node_modules/element-plus/dist/locale/zh-cn.mjs
|
||||
var zh_cn_default = {
|
||||
name: "zh-cn",
|
||||
el: {
|
||||
breadcrumb: { label: "面包屑" },
|
||||
colorpicker: {
|
||||
confirm: "确定",
|
||||
clear: "清空",
|
||||
defaultLabel: "颜色选择器",
|
||||
description: "当前颜色 {color},按 Enter 键选择新颜色",
|
||||
alphaLabel: "选择透明度的值",
|
||||
alphaDescription: "透明度 {alpha}, 当前颜色 {color}",
|
||||
hueLabel: "选择色相值",
|
||||
hueDescription: "色相 {hue}, 当前颜色 {color}",
|
||||
svLabel: "选择饱和度与明度的值",
|
||||
svDescription: "饱和度 {saturation}, 明度 {brightness}, 当前颜色 {color}",
|
||||
predefineDescription: "选择 {value} 作为颜色"
|
||||
},
|
||||
datepicker: {
|
||||
now: "此刻",
|
||||
today: "今天",
|
||||
cancel: "取消",
|
||||
clear: "清空",
|
||||
confirm: "确定",
|
||||
dateTablePrompt: "使用方向键与 Enter 键可选择日期",
|
||||
monthTablePrompt: "使用方向键与 Enter 键可选择月份",
|
||||
yearTablePrompt: "使用方向键与 Enter 键可选择年份",
|
||||
selectedDate: "已选日期",
|
||||
selectDate: "选择日期",
|
||||
selectTime: "选择时间",
|
||||
startDate: "开始日期",
|
||||
startTime: "开始时间",
|
||||
endDate: "结束日期",
|
||||
endTime: "结束时间",
|
||||
prevYear: "前一年",
|
||||
nextYear: "后一年",
|
||||
prevMonth: "上个月",
|
||||
nextMonth: "下个月",
|
||||
year: "年",
|
||||
month1: "1 月",
|
||||
month2: "2 月",
|
||||
month3: "3 月",
|
||||
month4: "4 月",
|
||||
month5: "5 月",
|
||||
month6: "6 月",
|
||||
month7: "7 月",
|
||||
month8: "8 月",
|
||||
month9: "9 月",
|
||||
month10: "10 月",
|
||||
month11: "11 月",
|
||||
month12: "12 月",
|
||||
weeks: {
|
||||
sun: "日",
|
||||
mon: "一",
|
||||
tue: "二",
|
||||
wed: "三",
|
||||
thu: "四",
|
||||
fri: "五",
|
||||
sat: "六"
|
||||
},
|
||||
weeksFull: {
|
||||
sun: "星期日",
|
||||
mon: "星期一",
|
||||
tue: "星期二",
|
||||
wed: "星期三",
|
||||
thu: "星期四",
|
||||
fri: "星期五",
|
||||
sat: "星期六"
|
||||
},
|
||||
months: {
|
||||
jan: "一月",
|
||||
feb: "二月",
|
||||
mar: "三月",
|
||||
apr: "四月",
|
||||
may: "五月",
|
||||
jun: "六月",
|
||||
jul: "七月",
|
||||
aug: "八月",
|
||||
sep: "九月",
|
||||
oct: "十月",
|
||||
nov: "十一月",
|
||||
dec: "十二月"
|
||||
}
|
||||
},
|
||||
inputNumber: {
|
||||
decrease: "减少数值",
|
||||
increase: "增加数值"
|
||||
},
|
||||
select: {
|
||||
loading: "加载中",
|
||||
noMatch: "无匹配数据",
|
||||
noData: "无数据",
|
||||
placeholder: "请选择"
|
||||
},
|
||||
mention: { loading: "加载中" },
|
||||
dropdown: { toggleDropdown: "切换下拉选项" },
|
||||
cascader: {
|
||||
noMatch: "无匹配数据",
|
||||
loading: "加载中",
|
||||
placeholder: "请选择",
|
||||
noData: "暂无数据"
|
||||
},
|
||||
pagination: {
|
||||
goto: "前往",
|
||||
pagesize: "条/页",
|
||||
total: "共 {total} 条",
|
||||
pageClassifier: "页",
|
||||
page: "页",
|
||||
prev: "上一页",
|
||||
next: "下一页",
|
||||
currentPage: "第 {pager} 页",
|
||||
prevPages: "向前 {pager} 页",
|
||||
nextPages: "向后 {pager} 页",
|
||||
deprecationWarning: "你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档"
|
||||
},
|
||||
dialog: { close: "关闭此对话框" },
|
||||
drawer: { close: "关闭此对话框" },
|
||||
messagebox: {
|
||||
title: "提示",
|
||||
confirm: "确定",
|
||||
cancel: "取消",
|
||||
error: "输入的数据不合法!",
|
||||
close: "关闭此对话框"
|
||||
},
|
||||
upload: {
|
||||
deleteTip: "按 Delete 键可删除",
|
||||
delete: "删除",
|
||||
preview: "查看图片",
|
||||
continue: "继续上传"
|
||||
},
|
||||
slider: {
|
||||
defaultLabel: "滑块介于 {min} 至 {max}",
|
||||
defaultRangeStartLabel: "选择起始值",
|
||||
defaultRangeEndLabel: "选择结束值"
|
||||
},
|
||||
table: {
|
||||
emptyText: "暂无数据",
|
||||
confirmFilter: "筛选",
|
||||
resetFilter: "重置",
|
||||
clearFilter: "全部",
|
||||
sumText: "合计",
|
||||
selectAllLabel: "选择所有行",
|
||||
selectRowLabel: "选择当前行",
|
||||
expandRowLabel: "展开当前行",
|
||||
collapseRowLabel: "收起当前行",
|
||||
sortLabel: "按 {column} 排序",
|
||||
filterLabel: "按 {column} 过滤"
|
||||
},
|
||||
tag: { close: "关闭此标签" },
|
||||
tour: {
|
||||
next: "下一步",
|
||||
previous: "上一步",
|
||||
finish: "结束导览",
|
||||
close: "关闭此对话框"
|
||||
},
|
||||
tree: { emptyText: "暂无数据" },
|
||||
transfer: {
|
||||
noMatch: "无匹配数据",
|
||||
noData: "无数据",
|
||||
titles: ["列表 1", "列表 2"],
|
||||
filterPlaceholder: "请输入搜索内容",
|
||||
noCheckedFormat: "共 {total} 项",
|
||||
hasCheckedFormat: "已选 {checked}/{total} 项"
|
||||
},
|
||||
image: { error: "加载失败" },
|
||||
pageHeader: { title: "返回" },
|
||||
popconfirm: {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消"
|
||||
},
|
||||
carousel: {
|
||||
leftArrow: "上一张幻灯片",
|
||||
rightArrow: "下一张幻灯片",
|
||||
indicator: "幻灯片切换至索引 {index}"
|
||||
},
|
||||
inputOTP: {
|
||||
groupLabel: "一次性密码输入框",
|
||||
defaultLabel: "请输入第 {index} 位 OTP 字符"
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
zh_cn_default as default
|
||||
};
|
||||
/*! Bundled license information:
|
||||
|
||||
element-plus/dist/locale/zh-cn.mjs:
|
||||
(*! Element Plus v2.14.1 *)
|
||||
*/
|
||||
//# sourceMappingURL=element-plus_dist_locale_zh-cn__mjs.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,348 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-J44PGZX2.js";
|
||||
import "./chunk-5WRI5ZAA.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>教室智能人脸考勤系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "attendance-system",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"element-plus": "^2.6.2",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"echarts": "^5.5.0",
|
||||
"axios": "^1.6.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.2.0",
|
||||
"sass": "^1.72.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/styles/variables.scss' as *;
|
||||
</style>
|
||||
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="data-card fade-in-up" @click="$emit('click')">
|
||||
<div class="card-header">
|
||||
<div class="card-icon" :style="{ background: iconBg }">
|
||||
<el-icon :size="20" :color="iconColor"><component :is="icon" /></el-icon>
|
||||
</div>
|
||||
<span class="card-label">{{ label }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-value">
|
||||
<span class="value-num">{{ value }}</span>
|
||||
<span v-if="unit" class="value-unit">{{ unit }}</span>
|
||||
</div>
|
||||
<div v-if="trend !== undefined" class="card-trend" :class="trend >= 0 ? 'up' : 'down'">
|
||||
<el-icon :size="14">
|
||||
<CaretTop v-if="trend >= 0" />
|
||||
<CaretBottom v-else />
|
||||
</el-icon>
|
||||
<span>{{ Math.abs(trend) }}%</span>
|
||||
<span class="trend-label">较昨日</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: { type: String, required: true },
|
||||
value: { type: [String, Number], required: true },
|
||||
unit: { type: String, default: '' },
|
||||
icon: { type: [String, Object], required: true },
|
||||
iconColor: { type: String, default: '#52c41a' },
|
||||
iconBg: { type: String, default: '#e8f9e0' },
|
||||
trend: { type: Number, default: undefined }
|
||||
})
|
||||
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.value-num {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #262626;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 14px;
|
||||
color: #525252;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 2px;
|
||||
|
||||
&.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
&.down {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.trend-label {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ collapsed: appStore.sidebarCollapsed }">
|
||||
<!-- Logo 区域 -->
|
||||
<div class="sidebar-logo">
|
||||
<div class="logo-icon">
|
||||
<el-icon :size="28"><Camera /></el-icon>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<span v-show="!appStore.sidebarCollapsed" class="logo-text">智能考勤系统</span>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 菜单区域 -->
|
||||
<el-scrollbar class="sidebar-menu">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="appStore.sidebarCollapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
background-color="transparent"
|
||||
text-color="#525252"
|
||||
active-text-color="#ffffff"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/behavior">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>课堂行为分析</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/history">
|
||||
<el-icon><Search /></el-icon>
|
||||
<span>历史记录查询</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/bigscreen">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>数据展示大屏</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="/settings">
|
||||
<template #title>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
<el-menu-item index="/settings/personnel">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>人员管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings/device">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>设备管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings/rules">
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>考勤规则设置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/settings/permissions">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>权限管理</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 底部收起按钮 -->
|
||||
<div class="sidebar-toggle" @click="appStore.toggleSidebar()">
|
||||
<el-icon :size="18">
|
||||
<DArrowLeft v-if="!appStore.sidebarCollapsed" />
|
||||
<DArrowRight v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const { path } = route
|
||||
if (path.startsWith('/settings')) return '/settings'
|
||||
return path
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 240px;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
z-index: 100;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.03);
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #52c41a, #49b018);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
color: #525252;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #52c41a;
|
||||
background: #f0fdf0;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="skeleton-container" :class="{ 'is-card': card }">
|
||||
<div v-if="card" class="skeleton-card">
|
||||
<div class="skeleton skeleton-circle"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line-short"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line-long"></div>
|
||||
</div>
|
||||
<div v-else class="skeleton-table">
|
||||
<div v-for="i in rows" :key="i" class="skeleton-row">
|
||||
<div v-for="j in cols" :key="j" class="skeleton skeleton-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
card: { type: Boolean, default: false },
|
||||
rows: { type: Number, default: 5 },
|
||||
cols: { type: Number, default: 4 }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.skeleton-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skeleton-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
width: 80%;
|
||||
|
||||
&.skeleton-line-short {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
&.skeleton-line-long {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skeleton-cell {
|
||||
flex: 1;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<header class="navbar">
|
||||
<div class="navbar-left">
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/dashboard' }">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="route.meta.title">
|
||||
{{ route.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-right">
|
||||
<!-- 快捷搜索 -->
|
||||
<div class="header-search">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索课程/班级..."
|
||||
:prefix-icon="Search"
|
||||
size="small"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<el-badge :value="appStore.messageCount" :hidden="appStore.messageCount === 0" class="header-badge">
|
||||
<el-button link @click="showNotifications = true">
|
||||
<el-icon :size="20"><Bell /></el-icon>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
|
||||
<!-- 全屏按钮 -->
|
||||
<el-button link @click="toggleFullscreen">
|
||||
<el-icon :size="20"><FullScreen /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<!-- 用户下拉 -->
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
|
||||
<span class="user-name">{{ userStore.userInfo.name }}</span>
|
||||
<el-icon class="user-arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 通知弹窗 -->
|
||||
<el-dialog v-model="showNotifications" title="消息通知" width="420px">
|
||||
<div class="notification-list">
|
||||
<div v-for="(item, index) in notifications" :key="index" class="notification-item">
|
||||
<div class="notif-icon" :class="item.type">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="notif-content">
|
||||
<div class="notif-title">{{ item.title }}</div>
|
||||
<div class="notif-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="notifications.length === 0" description="暂无通知" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const showNotifications = ref(false)
|
||||
|
||||
const notifications = ref([
|
||||
{ title: '今日考勤报告已生成', time: '10分钟前', type: 'success', icon: 'CircleCheckFilled' },
|
||||
{ title: '301教室摄像头离线', time: '30分钟前', type: 'danger', icon: 'WarningFilled' },
|
||||
{ title: '系统更新通知:新增行为分析功能', time: '2小时前', type: 'info', icon: 'InfoFilled' }
|
||||
])
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen()
|
||||
} else {
|
||||
document.documentElement.requestFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCommand = (cmd) => {
|
||||
if (cmd === 'logout') {
|
||||
ElMessage.success('已退出登录')
|
||||
router.push('/login')
|
||||
} else if (cmd === 'profile') {
|
||||
ElMessage.info('个人中心功能开发中')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 56px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
width: 200px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #f5f7fa;
|
||||
border-radius: 20px;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&.is-focus {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 1px #52c41a inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-badge {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-arrow {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.notif-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.success {
|
||||
background: #f0fdf0;
|
||||
color: #52c41a;
|
||||
}
|
||||
&.danger {
|
||||
background: #fff1f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
&.info {
|
||||
background: #e8f4ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.notif-content {
|
||||
flex: 1;
|
||||
|
||||
.notif-title {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.notif-time {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="main-layout" :class="{ collapsed: appStore.sidebarCollapsed }">
|
||||
<!-- 侧边栏 -->
|
||||
<SideMenu />
|
||||
<!-- 右侧主体 -->
|
||||
<div class="main-container">
|
||||
<TopNavbar />
|
||||
<div class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import SideMenu from '@/components/SideMenu.vue'
|
||||
import TopNavbar from '@/components/TopNavbar.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&.collapsed {
|
||||
.main-container {
|
||||
margin-left: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1;
|
||||
margin-left: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin-left 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 路由过渡动画
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/global.scss'
|
||||
import './styles/element-override.scss'
|
||||
|
||||
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, { locale: zhCn })
|
||||
app.mount('#app')
|
||||
@ -0,0 +1,80 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
{
|
||||
path: 'behavior',
|
||||
name: 'Behavior',
|
||||
component: () => import('@/views/behavior/index.vue'),
|
||||
meta: { title: '课堂行为分析', icon: 'TrendCharts' }
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
name: 'History',
|
||||
component: () => import('@/views/history/index.vue'),
|
||||
meta: { title: '历史记录查询', icon: 'Search' }
|
||||
},
|
||||
{
|
||||
path: 'bigscreen',
|
||||
name: 'BigScreen',
|
||||
component: () => import('@/views/bigscreen/index.vue'),
|
||||
meta: { title: '数据展示大屏', icon: 'DataAnalysis' }
|
||||
},
|
||||
{
|
||||
path: 'settings/personnel',
|
||||
name: 'Personnel',
|
||||
component: () => import('@/views/settings/personnel.vue'),
|
||||
meta: { title: '人员管理', icon: 'User' }
|
||||
},
|
||||
{
|
||||
path: 'settings/device',
|
||||
name: 'Device',
|
||||
component: () => import('@/views/settings/device.vue'),
|
||||
meta: { title: '设备管理', icon: 'Monitor' }
|
||||
},
|
||||
{
|
||||
path: 'settings/rules',
|
||||
name: 'Rules',
|
||||
component: () => import('@/views/settings/rules.vue'),
|
||||
meta: { title: '考勤规则设置', icon: 'Setting' }
|
||||
},
|
||||
{
|
||||
path: 'settings/permissions',
|
||||
name: 'Permissions',
|
||||
component: () => import('@/views/settings/permissions.vue'),
|
||||
meta: { title: '权限管理', icon: 'Lock' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title ? `${to.meta.title} - 教室智能人脸考勤系统` : '教室智能人脸考勤系统'
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,29 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const sidebarCollapsed = ref(false)
|
||||
const loading = ref(false)
|
||||
const messageCount = ref(3)
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const setLoading = (val) => {
|
||||
loading.value = val
|
||||
}
|
||||
|
||||
const clearMessage = () => {
|
||||
messageCount.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarCollapsed,
|
||||
loading,
|
||||
messageCount,
|
||||
toggleSidebar,
|
||||
setLoading,
|
||||
clearMessage
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,45 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref({
|
||||
name: '张教务',
|
||||
avatar: '',
|
||||
role: 'admin', // admin | teacher | staff
|
||||
school: '阳光实验学校'
|
||||
})
|
||||
|
||||
const roleName = computed(() => {
|
||||
const map = {
|
||||
admin: '管理员',
|
||||
teacher: '教师',
|
||||
staff: '教务员'
|
||||
}
|
||||
return map[userInfo.value.role] || '未知'
|
||||
})
|
||||
|
||||
const permissions = computed(() => {
|
||||
const permMap = {
|
||||
admin: ['dashboard', 'attendance', 'classroom', 'behavior', 'history', 'bigscreen', 'settings'],
|
||||
staff: ['dashboard', 'attendance', 'classroom', 'history'],
|
||||
teacher: ['dashboard', 'attendance', 'behavior', 'history']
|
||||
}
|
||||
return permMap[userInfo.value.role] || []
|
||||
})
|
||||
|
||||
const hasPermission = (page) => {
|
||||
return permissions.value.includes(page)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userInfo.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
roleName,
|
||||
permissions,
|
||||
hasPermission,
|
||||
logout
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,184 @@
|
||||
// === element-plus 样式覆盖 ===
|
||||
|
||||
// 覆盖 Element Plus 主色
|
||||
:root {
|
||||
--el-color-primary: #52c41a;
|
||||
--el-color-primary-light-3: #84d155;
|
||||
--el-color-primary-light-5: #a7e280;
|
||||
--el-color-primary-light-7: #c9efb0;
|
||||
--el-color-primary-light-8: #dcf5cc;
|
||||
--el-color-primary-light-9: #eefae6;
|
||||
--el-color-primary-dark-2: #49b018;
|
||||
--el-color-success: #52c41a;
|
||||
--el-color-danger: #f5222d;
|
||||
--el-color-warning: #faad14;
|
||||
--el-color-info: #909399;
|
||||
--el-border-radius-base: 6px;
|
||||
--el-border-radius-small: 4px;
|
||||
--el-border-radius-round: 20px;
|
||||
--el-font-family: 'Source Han Sans CN', '思源黑体', 'Noto Sans SC',
|
||||
'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
// 按钮样式覆盖
|
||||
.el-button {
|
||||
&--primary {
|
||||
--el-button-bg-color: #52c41a;
|
||||
--el-button-border-color: #52c41a;
|
||||
--el-button-hover-bg-color: #49b018;
|
||||
--el-button-hover-border-color: #49b018;
|
||||
--el-button-active-bg-color: #3d9412;
|
||||
--el-button-active-border-color: #3d9412;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式覆盖
|
||||
.el-input {
|
||||
--el-input-border-radius: 6px;
|
||||
|
||||
.el-input__wrapper {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 1px #d9d9d9 inset;
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #b3b3b3 inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-focus .el-input__wrapper {
|
||||
box-shadow: 0 0 0 1px #1890ff inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单样式覆盖
|
||||
.el-menu {
|
||||
border-right: none !important;
|
||||
|
||||
.el-menu-item {
|
||||
border-radius: 8px;
|
||||
margin: 2px 8px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
will-change: background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #e8f9e0 !important;
|
||||
color: #52c41a !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: linear-gradient(135deg, #52c41a, #49b018) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu {
|
||||
margin: 2px 8px;
|
||||
|
||||
.el-sub-menu__title {
|
||||
border-radius: 8px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e8f9e0 !important;
|
||||
color: #52c41a !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式覆盖
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式覆盖
|
||||
.el-table {
|
||||
--el-table-border-color: #f0f0f0;
|
||||
--el-table-header-bg-color: #fafafa;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
th.el-table__cell {
|
||||
background-color: #fafafa;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover > td.el-table__cell {
|
||||
background-color: #e8f9e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页样式
|
||||
.el-pagination {
|
||||
--el-pagination-hover-color: #52c41a;
|
||||
margin-top: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 弹窗样式
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-dialog__header {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记/标签样式
|
||||
.el-tag {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 下拉菜单样式
|
||||
.el-dropdown-menu {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
// 选择器样式
|
||||
.el-select {
|
||||
--el-select-border-color-hover: #b3b3b3;
|
||||
--el-select-input-focus-border-color: #1890ff;
|
||||
}
|
||||
|
||||
// 日期选择器样式
|
||||
.el-date-editor {
|
||||
--el-date-editor-active-border-color: #1890ff;
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
// === 全局样式 ===
|
||||
@use './variables.scss' as *;
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: $font-family;
|
||||
font-size: $font-body;
|
||||
color: $text-primary;
|
||||
background-color: $bg-page;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-primary;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用工具类
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: $font-caption;
|
||||
}
|
||||
|
||||
// 页面通用布局
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - $navbar-height);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.page-title {
|
||||
font-size: $font-title;
|
||||
font-weight: 700;
|
||||
color: $text-primary;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: $font-caption;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选区样式
|
||||
.filter-bar {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-md;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 统计卡片区域
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 骨架屏动画
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 37%, #f0f0f0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.4s ease infinite;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
// 淡入动画
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.4s ease forwards;
|
||||
}
|
||||
|
||||
// 脉冲动画
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
// 响应式布局辅助
|
||||
@media (max-width: $breakpoint-sm) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json;charset=UTF-8' }
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可在此添加 token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const { code, message, data } = response.data
|
||||
if (code === 200) {
|
||||
return data
|
||||
}
|
||||
ElMessage.error(message || '请求失败')
|
||||
return Promise.reject(new Error(message))
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const { status } = error.response
|
||||
switch (status) {
|
||||
case 401:
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有访问权限')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器异常,请稍后重试')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(`请求错误 ${status}`)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('网络异常,请检查网络连接')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="props.modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
:title="`${props.detailData.courseName} - 考勤详情`"
|
||||
width="720px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="detail-header">
|
||||
<span>{{ props.detailData.time }}</span>
|
||||
<span>{{ props.detailData.classroom }}</span>
|
||||
<span>应到 {{ props.detailData.total }} / 实到 {{ props.detailData.actual }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="detailTab">
|
||||
<el-tab-pane label="未签到学生" name="absent">
|
||||
<el-table :data="absentStudents" stripe max-height="300">
|
||||
<el-table-column type="selection" width="45" />
|
||||
<el-table-column prop="studentId" label="学号" width="120" />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="className" label="班级" min-width="140" />
|
||||
<el-table-column prop="reason" label="原因" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.reason === '请假' ? 'warning' : 'danger'">
|
||||
{{ row.reason }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="出勤统计" name="stats">
|
||||
<div ref="detailChartRef" style="height: 300px"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:modelValue', false)">关闭</el-button>
|
||||
<el-button type="primary" @click="batchExport">批量导出</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
detailData: { type: Object, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const detailTab = ref('absent')
|
||||
const detailChartRef = ref(null)
|
||||
|
||||
const absentStudents = ref([
|
||||
{ studentId: '2021001', name: '张三', className: '计算机2021-1班', reason: '缺勤' },
|
||||
{ studentId: '2021015', name: '李四', className: '计算机2021-1班', reason: '请假' },
|
||||
{ studentId: '2021020', name: '王五', className: '计算机2021-1班', reason: '缺勤' }
|
||||
])
|
||||
|
||||
const initDetailChart = () => {
|
||||
if (!detailChartRef.value) return
|
||||
const chart = echarts.init(detailChartRef.value)
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['出勤', '缺勤', '请假'], bottom: 0 },
|
||||
grid: { top: 20, right: 20, bottom: 40, left: 40 },
|
||||
xAxis: { type: 'category', data: ['5/26', '5/27', '5/28', '5/29', '5/30', '5/31', '6/01'] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: '出勤', type: 'bar', stack: 'total', data: [40, 42, 43, 41, 44, 43, 44], color: '#52c41a' },
|
||||
{ name: '缺勤', type: 'bar', stack: 'total', data: [3, 1, 0, 2, 1, 0, 1], color: '#f5222d' },
|
||||
{ name: '请假', type: 'bar', stack: 'total', data: [2, 2, 2, 2, 0, 2, 0], color: '#faad14' }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const batchExport = () => ElMessage.success('批量导出成功')
|
||||
|
||||
// 弹窗打开时初始化图表
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
detailTab.value = 'absent'
|
||||
nextTick(() => initDetailChart())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
color: #525252;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
size="default"
|
||||
/>
|
||||
<el-select v-model="filters.department" placeholder="选择学院" clearable size="default" style="width: 150px">
|
||||
<el-option label="计算机学院" value="cs" />
|
||||
<el-option label="数学学院" value="math" />
|
||||
<el-option label="外国语学院" value="english" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.class" placeholder="选择班级" clearable size="default" style="width: 150px">
|
||||
<el-option label="2021级1班" value="c1" />
|
||||
<el-option label="2022级2班" value="c2" />
|
||||
<el-option label="2023级1班" value="c3" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索课程/班级..."
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
size="default"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计条 -->
|
||||
<div class="summary-bar">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">总课程数</span>
|
||||
<span class="summary-value">{{ tableData.length }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">应到总人次</span>
|
||||
<span class="summary-value">{{ totalShould }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">实到总人次</span>
|
||||
<span class="summary-value green">{{ totalActual }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">平均缺勤率</span>
|
||||
<span class="summary-value red">{{ avgAbsentRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" stripe v-loading="loading">
|
||||
<el-table-column prop="courseName" label="课程名称" min-width="150" />
|
||||
<el-table-column prop="teacher" label="授课教师" width="100" />
|
||||
<el-table-column prop="classroom" label="教室" width="120" />
|
||||
<el-table-column prop="time" label="上课时间" width="170" sortable />
|
||||
<el-table-column prop="total" label="应到" width="70" align="center" />
|
||||
<el-table-column prop="actual" label="实到" width="70" align="center" />
|
||||
<el-table-column prop="absentCount" label="缺勤" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-red': row.absentCount > 3 }">{{ row.absentCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="absentRate" label="缺勤率" width="90" align="center" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAbsentType(row.absentRate)" size="small">{{ row.absentRate }}%</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="$emit('showDetail', row)">详情</el-button>
|
||||
<el-button link size="small" @click="handleExport(row)">导出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="tableData.length"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineEmits(['showDetail'])
|
||||
|
||||
const loading = ref(false)
|
||||
const filters = reactive({ dateRange: null, department: '', class: '', keyword: '' })
|
||||
const pagination = reactive({ current: 1, pageSize: 10 })
|
||||
|
||||
const tableData = ref([
|
||||
{ courseName: '高等数学A', teacher: '王教授', classroom: '301教室', time: '2024-06-01 08:00-09:40', total: 45, actual: 44, absentCount: 1, absentRate: 2.2 },
|
||||
{ courseName: '大学英语B', teacher: '李老师', classroom: '205教室', time: '2024-06-01 10:00-11:40', total: 38, actual: 38, absentCount: 0, absentRate: 0 },
|
||||
{ courseName: '计算机导论', teacher: '张教授', classroom: '102实验室', time: '2024-06-01 14:00-15:40', total: 52, actual: 49, absentCount: 3, absentRate: 5.8 },
|
||||
{ courseName: '线性代数', teacher: '赵教授', classroom: '408教室', time: '2024-05-31 08:00-09:40', total: 40, actual: 38, absentCount: 2, absentRate: 5.0 },
|
||||
{ courseName: '马克思原理', teacher: '刘老师', classroom: '大阶梯教室', time: '2024-05-31 10:00-11:40', total: 120, actual: 108, absentCount: 12, absentRate: 10.0 },
|
||||
{ courseName: '数据结构', teacher: '陈教授', classroom: '201教室', time: '2024-05-31 14:00-15:40', total: 48, actual: 47, absentCount: 1, absentRate: 2.1 },
|
||||
{ courseName: '操作系统', teacher: '杨教授', classroom: '310教室', time: '2024-05-30 08:00-09:40', total: 42, actual: 40, absentCount: 2, absentRate: 4.8 },
|
||||
{ courseName: '数据库原理', teacher: '周老师', classroom: '105实验室', time: '2024-05-30 10:00-11:40', total: 36, actual: 36, absentCount: 0, absentRate: 0 }
|
||||
])
|
||||
|
||||
const totalShould = computed(() => tableData.value.reduce((s, r) => s + r.total, 0))
|
||||
const totalActual = computed(() => tableData.value.reduce((s, r) => s + r.actual, 0))
|
||||
const avgAbsentRate = computed(() => {
|
||||
return tableData.value.length
|
||||
? (tableData.value.reduce((s, r) => s + r.absentRate, 0) / tableData.value.length).toFixed(1)
|
||||
: 0
|
||||
})
|
||||
|
||||
const getAbsentType = (rate) => (rate >= 10 ? 'danger' : rate > 5 ? 'warning' : 'success')
|
||||
|
||||
const handleSearch = () => ElMessage.success('搜索完成')
|
||||
const handleReset = () => {
|
||||
Object.assign(filters, { dateRange: null, department: '', class: '', keyword: '' })
|
||||
ElMessage.info('已重置')
|
||||
}
|
||||
const handleExport = (row) => ElMessage.success(`正在导出 ${row.courseName} 考勤明细...`)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-bar {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #262626;
|
||||
|
||||
&.green { color: #52c41a; }
|
||||
&.red { color: #f5222d; }
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: #f5222d;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>近7天出勤趋势</h3>
|
||||
<el-tag size="small" type="success">实时</el-tag>
|
||||
</div>
|
||||
<div ref="trendChartRef" class="chart-body"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const trendChartRef = ref(null)
|
||||
let trendChart = null
|
||||
|
||||
const initTrendChart = () => {
|
||||
if (!trendChartRef.value) return
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { top: 20, right: 20, bottom: 20, left: 40 },
|
||||
xAxis: { type: 'category', data: ['5/26', '5/27', '5/28', '5/29', '5/30', '5/31', '6/01'], axisTick: { show: false } },
|
||||
yAxis: { type: 'value', min: 80, max: 100, axisLabel: { formatter: '{value}%' } },
|
||||
series: [{
|
||||
data: [93, 95, 94.5, 96, 95.8, 97, 96.8],
|
||||
type: 'line', smooth: true, symbol: 'circle', symbolSize: 6,
|
||||
lineStyle: { color: '#52c41a', width: 3 }, itemStyle: { color: '#52c41a' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(82,196,26,0.15)' },
|
||||
{ offset: 1, color: 'rgba(82,196,26,0.02)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
const handleResize = () => trendChart?.resize()
|
||||
|
||||
onMounted(() => {
|
||||
initTrendChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
trendChart?.dispose()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
height: 240px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<h3>各班级出勤率对比</h3>
|
||||
</div>
|
||||
<div class="class-rank">
|
||||
<div v-for="(item, index) in classRank" :key="index" class="rank-item">
|
||||
<span class="rank-num" :class="{ top: index < 3 }">{{ index + 1 }}</span>
|
||||
<span class="rank-name">{{ item.name }}</span>
|
||||
<div class="rank-bar-wrap">
|
||||
<div
|
||||
class="rank-bar"
|
||||
:style="{ width: item.rate + '%', background: index < 3 ? '#52c41a' : '#1890ff' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="rank-value">{{ item.rate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const classRank = ref([
|
||||
{ name: '计算机科学2021-1班', rate: 98.5 },
|
||||
{ name: '软件工程2022-2班', rate: 97.2 },
|
||||
{ name: '人工智能2023-1班', rate: 96.8 },
|
||||
{ name: '大数据2021-2班', rate: 95.4 },
|
||||
{ name: '网络安全2022-1班', rate: 94.0 }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.class-rank {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rank-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.top {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
width: 140px;
|
||||
font-size: 13px;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rank-bar-wrap {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rank-bar {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
.rank-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
width: 45px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 核心数据区 -->
|
||||
<div class="stats-grid">
|
||||
<DataCard
|
||||
label="今日出勤率"
|
||||
:value="stats.attendanceRate"
|
||||
unit="%"
|
||||
icon="UserFilled"
|
||||
iconColor="#52c41a"
|
||||
iconBg="#e8f9e0"
|
||||
:trend="2.5"
|
||||
@click="activeTab = 'manage'"
|
||||
/>
|
||||
<DataCard
|
||||
label="教室平均使用率"
|
||||
:value="stats.classroomUsage"
|
||||
unit="%"
|
||||
icon="OfficeBuilding"
|
||||
iconColor="#1890ff"
|
||||
iconBg="#e8f4ff"
|
||||
:trend="-1.2"
|
||||
@click="$router.push('/bigscreen')"
|
||||
/>
|
||||
<DataCard
|
||||
label="异常行为预警数"
|
||||
:value="stats.warningCount"
|
||||
unit="次"
|
||||
icon="WarningFilled"
|
||||
iconColor="#f5222d"
|
||||
iconBg="#fff1f0"
|
||||
:trend="-15.0"
|
||||
@click="$router.push('/behavior')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="quick-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="activeTab = 'manage'">新增考勤任务</el-button>
|
||||
<el-button :icon="Download" @click="handleExportAll">导出考勤报表</el-button>
|
||||
<el-button :icon="VideoCamera" @click="$router.push('/bigscreen')">查看实时监控</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 图表区 -->
|
||||
<div class="charts-row">
|
||||
<AttendanceTrendChart />
|
||||
<ClassRanking />
|
||||
</div>
|
||||
|
||||
<!-- 考勤记录区 -->
|
||||
<div class="records-card">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="考勤概览" name="overview">
|
||||
<div class="records-header">
|
||||
<h3>最近考勤记录</h3>
|
||||
</div>
|
||||
<el-table :data="recentRecords" stripe>
|
||||
<el-table-column prop="courseName" label="课程名称" min-width="150" />
|
||||
<el-table-column prop="classroom" label="教室" width="120" />
|
||||
<el-table-column prop="time" label="上课时间" width="170" sortable />
|
||||
<el-table-column prop="total" label="应到" width="70" align="center" />
|
||||
<el-table-column prop="actual" label="实到" width="70" align="center" />
|
||||
<el-table-column prop="absentRate" label="缺勤率" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.absentRate > 10 ? 'danger' : 'success'" size="small">
|
||||
{{ row.absentRate }}%
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
||||
<el-button link size="small" @click="handleExport(row)">导出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="考勤管理" name="manage">
|
||||
<AttendanceManage @showDetail="showDetail" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<AttendanceDetail v-model="detailVisible" :detail-data="currentDetail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import DataCard from '@/components/DataCard.vue'
|
||||
import AttendanceTrendChart from './components/AttendanceTrendChart.vue'
|
||||
import ClassRanking from './components/ClassRanking.vue'
|
||||
import AttendanceManage from './components/AttendanceManage.vue'
|
||||
import AttendanceDetail from './components/AttendanceDetail.vue'
|
||||
|
||||
// ===== 数据卡片 =====
|
||||
const stats = ref({ attendanceRate: 96.8, classroomUsage: 78.5, warningCount: 3 })
|
||||
|
||||
// ===== tabs =====
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// ===== 考勤概览 =====
|
||||
const recentRecords = ref([
|
||||
{ courseName: '高等数学A', teacher: '王教授', classroom: '301教室', time: '2024-06-01 08:00-09:40', total: 45, actual: 44, absentCount: 1, absentRate: 2.2 },
|
||||
{ courseName: '大学英语B', teacher: '李老师', classroom: '205教室', time: '2024-06-01 10:00-11:40', total: 38, actual: 38, absentCount: 0, absentRate: 0 },
|
||||
{ courseName: '计算机导论', teacher: '张教授', classroom: '102实验室', time: '2024-06-01 14:00-15:40', total: 52, actual: 49, absentCount: 3, absentRate: 5.8 },
|
||||
{ courseName: '线性代数', teacher: '赵教授', classroom: '408教室', time: '2024-05-31 08:00-09:40', total: 40, actual: 38, absentCount: 2, absentRate: 5.0 },
|
||||
{ courseName: '马克思原理', teacher: '刘老师', classroom: '大阶梯教室', time: '2024-05-31 10:00-11:40', total: 120, actual: 108, absentCount: 12, absentRate: 10.0 }
|
||||
])
|
||||
|
||||
// ===== 详情弹窗 =====
|
||||
const detailVisible = ref(false)
|
||||
const currentDetail = ref({})
|
||||
|
||||
const showDetail = (row) => {
|
||||
currentDetail.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// ===== 导出 =====
|
||||
const handleExport = (row) => ElMessage.success(`正在导出 ${row.courseName} 考勤明细...`)
|
||||
const handleExportAll = () => ElMessage.info('正在生成考勤报表...')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-button {
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
&:not(.el-button--primary) {
|
||||
border-color: #d9d9d9;
|
||||
color: #525252;
|
||||
|
||||
&:hover {
|
||||
color: #52c41a;
|
||||
border-color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.records-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 8px 20px 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.records-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.charts-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,397 @@
|
||||
<template>
|
||||
<div class="page-container fade-in-up">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">历史记录查询</h2>
|
||||
<p class="page-subtitle">查询课程考勤与行为识别历史记录</p>
|
||||
</div>
|
||||
|
||||
<!-- 查询区 -->
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="query.courseName"
|
||||
placeholder="输入课程名称"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
size="default"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-date-picker v-model="query.date" type="date" placeholder="选择日期" size="default" />
|
||||
<el-time-select
|
||||
v-model="query.startTime"
|
||||
start="08:00"
|
||||
step="00:30"
|
||||
end="18:00"
|
||||
placeholder="开始时间"
|
||||
size="default"
|
||||
/>
|
||||
<el-time-select
|
||||
v-model="query.endTime"
|
||||
start="08:00"
|
||||
step="00:30"
|
||||
end="18:00"
|
||||
placeholder="结束时间"
|
||||
size="default"
|
||||
/>
|
||||
<el-select v-model="query.behaviorType" placeholder="行为类型" clearable size="default" style="width: 140px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="专注" value="focus" />
|
||||
<el-option label="举手" value="hand" />
|
||||
<el-option label="低头" value="down" />
|
||||
<el-option label="交谈" value="talk" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" @click="doSearch">查询</el-button>
|
||||
<el-button :icon="RefreshRight" @click="doReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 时间轴工具栏 -->
|
||||
<div class="timeline-toolbar">
|
||||
<div class="timeline-slider">
|
||||
<span class="slider-label">时间轴</span>
|
||||
<el-slider v-model="timelinePosition" :max="100" :step="10" show-stops size="small" style="flex: 1" />
|
||||
<span class="slider-time">{{ currentTimeline }}</span>
|
||||
</div>
|
||||
<el-button-group size="small">
|
||||
<el-button :icon="VideoPlay" @click="toggleSlideshow">{{ playing ? '暂停' : '连续播放' }}</el-button>
|
||||
<el-button :icon="FullScreen" @click="fullscreenMode = true">全屏</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- 图片宫格 -->
|
||||
<div class="result-section">
|
||||
<div class="section-header">
|
||||
<h3>查询结果</h3>
|
||||
<span class="result-count">共 {{ filteredImages.length }} 条记录</span>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="image-masonry">
|
||||
<div
|
||||
v-for="(img, index) in filteredImages"
|
||||
:key="index"
|
||||
class="masonry-item"
|
||||
@click="viewOriginal(img)"
|
||||
>
|
||||
<div class="masonry-thumb">
|
||||
<div class="thumb-placeholder" :style="{ borderColor: getBorderColor(img.type) }">
|
||||
<el-icon :size="28" :color="getBorderColor(img.type)">
|
||||
<PictureFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="thumb-badge">
|
||||
<el-tag :type="getTagType(img.type)" size="small" effect="dark">{{ img.type }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="masonry-info">
|
||||
<span class="masonry-time">
|
||||
<el-icon :size="12"><Clock /></el-icon>
|
||||
{{ img.time }}
|
||||
</span>
|
||||
<span class="masonry-course">{{ img.course }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
:total="filteredImages.length"
|
||||
:page-size="pagination.pageSize"
|
||||
small
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
style="justify-content: center; margin-top: 16px"
|
||||
/>
|
||||
|
||||
<el-empty v-if="!loading && filteredImages.length === 0" description="暂无数据,请调整查询条件" />
|
||||
</div>
|
||||
|
||||
<!-- 原图预览 -->
|
||||
<el-dialog v-model="previewVisible" title="原图预览" width="700px" destroy-on-close>
|
||||
<div class="original-preview">
|
||||
<div class="preview-image-area">
|
||||
<el-icon :size="80" color="#d9d9d9"><PictureFilled /></el-icon>
|
||||
</div>
|
||||
<div class="preview-detail">
|
||||
<span>{{ currentImage?.time }}</span>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tag :type="getTagType(currentImage?.type)" size="small">{{ currentImage?.type }}</el-tag>
|
||||
<el-divider direction="vertical" />
|
||||
<span>{{ currentImage?.course }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 全屏连续播放 -->
|
||||
<el-dialog v-model="fullscreenMode" title="连续播放" fullscreen destroy-on-close>
|
||||
<div class="slideshow-container">
|
||||
<div class="slideshow-image">
|
||||
<el-icon :size="100" color="#d9d9d9"><VideoCamera /></el-icon>
|
||||
<p class="slideshow-hint">连续播放模式 - 图片序列展示区域</p>
|
||||
</div>
|
||||
<div class="slideshow-controls">
|
||||
<el-button-group>
|
||||
<el-button :icon="DArrowLeft">上一张</el-button>
|
||||
<el-button type="primary" @click="toggleSlideshow">
|
||||
<el-icon><component :is="playing ? 'VideoPause' : 'VideoPlay'" /></el-icon>
|
||||
{{ playing ? '暂停' : '播放' }}
|
||||
</el-button>
|
||||
<el-button :icon="DArrowRight">下一张</el-button>
|
||||
</el-button-group>
|
||||
<el-slider v-model="timelinePosition" style="width: 300px" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const playing = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const fullscreenMode = ref(false)
|
||||
const currentImage = ref(null)
|
||||
const timelinePosition = ref(50)
|
||||
|
||||
const query = reactive({
|
||||
courseName: '',
|
||||
date: null,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
behaviorType: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
const images = ref([
|
||||
{ time: '2024-06-01 08:15', type: '专注', course: '高等数学A' },
|
||||
{ time: '2024-06-01 08:18', type: '举手', course: '高等数学A' },
|
||||
{ time: '2024-06-01 08:22', type: '专注', course: '高等数学A' },
|
||||
{ time: '2024-06-01 08:30', type: '低头', course: '高等数学A' },
|
||||
{ time: '2024-06-01 10:05', type: '专注', course: '大学英语B' },
|
||||
{ time: '2024-06-01 10:12', type: '交谈', course: '大学英语B' },
|
||||
{ time: '2024-06-01 14:10', type: '专注', course: '计算机导论' },
|
||||
{ time: '2024-06-01 14:20', type: '举手', course: '计算机导论' },
|
||||
{ time: '2024-05-31 08:15', type: '专注', course: '线性代数' },
|
||||
{ time: '2024-05-31 08:30', type: '低头', course: '线性代数' },
|
||||
{ time: '2024-05-31 10:20', type: '专注', course: '马克思原理' },
|
||||
{ time: '2024-05-31 10:40', type: '交谈', course: '马克思原理' }
|
||||
])
|
||||
|
||||
const filteredImages = computed(() => images.value)
|
||||
|
||||
const currentTimeline = computed(() => {
|
||||
const times = images.value.map(i => i.time)
|
||||
return times[Math.floor(timelinePosition.value / 100 * (times.length - 1))] || ''
|
||||
})
|
||||
|
||||
const getBorderColor = (type) => {
|
||||
const map = { '专注': '#52c41a', '举手': '#1890ff', '低头': '#722ed1', '交谈': '#faad14' }
|
||||
return map[type] || '#d9d9d9'
|
||||
}
|
||||
|
||||
const getTagType = (type) => {
|
||||
const map = { '专注': 'success', '举手': '', '低头': 'info', '交谈': 'warning' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
const viewOriginal = (img) => {
|
||||
currentImage.value = img
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const doSearch = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => { loading.value = false }, 500)
|
||||
}
|
||||
|
||||
const doReset = () => {
|
||||
query.courseName = ''
|
||||
query.date = null
|
||||
query.startTime = ''
|
||||
query.endTime = ''
|
||||
query.behaviorType = ''
|
||||
}
|
||||
|
||||
const toggleSlideshow = () => {
|
||||
playing.value = !playing.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-toolbar {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.timeline-slider {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.slider-label {
|
||||
font-size: 12px;
|
||||
color: #525252;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slider-time {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
font-family: monospace;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.result-count {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.image-masonry {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.masonry-item {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
&:hover {
|
||||
border-color: #52c41a;
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.masonry-thumb {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
height: 140px;
|
||||
background: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 3px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.thumb-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.masonry-info {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.masonry-time {
|
||||
font-size: 12px;
|
||||
color: #525252;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.masonry-course {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.original-preview {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-image-area {
|
||||
height: 360px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.slideshow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.slideshow-image {
|
||||
width: 80%;
|
||||
max-width: 900px;
|
||||
height: 500px;
|
||||
background: #1a1a2e;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
color: #525252;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.slideshow-hint {
|
||||
font-size: 16px;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
.slideshow-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="page-container fade-in-up">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">考勤规则设置</h2>
|
||||
<p class="page-subtitle">自定义各课程考勤规则与异常预警参数</p>
|
||||
</div>
|
||||
|
||||
<div class="rules-grid">
|
||||
<!-- 缺勤阈值设置 -->
|
||||
<div class="rule-card">
|
||||
<div class="rule-card-header">
|
||||
<el-icon :size="18" color="#52c41a"><Warning /></el-icon>
|
||||
<h4>缺勤阈值设置</h4>
|
||||
</div>
|
||||
<el-form label-width="120px" label-position="left">
|
||||
<el-form-item label="迟到判定(分钟)">
|
||||
<el-input-number v-model="rules.lateMinutes" :min="1" :max="60" size="default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缺勤预警阈值(%)">
|
||||
<el-slider v-model="rules.absentWarning" :min="5" :max="30" :step="5" show-stops />
|
||||
</el-form-item>
|
||||
<el-form-item label="迟到预警阈值(%)">
|
||||
<el-slider v-model="rules.lateWarning" :min="5" :max="30" :step="5" show-stops />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 考勤时段设置 -->
|
||||
<div class="rule-card">
|
||||
<div class="rule-card-header">
|
||||
<el-icon :size="18" color="#1890ff"><Clock /></el-icon>
|
||||
<h4>考勤时段设置</h4>
|
||||
</div>
|
||||
<el-form label-width="120px" label-position="left">
|
||||
<el-form-item label="上午考勤时段">
|
||||
<el-time-picker
|
||||
v-model="rules.morningTime"
|
||||
is-range
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
format="HH:mm"
|
||||
size="default"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="下午考勤时段">
|
||||
<el-time-picker
|
||||
v-model="rules.afternoonTime"
|
||||
is-range
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
format="HH:mm"
|
||||
size="default"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="晚自习时段">
|
||||
<el-time-picker
|
||||
v-model="rules.eveningTime"
|
||||
is-range
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
format="HH:mm"
|
||||
size="default"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 异常预警规则 -->
|
||||
<div class="rule-card">
|
||||
<div class="rule-card-header">
|
||||
<el-icon :size="18" color="#f5222d"><Bell /></el-icon>
|
||||
<h4>异常预警规则</h4>
|
||||
</div>
|
||||
<el-form label-width="140px" label-position="left">
|
||||
<el-form-item label="连续缺勤预警(次)">
|
||||
<el-input-number v-model="rules.continuousAbsent" :min="2" :max="10" size="default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="专注度低阈值(%)">
|
||||
<el-input-number v-model="rules.lowFocus" :min="30" :max="80" :step="5" size="default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="异常行为频次阈值">
|
||||
<el-input-number v-model="rules.abnormalFreq" :min="1" :max="20" size="default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮件通知管理员">
|
||||
<el-switch v-model="rules.emailNotify" active-color="#52c41a" />
|
||||
</el-form-item>
|
||||
<el-form-item label="短信通知班主任">
|
||||
<el-switch v-model="rules.smsNotify" active-color="#52c41a" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 课程特殊规则 -->
|
||||
<div class="rule-card rule-card-full">
|
||||
<div class="rule-card-header">
|
||||
<el-icon :size="18" color="#722ed1"><Notebook /></el-icon>
|
||||
<h4>课程特殊考勤规则</h4>
|
||||
</div>
|
||||
<el-table :data="courseRules" stripe>
|
||||
<el-table-column prop="courseName" label="课程名称" min-width="160" />
|
||||
<el-table-column prop="attendanceType" label="考勤方式" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.attendanceType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="absentThreshold" label="缺勤阈值" width="100" align="center" />
|
||||
<el-table-column prop="autoFlag" label="自动标记" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.autoFlag" size="small" active-color="#52c41a" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default>
|
||||
<el-button link type="primary" size="small">编辑</el-button>
|
||||
<el-button link type="danger" size="small">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top: 12px">
|
||||
<el-button :icon="Plus" size="small" @click="addCourseRule">添加课程规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<el-button type="primary" size="large" :icon="Check" @click="saveRules">保存配置</el-button>
|
||||
<el-button size="large" @click="resetRules">恢复默认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const rules = ref({
|
||||
lateMinutes: 10,
|
||||
absentWarning: 10,
|
||||
lateWarning: 15,
|
||||
morningTime: [new Date(2024, 0, 1, 8, 0), new Date(2024, 0, 1, 12, 0)],
|
||||
afternoonTime: [new Date(2024, 0, 1, 14, 0), new Date(2024, 0, 1, 17, 30)],
|
||||
eveningTime: [new Date(2024, 0, 1, 19, 0), new Date(2024, 0, 1, 21, 30)],
|
||||
continuousAbsent: 3,
|
||||
lowFocus: 50,
|
||||
abnormalFreq: 5,
|
||||
emailNotify: true,
|
||||
smsNotify: false
|
||||
})
|
||||
|
||||
const courseRules = ref([
|
||||
{ courseName: '高等数学A', attendanceType: '人脸签到', absentThreshold: '10%', autoFlag: true },
|
||||
{ courseName: '大学英语B', attendanceType: '人脸签到', absentThreshold: '15%', autoFlag: true },
|
||||
{ courseName: '计算机导论', attendanceType: '人脸+码签到', absentThreshold: '10%', autoFlag: true }
|
||||
])
|
||||
|
||||
const saveRules = () => {
|
||||
ElMessage.success('考勤规则保存成功,已实时生效')
|
||||
}
|
||||
|
||||
const resetRules = () => {
|
||||
ElMessage.info('已恢复默认配置')
|
||||
}
|
||||
|
||||
const addCourseRule = () => {
|
||||
ElMessage.info('添加课程规则功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&.rule-card-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h4 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
|
||||
.el-button {
|
||||
border-radius: 8px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.rules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "@/styles/variables.scss" as *;`
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue