Browse Source

保存文件弹窗

Steven 1 month ago
parent
commit
1c52a5cf65

+ 10 - 0
KulexiuForTeacher/KulexiuForTeacher.xcodeproj/project.pbxproj

@@ -840,6 +840,8 @@
 		BCA1135828A2439D007FAFB9 /* HomeBannerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCA1135628A2439C007FAFB9 /* HomeBannerCell.xib */; };
 		BCA193B5282A80AA004A585D /* RecentCourseModel.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA193B3282A80A9004A585D /* RecentCourseModel.m */; };
 		BCA193BE282B9866004A585D /* MinePageGroupModel.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA193BC282B9866004A585D /* MinePageGroupModel.m */; };
+		BCA3369E2C942B340074DF75 /* KSSourceDownloadAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCA3369C2C942B340074DF75 /* KSSourceDownloadAlert.xib */; };
+		BCA3369F2C942B340074DF75 /* KSSourceDownloadAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA3369D2C942B340074DF75 /* KSSourceDownloadAlert.m */; };
 		BCA353CE2858A86200377661 /* MusicRoomCourseCell.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA353CC2858A86200377661 /* MusicRoomCourseCell.m */; };
 		BCA353CF2858A86200377661 /* MusicRoomCourseCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCA353CD2858A86200377661 /* MusicRoomCourseCell.xib */; };
 		BCA353D62858B18100377661 /* Records.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA353D32858B17E00377661 /* Records.m */; };
@@ -2526,6 +2528,9 @@
 		BCA193B4282A80A9004A585D /* RecentCourseModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentCourseModel.h; sourceTree = "<group>"; };
 		BCA193BC282B9866004A585D /* MinePageGroupModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MinePageGroupModel.m; sourceTree = "<group>"; };
 		BCA193BD282B9866004A585D /* MinePageGroupModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MinePageGroupModel.h; sourceTree = "<group>"; };
+		BCA3369B2C942B340074DF75 /* KSSourceDownloadAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSSourceDownloadAlert.h; sourceTree = "<group>"; };
+		BCA3369C2C942B340074DF75 /* KSSourceDownloadAlert.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = KSSourceDownloadAlert.xib; sourceTree = "<group>"; };
+		BCA3369D2C942B340074DF75 /* KSSourceDownloadAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSSourceDownloadAlert.m; sourceTree = "<group>"; };
 		BCA353CB2858A86200377661 /* MusicRoomCourseCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MusicRoomCourseCell.h; sourceTree = "<group>"; };
 		BCA353CC2858A86200377661 /* MusicRoomCourseCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MusicRoomCourseCell.m; sourceTree = "<group>"; };
 		BCA353CD2858A86200377661 /* MusicRoomCourseCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MusicRoomCourseCell.xib; sourceTree = "<group>"; };
@@ -5239,6 +5244,9 @@
 			children = (
 				277935E927E32A930010E277 /* KSBaseWKWebViewController.h */,
 				277935EA27E32A930010E277 /* KSBaseWKWebViewController.m */,
+				BCA3369B2C942B340074DF75 /* KSSourceDownloadAlert.h */,
+				BCA3369D2C942B340074DF75 /* KSSourceDownloadAlert.m */,
+				BCA3369C2C942B340074DF75 /* KSSourceDownloadAlert.xib */,
 				BC0D951A2AC2868400E54D3F /* KSWebLoadRefreshView.h */,
 				BC0D951B2AC2868400E54D3F /* KSWebLoadRefreshView.m */,
 				BC0D951C2AC2868400E54D3F /* KSWebLoadRefreshView.xib */,
@@ -7670,6 +7678,7 @@
 				BCC9F40727F69BD200647449 /* SealClass.strings in Resources */,
 				BCD6D16C28195FC5009A773E /* CashRecordHeadView.xib in Resources */,
 				BC5EB5AB2803D86500B4A3B0 /* AccompanyAlertView.xib in Resources */,
+				BCA3369E2C942B340074DF75 /* KSSourceDownloadAlert.xib in Resources */,
 				BCE6A08C27F823A300C97704 /* AccompanyCourseCell.xib in Resources */,
 				BC7CFFBA2817E9FC00CAEB21 /* echarts.min.js in Resources */,
 				BC3673D528A606A500059721 /* live_animation_2.png in Resources */,
