From e3cdb205795699a3ffbf14c19777378c81b939bd Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sun, 11 Dec 2022 21:42:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AD=BE=E5=88=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/PublicController.php | 183 ++++++--------- app/Http/Controllers/Api/SystemController.php | 222 ++++++++++++++++++ app/Http/Controllers/Api/UsersController.php | 84 +++++++ app/Http/Middleware/VerifyCsrfToken.php | 6 + app/Models/UserCheckin.php | 31 +++ app/Models/UserCheckinRecord.php | 31 +++ ...2_11_214403_create_user_checkins_table.php | 34 +++ ...4409_create_user_checkin_records_table.php | 34 +++ .../js/pages/manage/setting/checkin.vue | 113 +++++++++ .../setting/components/SystemCheckin.vue | 195 +++++++++++++++ .../assets/js/pages/manage/setting/index.vue | 3 +- .../assets/js/pages/manage/setting/system.vue | 5 + resources/assets/js/routes.js | 5 + resources/assets/sass/pages/page-setting.scss | 21 ++ 14 files changed, 859 insertions(+), 108 deletions(-) create mode 100644 app/Models/UserCheckin.php create mode 100644 app/Models/UserCheckinRecord.php create mode 100644 database/migrations/2022_12_11_214403_create_user_checkins_table.php create mode 100644 database/migrations/2022_12_11_214409_create_user_checkin_records_table.php create mode 100644 resources/assets/js/pages/manage/setting/checkin.vue create mode 100644 resources/assets/js/pages/manage/setting/components/SystemCheckin.vue diff --git a/app/Http/Controllers/Api/PublicController.php b/app/Http/Controllers/Api/PublicController.php index c942c1761..ea0e6c9ed 100755 --- a/app/Http/Controllers/Api/PublicController.php +++ b/app/Http/Controllers/Api/PublicController.php @@ -2,9 +2,10 @@ namespace App\Http\Controllers\Api; - use App\Exceptions\ApiException; use App\Models\User; +use App\Models\UserCheckin; +use App\Models\UserCheckinRecord; use App\Module\Base; use Carbon\Carbon; use Request; @@ -16,127 +17,95 @@ use Request; */ class PublicController extends AbstractController { - const appid = "10001"; - const appkey = "TWVCVBJSiCjAFOPpFVdkpQCMWDw66EUY"; - /** - * 验证签名 - * @return void + * 签到 - 路由器(openwrt)功能安装脚本 + * + * @apiParam {String} key + * + * @return string */ - private function _sign() + public function checkin__install() { - $query = Request::query(); - $query['sign'] = Request::header('sign') ?: Request::input('sign'); - // 检查必要参数 - if ($query['appid'] != self::appid) { - throw new ApiException('appid is error'); + $key = trim(Request::input('key')); + // + $setting = Base::setting('checkinSetting'); + if ($setting['wifi'] !== 'open') { + return << $v) { - if ($v != '' && $k != 'sign') { - $string .= $k . "=" . $v . "&"; - } - } - $sign = md5($string . self::appkey); - if ($sign != $query['sign']) { - throw new ApiException('sign is error'); + if ($key != $setting['key']) { + return << /etc/init.d/dootask-checkin-report </tmp/cronbak + sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak + sed -i '/^$/d' /tmp/cronbak + echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak + crontab /tmp/cronbak + rm -f /tmp/cronbak + /etc/init.d/cron enable + /etc/init.d/cron restart + + echo 'installed' + EOE; } /** - * @api {get} api/public/attendance/portraitlist 01. 【考勤】人员头像数据 + * {post} 签到 - 路由器(openwrt)上报 * - * @apiDescription 需要签名 - * @apiVersion 1.0.0 - * @apiGroup public - * @apiName attendance__portraitlist + * @apiParam {String} key + * @apiParam {String} mac 使用逗号分割多个 + * @apiParam {String} time * - * @apiParam {String} last_at 最后获取时间(格式示例:2022-01-01 12:50:01) - * - * @apiParam {String} appid 唯一身份ID,跟签名appkey配合使用 - * @apiParam {String} ver 版本号,如:1.0 - * @apiParam {Number} ts 10位数时间戳(有效时间300秒) - * @apiParam {String} nonce 随机字符串 - * @apiParam {String} sign 签名字符串=md5(query_key1=query_val1&query_key2=query_val2...&appkey) - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 + * @return string */ - public function attendance__portraitlist() + public function checkin__report() { - $this->_sign(); - // - $last_at = Request::input('last_at'); - // - $builder = User::where('userimg', '!=', '')->whereNull('disable_at'); - if (strtotime($last_at)) { - $builder->where('updated_at', '>', Carbon::parse($last_at)); - } - $list = $builder->orderBy('updated_at')->take(50)->get(); - // - $array = []; - foreach ($list as $item) { - $array[] = [ - 'userid' => $item->userid, - 'userimg' => $item->userimg, - 'updated_at' => $item->updated_at, - ]; - } - // - return Base::retSuccess('success', $array); - } - - /** - * @api {get} api/public/attendance/update 02. 【考勤】上报考勤数据 - * - * @apiDescription 需要签名 - * @apiVersion 1.0.0 - * @apiGroup public - * @apiName attendance__update - * - * @apiParam {Number} userid 会员ID - * @apiParam {Number} time 时间数据(10位数时间戳) - * - * @apiParam {String} appid 唯一身份ID,跟签名appkey配合使用 - * @apiParam {String} ver 版本号,如:1.0 - * @apiParam {Number} ts 10位数时间戳(有效时间300秒) - * @apiParam {String} nonce 随机字符串 - * @apiParam {String} sign 签名字符串=md5(query_key1=query_val1&query_key2=query_val2...&appkey) - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - */ - public function attendance__update() - { - $this->_sign(); - // - $userid = intval(Request::input('userid')); + $key = trim(Request::input('key')); + $mac = trim(Request::input('mac')); $time = intval(Request::input('time')); // - $user = User::whereUserid($userid)->first(); - if (empty($user)) { - return Base::retError('user not exist'); + $setting = Base::setting('checkinSetting'); + if ($setting['wifi'] !== 'open') { + return 'function off'; + } + if ($key != $setting['key']) { + return 'key error'; } - // todo 保存到考勤数据库 - info([ - 'userid' => $user->userid, - 'input' => Request::input(), - 'time' => $time, - 'at' => Carbon::now()->toDateTimeString(), - ]); // - return Base::retSuccess('success'); + $macs = explode(",", $mac); + foreach ($macs as $item) { + $item = strtoupper($item); + if (empty($item) || !preg_match("/^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/", $item)) { + continue; + } + $userCheckin = UserCheckin::whereMac($item)->first(); + if ($userCheckin) { + UserCheckinRecord::createInstance([ + 'userid' => $userCheckin->userid, + 'mac' => $userCheckin->mac, + 'time' => $time, + ])->save(); + } + } + return 'success'; } } diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 50ecb4168..0b84b90a3 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -4,11 +4,17 @@ namespace App\Http\Controllers\Api; use App\Models\Setting; use App\Models\User; +use App\Models\UserCheckinRecord; use App\Module\Base; +use App\Module\BillExport; +use App\Module\BillMultipleExport; use Arr; +use Carbon\Carbon; use Guanguans\Notify\Factory; use Guanguans\Notify\Messages\EmailMessage; +use Madzipper; use Request; +use Session; /** * @apiDefine system @@ -208,6 +214,57 @@ class SystemController extends AbstractController return Base::retSuccess('success', $setting ?: json_decode('{}')); } + /** + * @api {get} api/system/setting/checkin 03. 获取签到设置、保存签到设置(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName setting__checkin + * + * @apiParam {String} type + * - get: 获取(默认) + * - save: 保存设置(参数:['wifi', 'key']) + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function setting__checkin() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'save') { + if (env("SYSTEM_SETTING") == 'disabled') { + return Base::retError('当前环境禁止修改'); + } + $all = Request::input(); + foreach ($all as $key => $value) { + if (!in_array($key, [ + 'wifi', + 'key', + ])) { + unset($all[$key]); + } + } + if ($all['wifi'] === 'close') { + $all['key'] = md5(Base::generatePassword(32)); + } + $setting = Base::setting('checkinSetting', Base::newTrim($all)); + } else { + $setting = Base::setting('checkinSetting'); + } + // + if (empty($setting['key'])) { + $setting['key'] = md5(Base::generatePassword(32)); + Base::setting('checkinSetting', $setting); + } + // + $setting['wifi'] = $setting['wifi'] ?: 'close'; + $setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh"; + // + return Base::retSuccess('success', $setting ?: json_decode('{}')); + } + /** * @api {get} api/system/setting/apppush 04. 获取APP推送设置、保存APP推送设置(限管理员) * @@ -742,6 +799,171 @@ class SystemController extends AbstractController } } + /** + * @api {get} api/system/checkin/export 17. 导出签到数据(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName checkin__export + * + * @apiParam {Array} [userid] 指定会员,如:[1, 2] + * @apiParam {Array} [date] 指定日期范围,如:['2020-12-12', '2020-12-30'] + * @apiParam {Array} [time] 指定时间范围,如:['09:00', '18:00'] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__export() + { + User::auth('admin'); + // + $userid = Base::arrayRetainInt(Request::input('userid'), true); + $date = Request::input('date'); + $time = Request::input('time'); + // + if (empty($userid) || empty($date) || empty($time)) { + return Base::retError('参数错误'); + } + if (count($userid) > 20) { + return Base::retError('导出成员限制最多20个'); + } + if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) { + return Base::retError('日期选择错误'); + } + if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) { + return Base::retError('日期范围限制最大35天'); + } + if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) { + return Base::retError('时间选择错误'); + } + // + $secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00"); + $secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00"); + // + $headings = []; + $headings[] = '成员ID'; + $headings[] = '成员名称'; + $headings[] = '成员邮箱'; + $headings[] = '签到日期'; + $headings[] = '签到时间1'; + $headings[] = '签到时间2'; + // + $sheets = []; + $start = Carbon::parse($date[0])->startOfDay(); + $end = Carbon::parse($date[1])->endOfDay(); + $users = User::whereIn('userid', $userid)->take(20)->get(); + /** @var User $user */ + foreach ($users as $user) { + $records = UserCheckinRecord::whereUserid($user->userid)->whereBetween("created_at", [$start, $end])->orderBy('id')->get(); + // + $datas = []; + $styles = []; + $startT = $start->timestamp; + $endT = $end->timestamp; + $index = 1; + while ($startT < $endT) { + $index++; + $first = $records->whereBetween("created_at", [Carbon::parse($startT), Carbon::parse($startT + $secondStart)])->first(); + $last = $records->whereBetween("created_at", [Carbon::parse($startT + $secondEnd), Carbon::parse($startT + 86400)])->last(); + $first = $first ? Carbon::parse($first->created_at)->timestamp : 0; + $last = $last ? Carbon::parse($last->created_at)->timestamp : 0; + if (empty($first) || $first > $startT + $secondStart) { + $styles["E{$index}"] = [ + 'font' => [ + 'color' => [ + 'rgb' => 'ff0000' + ] + ], + ]; + } + if (empty($last) || $last < $startT + $secondEnd) { + $styles["F{$index}"] = [ + 'font' => [ + 'color' => [ + 'rgb' => 'ff0000' + ] + ], + ]; + } + $datas[] = [ + $user->userid, + $user->nickname, + $user->email, + date("Y-m-d", $startT), + $first ? date("H:i", $first) : '-', + $last ? date("H:i", $last) : '-', + ]; + $startT += 86400; + } + $sheets[] = BillExport::create()->setTitle($user->nickname)->setHeadings($headings)->setData($datas)->setStyles($styles); + } + if (empty($sheets)) { + return Base::retError('没有任何数据'); + } + // + $fileName = $users[0]->nickname; + if (count($users) > 1) { + $fileName .= "等" . count($userid) . "位成员"; + } + $fileName .= '签到记录_' . Base::time() . '.xls'; + $filePath = "temp/checkin/export/" . date("Ym", Base::time()); + $export = new BillMultipleExport($sheets); + $res = $export->store($filePath . "/" . $fileName); + if ($res != 1) { + return Base::retError('导出失败,' . $fileName . '!'); + } + $xlsPath = storage_path("app/" . $filePath . "/" . $fileName); + $zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip"; + $zipPath = storage_path($zipFile); + if (file_exists($zipPath)) { + Base::deleteDirAndFile($zipPath, true); + } + try { + Madzipper::make($zipPath)->add($xlsPath)->close(); + } catch (\Throwable) { + } + // + if (file_exists($zipPath)) { + $base64 = base64_encode(Base::array2string([ + 'file' => $zipFile, + ])); + Session::put('checkin::export:userid', $user->userid); + return Base::retSuccess('success', [ + 'size' => Base::twoFloat(filesize($zipPath) / 1024, true), + 'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)), + ]); + } else { + return Base::retError('打包失败,请稍后再试...'); + } + } + + /** + * @api {get} api/system/checkin/down 17. 下载导出的签到数据 + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName checkin__down + * + * @apiParam {String} key 通过export接口得到的下载钥匙 + * + * @apiSuccess {File} data 返回数据(直接下载文件) + */ + public function checkin__down() + { + $userid = Session::get('checkin::export:userid'); + if (empty($userid)) { + return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502); + } + // + $array = Base::string2array(base64_decode(urldecode(Request::input('key')))); + $file = $array['file']; + if (empty($file) || !file_exists(storage_path($file))) { + return Base::ajaxError("文件不存在!", [], 0, 502); + } + return response()->download(storage_path($file)); + } + /** * @api {get} api/system/version 18. 获取版本号 * diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 087508052..e3b5b8c3c 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -7,6 +7,7 @@ use App\Models\Meeting; use App\Models\Project; use App\Models\UmengAlias; use App\Models\User; +use App\Models\UserCheckin; use App\Models\UserDelete; use App\Models\UserDepartment; use App\Models\UserEmailVerification; @@ -1287,4 +1288,87 @@ class UsersController extends AbstractController // return Base::retSuccess('删除成功'); } + + /** + * @api {get} api/users/checkin/get 22. 获取签到设置 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName checkin__get + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__get() + { + $user = User::auth(); + // + $list = UserCheckin::whereUserid($user->userid)->orderBy('id')->get(); + // + return Base::retSuccess('success', $list); + } + + /** + * @api {post} api/users/checkin/save 22. 保存签到设置 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName checkin__save + * + * @apiParam {Array} list 优先级数据,格式:[{mac,remark}] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__save() + { + $user = User::auth(); + // + if (Base::settingFind('checkinSetting', 'wifi') !== 'open') { + return Base::retError('此功能未开启,请联系管理员开启'); + } + // + $list = Base::getPostValue('list'); + $array = []; + if (empty($list) || !is_array($list)) { + return Base::retError('参数错误'); + } + foreach ($list AS $item) { + $item = Base::newTrim($item); + if (empty($item['mac']) || !preg_match("/^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/", $item['mac'])) { + continue; + } + $array[] = [ + 'mac' => strtoupper($item['mac']), + 'remark' => substr($item['remark'], 0, 50), + ]; + } + if (count($array) > 3) { + return Base::retError('最多只能添加3个MAC地址'); + } + // + return AbstractModel::transaction(function() use ($array, $user) { + $ids = []; + $list = []; + foreach ($array as $item) { + $row = UserCheckin::updateInsert([ + 'userid' => $user->userid, + 'mac' => $item['mac'], + ], [ + 'remark' => $item['remark'], + ]); + if ($row) { + $ids[] = $row->id; + $list[] = $row; + } + } + UserCheckin::whereUserid($user->userid)->whereNotIn('id', $ids)->delete(); + // + return Base::retSuccess('success', $list); + }); + } } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 287d1c15b..ada79d014 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -54,6 +54,12 @@ class VerifyCsrfToken extends Middleware // 保存汇报 'api/report/store/', + // 签到设置 + 'api/users/checkin/save/', + + // 签到上报 + 'api/public/checkin/report/', + // 发布桌面端 'desktop/publish/', ]; diff --git a/app/Models/UserCheckin.php b/app/Models/UserCheckin.php new file mode 100644 index 000000000..7729ecbf0 --- /dev/null +++ b/app/Models/UserCheckin.php @@ -0,0 +1,31 @@ +bigIncrements('id'); + $table->bigInteger('userid')->nullable()->default(0)->comment('会员id'); + $table->string('mac', 100)->nullable()->default('')->comment('MAC地址'); + $table->string('remark', 100)->nullable()->default('')->comment('备注'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_checkins'); + } +} diff --git a/database/migrations/2022_12_11_214409_create_user_checkin_records_table.php b/database/migrations/2022_12_11_214409_create_user_checkin_records_table.php new file mode 100644 index 000000000..a1fcc2db4 --- /dev/null +++ b/database/migrations/2022_12_11_214409_create_user_checkin_records_table.php @@ -0,0 +1,34 @@ +bigIncrements('id'); + $table->bigInteger('userid')->nullable()->default(0)->comment('会员id'); + $table->string('mac', 100)->nullable()->default('')->comment('MAC地址'); + $table->bigInteger('time')->nullable()->default(0)->comment('上报的时间戳'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_checkin_records'); + } +} diff --git a/resources/assets/js/pages/manage/setting/checkin.vue b/resources/assets/js/pages/manage/setting/checkin.vue new file mode 100644 index 000000000..3362f01d1 --- /dev/null +++ b/resources/assets/js/pages/manage/setting/checkin.vue @@ -0,0 +1,113 @@ + + + diff --git a/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue new file mode 100644 index 000000000..9868e80d4 --- /dev/null +++ b/resources/assets/js/pages/manage/setting/components/SystemCheckin.vue @@ -0,0 +1,195 @@ + + + diff --git a/resources/assets/js/pages/manage/setting/index.vue b/resources/assets/js/pages/manage/setting/index.vue index c7a642dcc..73db84a20 100644 --- a/resources/assets/js/pages/manage/setting/index.vue +++ b/resources/assets/js/pages/manage/setting/index.vue @@ -71,6 +71,7 @@ export default { menu() { const menu = [ {path: 'personal', name: '个人设置'}, + {path: 'checkin', name: '签到设置', desc: ' (Beta)'}, {path: 'language', name: '语言设置'}, {path: 'theme', name: '主题设置'}, {path: 'password', name: '密码设置'}, @@ -114,7 +115,7 @@ export default { let name = ''; menu.some((item) => { if (routeName === `manage-setting-${item.path}`) { - name = item.name; + name = `${item.name}${item.desc||''}`; return true; } }) diff --git a/resources/assets/js/pages/manage/setting/system.vue b/resources/assets/js/pages/manage/setting/system.vue index 3ca167a4d..fc0d2293c 100644 --- a/resources/assets/js/pages/manage/setting/system.vue +++ b/resources/assets/js/pages/manage/setting/system.vue @@ -13,6 +13,9 @@ + + + @@ -30,9 +33,11 @@ import SystemColumnTemplate from "./components/SystemColumnTemplate"; import SystemEmailSetting from "./components/SystemEmailSetting"; import SystemAppPush from "./components/SystemAppPush"; import SystemMeeting from "./components/SystemMeeting"; +import SystemCheckin from "./components/SystemCheckin"; export default { components: { + SystemCheckin, SystemMeeting, SystemAppPush, SystemColumnTemplate, SystemTaskPriority, SystemSetting, SystemEmailSetting}, data() { diff --git a/resources/assets/js/routes.js b/resources/assets/js/routes.js index 59ac89d69..0461afe9a 100755 --- a/resources/assets/js/routes.js +++ b/resources/assets/js/routes.js @@ -39,6 +39,11 @@ export default [ path: 'personal', component: () => import('./pages/manage/setting/personal.vue'), }, + { + name: 'manage-setting-checkin', + path: 'checkin', + component: () => import('./pages/manage/setting/checkin.vue'), + }, { name: 'manage-setting-language', path: 'language', diff --git a/resources/assets/sass/pages/page-setting.scss b/resources/assets/sass/pages/page-setting.scss index 798a5371a..118b7faba 100755 --- a/resources/assets/sass/pages/page-setting.scss +++ b/resources/assets/sass/pages/page-setting.scss @@ -243,6 +243,13 @@ .block-setting-placeholder { height: 8px; } + .export-data { + cursor: pointer; + color: #2b85e4; + &:hover { + text-decoration: underline; + } + } } } } @@ -297,6 +304,20 @@ } } +.page-setting-checkin-export-common { + display: flex; + align-items: center; + > em { + cursor: pointer; + color: #2b85e4; + margin-left: 8px; + font-style: normal; + &:hover { + text-decoration: underline; + } + } +} + @media (max-width: 768px) { .page-setting { .setting-head {