@@ -8002,6 +8011,7 @@
 				BC8B6E6828575DD200866917 /* MusicDisplayView.m in Sources */,
 				BC31BF802B219C5700F7D538 /* ToneTuningViewController.m in Sources */,
 				27BC3B2527F2B76900D81E30 /* MusicMessageCell.m in Sources */,
+				BCA3369F2C942B340074DF75 /* KSSourceDownloadAlert.m in Sources */,
 				BC71DF062A89F470003F165E /* TXFullVideoView.m in Sources */,
 				BCC9F42E27F69BD200647449 /* RolePortraitView.m in Sources */,
 				277931ED27E30FC20010E277 /* zhPopupController.m in Sources */,

+ 227 - 78
KulexiuForTeacher/KulexiuForTeacher/Common/Base/WebView/KSBaseWKWebViewController.m

@@ -39,6 +39,7 @@
 
 #import "KSNewAlertView.h"
 #import "KSLogManager.h"
+#import "KSSourceDownloadAlert.h"
 
 typedef NS_ENUM(NSInteger, CHOOSETYPE) {
     CHOOSETYPE_XML,
@@ -74,13 +75,20 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
 
 @property (nonatomic, strong) KSUMShareManager *shareManager;
 
-@property (nonatomic, assign) BOOL isDownloadFile;
 
 @property (nonatomic, strong) KSNewAlertView *wifiAlert;
 
 // 是否需要重新加载
 @property (nonatomic, assign) BOOL needReload;
 
+@property (nonatomic, assign) BOOL isDownloadFile;
+
+@property (nonatomic, strong) NSDictionary *downloadParm;
+// 下载文件地址
+@property (nonatomic, strong) NSURL *downloadFileUrl;
+//文件预览(写全局变量,否则操作不成功)
+@property (nonatomic, strong) UIDocumentInteractionController *documentVC;
+
 @end
 
 @implementation KSBaseWKWebViewController
@@ -377,6 +385,10 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
             }];
         }
     }
+    else if ([[parm ks_stringValueForKey:@"api"] isEqualToString:@"downloadFile"]) {
+        NSString *url = [[parm ks_dictionaryValueForKey:@"content"] ks_stringValueForKey:@"downloadUrl"];
+        [self downloadFileWithUrl:url parm:parm];
+    }
     // 回调是否刘海屏
     else if ([[parm ks_stringValueForKey:@"api"] isEqualToString:@"isSpecialShapedScreen"]) {
         BOOL isShapedScreen = iPhoneXSafeTopMargin > 0 ? YES : NO;
@@ -757,6 +769,83 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
     }];
 }
 
+- (void)downloadFileWithUrl:(NSString *)fileUrl parm:(NSDictionary *)parm {
+    [LOADING_MANAGER showCustomLoading:@"文件下载中"];
+    [KSNetworkingManager downloadFileRequestWithFileUrl:fileUrl progress:^(int64_t bytesRead, int64_t totalBytes) {
+        // 显示进度
+        NSInteger progress = (NSInteger)(bytesRead*1.0 / totalBytes) * 100;
+        NSString *tipsString = [NSString stringWithFormat:@"文件下载中\n%zd%%",progress];
+        dispatch_main_async_safe(^{
+            [LOADING_MANAGER showCustomLoading:tipsString];
+        });
+    } success:^(NSURL * _Nonnull fileUrl) {
+        [LOADING_MANAGER removeCustomLoading];
+        // 修改文件名
+        NSURL *saveUrl = fileUrl;
+        NSDictionary *content = [parm ks_dictionaryValueForKey:@"content"];
+        NSString *fileName = [content ks_stringValueForKey:@"fileName"];
+        if (![NSString isEmptyString:fileName]) { // 重命名
+            saveUrl = [self renameFile:fileUrl withNewName:fileName];
+        }
+        [self saveFileWithPath:saveUrl sendParm:parm];
+    } faliure:^(NSError * _Nonnull error) {
+        [LOADING_MANAGER removeCustomLoading];
+        [self downloadCallback:NO withParm:parm];
+    }];
+}
+
+- (void)downloadCallback:(BOOL)isSuccess withParm:(NSDictionary *)parm {
+    NSMutableDictionary *sendParm = [NSMutableDictionary dictionaryWithDictionary:parm];
+    NSMutableDictionary *content = [NSMutableDictionary dictionaryWithDictionary:[parm ks_dictionaryValueForKey:@"content"]];
+    [content setValue:@(isSuccess) forKey:@"isSuccess"];
+    [sendParm setValue:content forKey:@"content"];
+    [self postMessage:sendParm];
+}
+
+- (NSURL *)renameFile:(NSURL *)fileUrl withNewName:(NSString *)fileName {
+    // 获取文件的路径
+    NSURL *directoryURL = [fileUrl URLByDeletingLastPathComponent];
+    // 旧文件名
+    NSString *oldFileName = [fileUrl lastPathComponent];
+    NSString *fileExtension = [oldFileName pathExtension];
+    // 当前时间戳
+    NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] *1000;
+    // 构建新的文件名
+    NSString *newFileName = [NSString stringWithFormat:@"%.0f_%@.%@", interval, fileName, fileExtension];
+    // 构建新文件的完整 URL
+    NSURL *newURL = [directoryURL URLByAppendingPathComponent:newFileName];
+    
+    // 使用 NSFileManager 移动文件以重命名
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    
+    // 如果存在newURL 移除
+    if ([fileManager fileExistsAtPath:[newURL path]]) {
+        [fileManager removeItemAtURL:newURL error:nil];
+    }
+    
+    NSError *error = nil;
+    BOOL success = [fileManager moveItemAtURL:fileUrl toURL:newURL error:&error];
+    if (success) {
+        return newURL;
+    }
+    else {
+        return fileUrl;
+    }
+}
+
+- (void)saveFileWithPath:(NSURL *)fileUrl sendParm:(NSDictionary *)parm {
+    self.downloadParm = parm;
+    self.isDownloadFile = YES;
+    self.downloadFileUrl = fileUrl;
+    // 保存到文件目录下
+    KSDocumentViewController *documentPickerVC = [[KSDocumentViewController alloc] initWithURL:fileUrl inMode:UIDocumentPickerModeExportToService];
+    // 设置代理
+    documentPickerVC.delegate = self;
+    // 设置模态弹出方式
+    documentPickerVC.modalPresentationStyle = UIModalPresentationFormSheet;
+    [self.navigationController presentViewController:documentPickerVC animated:YES completion:nil];
+}
+
 - (void)downloadWithParm:(NSDictionary *)parm {
     MJWeakSelf;
     [COURSEWARE_MANAGER downloadCourseWithParm:parm callback:^(NSDictionary * _Nonnull sendParm) {
@@ -849,7 +938,9 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
 
 - (void)saveFileToPhone:(NSURL *)fileUrl {
     self.isDownloadFile = YES;
-    UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithURL:fileUrl inMode:UIDocumentPickerModeExportToService];
+    self.downloadFileUrl = fileUrl;
+    
+    KSDocumentViewController *documentPicker = [[KSDocumentViewController alloc] initWithURL:fileUrl inMode:UIDocumentPickerModeExportToService];
     documentPicker.delegate = self;
     documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
     [self presentViewController:documentPicker animated:YES completion:nil];
@@ -1585,12 +1676,146 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
 - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
     if (self.isDownloadFile) {
         self.isDownloadFile = NO;
+        if (self.downloadParm) {
+            [self downloadCallback:NO withParm:self.downloadParm];
+            self.downloadParm = nil;
+        }
     }
     else {
         [self fileChooseErrorCallback];
     }
 }
 
+- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls {
+    if (self.isDownloadFile) {
+        self.isDownloadFile = NO;
+        [self downloadCallback:YES withParm:self.downloadParm];
+        self.downloadParm = nil;
+        
+        NSURL *descUrl = [urls firstObject];
+        NSString *desc = [descUrl absoluteString];
+        KSSourceDownloadAlert *alertView = [KSSourceDownloadAlert shareInstance];
+        [alertView configWithDesc:desc];
+        [alertView actionCallbackCancel:^{
+            
+        } copyCallback:^{
+            [self copyAction:desc];
+        } sure:^{
+            [self openFileSource:descUrl];
+        }];
+        [alertView showAlert];
+    }
+    else {
+        NSURL *url =  [urls lastObject];
+        NSArray *array = [[url absoluteString] componentsSeparatedByString:@"/"];
+        NSString *fileName = [array lastObject];
+        fileName = [fileName stringByRemovingPercentEncoding];
+        
+        if (self.fileChooseType == CHOOSETYPE_XML) {
+            if (![fileName hasSuffix:@".xml"]) {
+                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传XML格式文件"];
+                [self fileChooseErrorCallback];
+                return;
+            }
+        }
+        else if (self.fileChooseType == CHOOSETYPE_MIDI) {
+            if (![fileName hasSuffix:@".mid"] || [fileName hasSuffix:@".midi"]) {
+                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传mid格式文件"];
+                [self fileChooseErrorCallback];
+                return;
+            }
+        }
+        else if (self.fileChooseType == CHOOSETYPE_MP3) {
+            if (![fileName hasSuffix:@".mp3"]) {
+                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传mp3格式文件"];
+                [self fileChooseErrorCallback];
+                return;
+            }
+        }
+        else {
+            if (![fileName hasSuffix:@".mp3"] && ![fileName hasSuffix:@".aac"]) {
+                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"暂不支持此格式!"];
+                [self fileChooseErrorCallback];
+                return;
+            }
+        }
+        
+        __block NSString * fileUrl = @"";
+        if ([KSICloudManager iCloudEnable]) {
+            [KSICloudManager downloadWithDocumentURL:url callBack:^(id obj) {
+                NSData *data = obj;
+                
+                //写入沙盒Documents
+                 NSString *pathStr = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/%@",fileName]];
+                [data writeToFile:pathStr atomically:YES];
+                
+                if (![[NSFileManager defaultManager] fileExistsAtPath:pathStr]) {
+                    BOOL isSuccess = [data writeToFile:pathStr atomically:YES];
+                    if (isSuccess) {
+                        fileUrl = pathStr;
+                    }
+                    else {
+                        [LOADING_MANAGER MBShowAUTOHidingInWindow:@"写入文件错误!"];
+                        [self fileChooseErrorCallback];
+                    }
+                }
+                else {
+                    fileUrl = pathStr;
+                }
+                [self uploadFile:fileName fileUrl:pathStr];
+                
+            }];
+        }
+        else {
+            [LOADING_MANAGER MBShowAUTOHidingInWindow:@"iCloud不可用!"];
+            [self fileChooseErrorCallback];
+        }
+    }
+}
+
+- (void)openFileSource:(NSURL *)path {
+    if (self.downloadFileUrl) {
+        [self displayFileWithUrl:path];
+    }
+    else {
+        // 开始访问权限
+        BOOL fileUrlAuthozied = [path startAccessingSecurityScopedResource];
+        
+        if (fileUrlAuthozied) {
+            // 通过 NSFileCoordinator 来协调读取
+            NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init];
+            NSError *error = nil;
+            
+            [fileCoordinator coordinateReadingItemAtURL:path options:0 error:&error byAccessor:^(NSURL *newURL) {
+                // 打开文档
+                [self displayFileWithUrl:newURL];
+            }];
+            // 释放权限
+            [path stopAccessingSecurityScopedResource];
+        } else {
+            [LOADING_MANAGER MBShowAUTOHidingInWindow:@"无法访问文件"];
+        }
+    }
+}
+
+- (void)displayFileWithUrl:(NSURL *)url {
+    // 打开文档
+    self.documentVC = [UIDocumentInteractionController interactionControllerWithURL:url];
+    // 2. 设置代理(如果需要处理特定交互事件)
+    self.documentVC.delegate = self;
+    
+    // 3. 显示“打开方式”菜单或文件预览
+    // 从指定的视图呈现操作菜单
+    [self.documentVC presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
+}
+
+- (void)copyAction:(NSString *)desc {
+    // 复制
+    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+    pasteboard.string = [NSString returnNoNullStringWithString:desc];
+    [LOADING_MANAGER MBShowAUTOHidingInWindow:@"复制成功"];
+}
+
 - (void)fileChooseErrorCallback {
     if (self.chooseFileParm) { // 回调
         [self.chooseFileParm setValue:@"" forKey:@"fileUrl"];
@@ -1599,8 +1824,6 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
     }
 }
 
-
-
 - (void)uploadFile:(NSString *)fileName fileUrl:(NSString *)fileUrl {
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
         [LOADING_MANAGER showHUD];
@@ -1682,80 +1905,6 @@ typedef NS_ENUM(NSInteger, CHOOSETYPE) {
 -(CGRect)documentInteractionControllerRectForPreview:(UIDocumentInteractionController*)controller {
     return self.view.frame;
 }
-#pragma mark - UIDocumentPickerDelegate
-- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls {
-    if (self.isDownloadFile) {
-        self.isDownloadFile = NO;
-        //保存成功
-       [LOADING_MANAGER MBShowAUTOHidingInWindow:@"保存成功"];
-    }
-    else {
-        NSURL *url =  [urls lastObject];
-        NSArray *array = [[url absoluteString] componentsSeparatedByString:@"/"];
-        NSString *fileName = [array lastObject];
-        fileName = [fileName stringByRemovingPercentEncoding];
-        
-        if (self.fileChooseType == CHOOSETYPE_XML) {
-            if (![fileName hasSuffix:@".xml"]) {
-                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传XML格式文件"];
-                [self fileChooseErrorCallback];
-                return;
-            }
-        }
-        else if (self.fileChooseType == CHOOSETYPE_MIDI) {
-            if (![fileName hasSuffix:@".mid"] || [fileName hasSuffix:@".midi"]) {
-                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传mid格式文件"];
-                [self fileChooseErrorCallback];
-                return;
-            }
-        }
-        else if (self.fileChooseType == CHOOSETYPE_MP3) {
-            if (![fileName hasSuffix:@".mp3"]) {
-                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"请上传mp3格式文件"];
-                [self fileChooseErrorCallback];
-                return;
-            }
-        }
-        else {
-            if (![fileName hasSuffix:@".mp3"] && ![fileName hasSuffix:@".aac"]) {
-                [LOADING_MANAGER MBShowAUTOHidingInWindow:@"暂不支持此格式!"];
-                [self fileChooseErrorCallback];
-                return;
-            }
-        }
-        
-        __block NSString * fileUrl = @"";
-        if ([KSICloudManager iCloudEnable]) {
-            [KSICloudManager downloadWithDocumentURL:url callBack:^(id obj) {
-                NSData *data = obj;
-                
-                //写入沙盒Documents
-                 NSString *pathStr = [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/%@",fileName]];
-                [data writeToFile:pathStr atomically:YES];
-                
-                if (![[NSFileManager defaultManager] fileExistsAtPath:pathStr]) {
-                    BOOL isSuccess = [data writeToFile:pathStr atomically:YES];
-                    if (isSuccess) {
-                        fileUrl = pathStr;
-                    }
-                    else {
-                        [LOADING_MANAGER MBShowAUTOHidingInWindow:@"写入文件错误!"];
-                        [self fileChooseErrorCallback];
-                    }
-                }
-                else {
-                    fileUrl = pathStr;
-                }
-                [self uploadFile:fileName fileUrl:pathStr];
-                
-            }];
-        }
-        else {
-            [LOADING_MANAGER MBShowAUTOHidingInWindow:@"iCloud不可用!"];
-            [self fileChooseErrorCallback];
-        }
-    }
-}
 
 #pragma mark ---- 镜像和投屏检测
 - (void)captureViewTips:(NSNotification *)notification {

+ 27 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/WebView/KSSourceDownloadAlert.h

@@ -0,0 +1,27 @@
+//
+//  KSSourceDownloadAlert.h
+//  TeacherDaya
+//
+//  Created by 王智 on 2024/9/13.
+//  Copyright © 2024 DayaMusic. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+typedef void(^DownloadSaveTipsCallback)(void);
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSSourceDownloadAlert : UIView
+
++ (instancetype)shareInstance;
+
+- (void)configWithDesc:(NSString *)desc;
+
+- (void)actionCallbackCancel:(DownloadSaveTipsCallback)cancel copyCallback:(DownloadSaveTipsCallback)copyBlock sure:(DownloadSaveTipsCallback)sure;
+
+- (void)showAlert;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 94 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/WebView/KSSourceDownloadAlert.m

@@ -0,0 +1,94 @@
+//
+//  KSSourceDownloadAlert.m
+//  TeacherDaya
+//
+//  Created by 王智 on 2024/9/13.
+//  Copyright © 2024 DayaMusic. All rights reserved.
+//
+
+#import "KSSourceDownloadAlert.h"
+
+@interface KSSourceDownloadAlert ()
+
+@property (weak, nonatomic) IBOutlet UIView *containerView;
+
+@property (weak, nonatomic) IBOutlet UILabel *descLabel;
+
+@property (nonatomic, strong) UITapGestureRecognizer *gesture;
+
+@property (nonatomic, copy) DownloadSaveTipsCallback cancelCallback;
+
+@property (nonatomic, copy) DownloadSaveTipsCallback copyBlock;
+
+@property (nonatomic, copy) DownloadSaveTipsCallback sureCallback;
+
+@end
+
+@implementation KSSourceDownloadAlert
+
++ (instancetype)shareInstance {
+    KSSourceDownloadAlert *view = [[[NSBundle mainBundle] loadNibNamed:@"KSSourceDownloadAlert" owner:nil options:nil] firstObject];
+    return view;
+}
+
+- (void)configWithDesc:(NSString *)desc {
+    if ([NSString isEmptyString:desc]) {
+        desc = @"";
+    }
+    self.descLabel.text = desc;
+}
+
+
+- (void)actionCallbackCancel:(DownloadSaveTipsCallback)cancel copyCallback:(DownloadSaveTipsCallback)copyBlock sure:(DownloadSaveTipsCallback)sure {
+    if (cancel) {
+        self.cancelCallback = cancel;
+    }
+    if (copyBlock) {
+        self.copyBlock = copyBlock;
+    }
+    if (sure) {
+        self.sureCallback = sure;
+    }
+}
+
+- (void)showAlert {
+    UIView *displayView = [NSObject getKeyWindow];
+    [displayView addSubview:self];
+    [self mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.right.top.bottom.mas_equalTo(displayView);
+    }];
+}
+
+- (IBAction)copyActipn:(id)sender {
+    if (self.copyBlock) {
+        self.copyBlock();
+    }
+}
+
+
+- (void)removeAlert {
+    [self removeFromSuperview];
+
+}
+- (IBAction)cancelAction:(id)sender {
+    [self removeAlert];
+    if (self.cancelCallback) {
+        self.cancelCallback();
+    }
+}
+
+- (IBAction)sureAction:(id)sender {
+    [self removeAlert];
+    if (self.sureCallback) {
+        self.sureCallback();
+    }
+}
+/*
+// Only override drawRect: if you perform custom drawing.
+// An empty implementation adversely affects performance during animation.
+- (void)drawRect:(CGRect)rect {
+    // Drawing code
+}
+*/
+
+@end

+ 176 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/WebView/KSSourceDownloadAlert.xib

@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="KSSourceDownloadAlert">
+            <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
+            <autoresizingMask key="autoresizingMask"/>
+            <subviews>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rhp-MD-4Jj">
+                    <rect key="frame" x="49" y="334" width="295" height="184"/>
+                    <subviews>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="下载成功" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lnB-qu-Uf7">
+                            <rect key="frame" x="20" y="15" width="65.333333333333329" height="25"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="25" id="IgN-1l-ZSs"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                            <color key="textColor" red="0.10196078431372549" green="0.10196078431372549" blue="0.10196078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="check_cancle" translatesAutoresizingMaskIntoConstraints="NO" id="Pwc-SA-BPV">
+                            <rect key="frame" x="261" y="20.666666666666686" width="14" height="14"/>
+                        </imageView>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XBP-di-P6K">
+                            <rect key="frame" x="245" y="10" width="40" height="40"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="40" id="AaQ-xy-6MC"/>
+                                <constraint firstAttribute="width" constant="40" id="sUc-rj-hAl"/>
+                            </constraints>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                        </button>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="文件已保存到以下地址" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EP8-xw-on3">
+                            <rect key="frame" x="20" y="55" width="163.33333333333334" height="26"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="26" id="fCS-pG-zdm"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                            <color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="coB-ye-PQ9">
+                            <rect key="frame" x="20" y="81" width="216" height="26"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="26" id="WCB-bj-Xft"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                            <color key="textColor" red="0.40000000000000002" green="0.40000000000000002" blue="0.40000000000000002" alpha="1" colorSpace="calibratedRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="复制" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vqA-fa-Tm9">
+                            <rect key="frame" x="241" y="81" width="34" height="26"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="26" id="SOT-u3-oZy"/>
+                                <constraint firstAttribute="width" constant="34" id="Vup-ys-5h4"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                            <color key="textColor" red="0.0039215686274509803" green="0.75686274509803919" blue="0.70980392156862748" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ddH-KT-ils">
+                            <rect key="frame" x="20" y="124" width="120" height="40"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="40" id="X1a-5l-auk"/>
+                            </constraints>
+                            <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                            <state key="normal" title="取消">
+                                <color key="titleColor" red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            </state>
+                            <userDefinedRuntimeAttributes>
+                                <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                    <real key="value" value="20"/>
+                                </userDefinedRuntimeAttribute>
+                                <userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
+                                    <real key="value" value="1"/>
+                                </userDefinedRuntimeAttribute>
+                                <userDefinedRuntimeAttribute type="color" keyPath="borderColor">
+                                    <color key="value" red="0.85882352941176465" green="0.85882352941176465" blue="0.85882352941176465" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                </userDefinedRuntimeAttribute>
+                            </userDefinedRuntimeAttributes>
+                            <connections>
+                                <action selector="cancelAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="ZSu-Gk-mpe"/>
+                            </connections>
+                        </button>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tz8-K5-sPv">
+                            <rect key="frame" x="155" y="124" width="120" height="40"/>
+                            <color key="backgroundColor" red="0.1764705882352941" green="0.7803921568627451" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            <fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                            <state key="normal" title="打开文件"/>
+                            <userDefinedRuntimeAttributes>
+                                <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                    <real key="value" value="20"/>
+                                </userDefinedRuntimeAttribute>
+                            </userDefinedRuntimeAttributes>
+                            <connections>
+                                <action selector="sureAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="8hA-Nj-c2w"/>
+                            </connections>
+                        </button>
+                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Jmn-VN-WqG">
+                            <rect key="frame" x="238" y="67" width="40" height="40"/>
+                            <constraints>
+                                <constraint firstAttribute="height" constant="40" id="5gX-ME-Jg2"/>
+                                <constraint firstAttribute="width" constant="40" id="lub-j2-xqz"/>
+                            </constraints>
+                            <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                            <connections>
+                                <action selector="copyActipn:" destination="iN0-l3-epB" eventType="touchUpInside" id="OoJ-tz-ppQ"/>
+                            </connections>
+                        </button>
+                    </subviews>
+                    <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                    <constraints>
+                        <constraint firstAttribute="width" constant="295" id="3eN-Q2-XWm"/>
+                        <constraint firstItem="tz8-K5-sPv" firstAttribute="height" secondItem="ddH-KT-ils" secondAttribute="height" id="3pM-TQ-xdm"/>
+                        <constraint firstItem="lnB-qu-Uf7" firstAttribute="top" secondItem="rhp-MD-4Jj" secondAttribute="top" constant="15" id="4GW-W9-UyJ"/>
+                        <constraint firstItem="vqA-fa-Tm9" firstAttribute="centerY" secondItem="coB-ye-PQ9" secondAttribute="centerY" id="6gf-ti-SgO"/>
+                        <constraint firstItem="vqA-fa-Tm9" firstAttribute="bottom" secondItem="Jmn-VN-WqG" secondAttribute="bottom" id="6tC-xV-Em0"/>
+                        <constraint firstItem="ddH-KT-ils" firstAttribute="leading" secondItem="rhp-MD-4Jj" secondAttribute="leading" constant="20" id="Bf6-Rk-qF4"/>
+                        <constraint firstAttribute="trailing" secondItem="vqA-fa-Tm9" secondAttribute="trailing" constant="20" id="Eqw-AM-TCV"/>
+                        <constraint firstItem="vqA-fa-Tm9" firstAttribute="centerX" secondItem="Jmn-VN-WqG" secondAttribute="centerX" id="HPp-JK-yOc"/>
+                        <constraint firstItem="tz8-K5-sPv" firstAttribute="leading" secondItem="ddH-KT-ils" secondAttribute="trailing" constant="15" id="Led-jk-T8L"/>
+                        <constraint firstItem="Pwc-SA-BPV" firstAttribute="centerY" secondItem="lnB-qu-Uf7" secondAttribute="centerY" id="Mcy-oo-gXb"/>
+                        <constraint firstAttribute="trailing" secondItem="Pwc-SA-BPV" secondAttribute="trailing" constant="20" id="Mzh-cE-g6X"/>
+                        <constraint firstItem="coB-ye-PQ9" firstAttribute="leading" secondItem="EP8-xw-on3" secondAttribute="leading" id="S8z-x4-lDg"/>
+                        <constraint firstAttribute="height" constant="184" id="VwM-cf-V34"/>
+                        <constraint firstItem="vqA-fa-Tm9" firstAttribute="leading" secondItem="coB-ye-PQ9" secondAttribute="trailing" constant="5" id="X0O-BV-Cms"/>
+                        <constraint firstItem="coB-ye-PQ9" firstAttribute="top" secondItem="EP8-xw-on3" secondAttribute="bottom" id="eeh-6H-wzh"/>
+                        <constraint firstAttribute="bottom" secondItem="ddH-KT-ils" secondAttribute="bottom" constant="20" id="eru-Yk-fPa"/>
+                        <constraint firstAttribute="trailing" secondItem="XBP-di-P6K" secondAttribute="trailing" constant="10" id="kxs-yL-w5a"/>
+                        <constraint firstAttribute="trailing" secondItem="tz8-K5-sPv" secondAttribute="trailing" constant="20" id="oWS-db-HNt"/>
+                        <constraint firstAttribute="bottom" secondItem="tz8-K5-sPv" secondAttribute="bottom" constant="20" id="pMr-bZ-bxQ"/>
+                        <constraint firstItem="XBP-di-P6K" firstAttribute="top" secondItem="rhp-MD-4Jj" secondAttribute="top" constant="10" id="smj-VX-d6T"/>
+                        <constraint firstItem="EP8-xw-on3" firstAttribute="leading" secondItem="rhp-MD-4Jj" secondAttribute="leading" constant="20" id="xcN-36-gGR"/>
+                        <constraint firstItem="tz8-K5-sPv" firstAttribute="width" secondItem="ddH-KT-ils" secondAttribute="width" id="xho-Dp-FwO"/>
+                        <constraint firstItem="EP8-xw-on3" firstAttribute="top" secondItem="lnB-qu-Uf7" secondAttribute="bottom" constant="15" id="xkt-Ro-jr6"/>
+                        <constraint firstItem="lnB-qu-Uf7" firstAttribute="leading" secondItem="rhp-MD-4Jj" secondAttribute="leading" constant="20" id="yNm-OP-7Rt"/>
+                    </constraints>
+                    <userDefinedRuntimeAttributes>
+                        <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                            <real key="value" value="20"/>
+                        </userDefinedRuntimeAttribute>
+                    </userDefinedRuntimeAttributes>
+                </view>
+            </subviews>
+            <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+            <gestureRecognizers/>
+            <constraints>
+                <constraint firstItem="rhp-MD-4Jj" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="IFT-nf-SXW"/>
+                <constraint firstItem="rhp-MD-4Jj" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="JhP-yi-vvP"/>
+            </constraints>
+            <nil key="simulatedTopBarMetrics"/>
+            <nil key="simulatedBottomBarMetrics"/>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <connections>
+                <outlet property="containerView" destination="rhp-MD-4Jj" id="CRJ-w6-5Iw"/>
+                <outlet property="descLabel" destination="coB-ye-PQ9" id="qkh-H7-5Vc"/>
+            </connections>
+            <point key="canvasLocation" x="35.877862595419849" y="19.718309859154932"/>
+        </view>
+    </objects>
+    <resources>
+        <image name="check_cancle" width="14" height="14"/>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
+    </resources>
+</document>