diff --git a/services/core/include/pasteboard_dialog.h b/services/core/include/pasteboard_dialog.h index f49838f02d8b811e22476f73daafdbe3ccb5b2a8..b63fc2741a1a8b62b989db753317d449add6ae68 100644 --- a/services/core/include/pasteboard_dialog.h +++ b/services/core/include/pasteboard_dialog.h @@ -30,22 +30,32 @@ public: std::string appName{ "unknown" }; std::string deviceType{ "unknown" }; }; + struct ToastMessageInfo { + std::string fromAppName{ "unknown" }; + std::string toAppName{ "unknown" }; + }; static constexpr uint32_t POPUP_INTERVAL = 1; // seconds static constexpr uint32_t MAX_LIFE_TIME = 300; // seconds + static constexpr uint32_t SHOW_TOAST_TIME = 2000; // milliseconds static constexpr const char *DEFAULT_LABEL = "unknown"; using Cancel = std::function; static PasteBoardDialog &GetInstance(); int32_t ShowDialog(const MessageInfo &message, const Cancel &cancel); + int32_t ShowToast(const ToastMessageInfo &message); void CancelDialog(); + void CancelToast(); private: static sptr GetAbilityManagerService(); static constexpr const char *PASTEBOARD_DIALOG_APP = "cn.openharmony.pasteboarddialog"; static constexpr const char *PASTEBOARD_DIALOG_ABILITY = "DialogExtensionAbility"; + static constexpr const char *PASTEBOARD_TOAST_ABILITY = "ToastExtensionAbility"; std::mutex connectionLock_; + std::mutex toastConnectionLock_; sptr connection_; + sptr toastConnection_; }; } // namespace OHOS::MiscServices #endif // PASTEBOARD_INTERFACES_KITS_NAPI_SRC_PASTE_BOARD_DAILOG_H diff --git a/services/core/include/pasteboard_service.h b/services/core/include/pasteboard_service.h index f5a479980616187383220104d8ecf8ea2cf48347..9b016506cafb2c5fd41d60fe378eaf17fa919441 100644 --- a/services/core/include/pasteboard_service.h +++ b/services/core/include/pasteboard_service.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Huawei Device Co., Ltd. + * Copyright (C) 2021-2023 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -127,18 +127,22 @@ private: static bool IsDefaultIME(const AppInfo &appInfo); static bool IsFocusedApp(int32_t tokenId); static void SetLocalPasteFlag(bool isCrossPaste, uint32_t tokenId, PasteData &pasteData); + void ShowHintToast(bool isValid, uint32_t tokenId, const std::shared_ptr &pasteData); + void OnAddSystemAbility(int32_t systemAbilityId, const std::string &deviceId) override; void DevManagerInit(); void DevProfileInit(); ServiceRunningState state_; std::shared_ptr serviceHandler_; std::mutex clipMutex_; + std::mutex hintMutex_; std::mutex observerMutex_; ObserverMap observerChangedMap_; ObserverMap observerEventMap_; ClipPlugin::GlobalEvent currentEvent_; const std::string filePath_ = ""; std::map> clips_; + std::map> hints_; std::recursive_mutex mutex; std::shared_ptr clipPlugin_ = nullptr; diff --git a/services/core/src/pasteboard_dialog.cpp b/services/core/src/pasteboard_dialog.cpp index 5d0f143413bc561aa806a2a1a8f402ca227687ca..8806b37166c925c98cd3be05ef9129bef2a31498 100644 --- a/services/core/src/pasteboard_dialog.cpp +++ b/services/core/src/pasteboard_dialog.cpp @@ -86,6 +86,32 @@ int32_t PasteBoardDialog::ShowDialog(const MessageInfo &message, const Cancel &c return 0; } +int32_t PasteBoardDialog::ShowToast(const ToastMessageInfo &message) +{ + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "begin, fromApp:%{public}s, toApp:%{public}s", + message.fromAppName.c_str(), message.toAppName.c_str()); + auto abilityManager = GetAbilityManagerService(); + if (abilityManager == nullptr) { + PASTEBOARD_HILOGE(PASTEBOARD_MODULE_SERVICE, "get ability manager failed"); + return -1; + } + Want want; + want.SetAction(""); + want.SetElementName(PASTEBOARD_DIALOG_APP, PASTEBOARD_TOAST_ABILITY); + want.SetParam("fromAppName", message.fromAppName); + want.SetParam("toAppName", message.toAppName); + + std::lock_guard lock(toastConnectionLock_); + toastConnection_ = new DialogConnection(nullptr); + int32_t result = IN_PROCESS_CALL(abilityManager->ConnectAbility(want, toastConnection_, nullptr)); + if (result != 0) { + PASTEBOARD_HILOGE(PASTEBOARD_MODULE_SERVICE, "start pasteboard toast failed, result:%{public}d", result); + return -1; + } + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "start pasteboard toast success."); + return 0; +} + void PasteBoardDialog::CancelDialog() { PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "begin"); @@ -99,6 +125,19 @@ void PasteBoardDialog::CancelDialog() PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "disconnect dialog ability:%{public}d", result); } +void PasteBoardDialog::CancelToast() +{ + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "begin"); + auto abilityManager = GetAbilityManagerService(); + if (abilityManager == nullptr) { + PASTEBOARD_HILOGE(PASTEBOARD_MODULE_SERVICE, "get ability manager failed"); + return; + } + std::lock_guard lock(toastConnectionLock_); + int result = IN_PROCESS_CALL(abilityManager->DisconnectAbility(toastConnection_)); + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "disconnect toast ability:%{public}d", result); +} + sptr PasteBoardDialog::GetAbilityManagerService() { PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "begin"); diff --git a/services/core/src/pasteboard_service.cpp b/services/core/src/pasteboard_service.cpp index d90770888420d8114eee0864fa66421242372e00..a20996893b733d5f4a6026c3c976e2033aac4d67 100644 --- a/services/core/src/pasteboard_service.cpp +++ b/services/core/src/pasteboard_service.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Huawei Device Co., Ltd. + * Copyright (C) 2021-2023 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -206,6 +206,11 @@ void PasteboardService::Clear() std::string bundleName = GetAppBundleName(IPCSkeleton::GetCallingTokenID()); NotifyObservers(bundleName, PasteboardEventStatus::PASTEBOARD_CLEAR); } + std::lock_guard lck(hintMutex_); + auto hintItem = hints_.find(userId); + if (hintItem != hints_.end()) { + hints_.erase(hintItem); + } CleanDistributedData(userId); } @@ -360,9 +365,46 @@ int32_t PasteboardService::GetPasteData(PasteData &data) std::string bundleName = GetAppBundleName(tokenId); NotifyObservers(bundleName, PasteboardEventStatus::PASTEBOARD_READ); GetPasteDataDot(data, pop, tokenId); + ShowHintToast(result, tokenId, std::make_shared(data)); return result ? static_cast(PasteboardError::E_OK) : static_cast(PasteboardError::E_ERROR); } +void PasteboardService::ShowHintToast(bool isValid, uint32_t tokenId, const std::shared_ptr &pasteData) +{ + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "show hint toast start"); + if (!isValid || pasteData == nullptr) { + PASTEBOARD_HILOGE(PASTEBOARD_MODULE_SERVICE, "data is invalid"); + return; + } + auto dataTokenId = pasteData->GetTokenId(); + if (IsDefaultIME(GetAppInfo(tokenId)) || dataTokenId == tokenId || pasteData->IsRemote()) { + PASTEBOARD_HILOGI(PASTEBOARD_MODULE_SERVICE, "not need show hint toast"); + return; + } + auto userId = GetCurrentAccountId(); + std::lock_guard lck(hintMutex_); + auto hintItem = hints_.find(userId); + if (hintItem != hints_.end()) { + auto hintTokenId = std::find(hintItem->second.begin(), hintItem->second.end(), tokenId); + if (hintTokenId != hintItem->second.end()) { + return; + } + } + hints_[userId].emplace_back(tokenId); + + PasteBoardDialog::ToastMessageInfo message; + message.fromAppName = GetAppLabel(dataTokenId); + message.toAppName = GetAppLabel(tokenId); + PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "toast should show, fromName=%{public}s, toName = %{public}s", + message.fromAppName.c_str(), message.toAppName.c_str()); + std::thread thread([this, message]() mutable { + PasteBoardDialog::GetInstance().ShowToast(message); + std::this_thread::sleep_for(std::chrono::milliseconds(PasteBoardDialog::SHOW_TOAST_TIME)); + PasteBoardDialog::GetInstance().CancelToast(); + }); + thread.detach(); +} + bool PasteboardService::GetPasteData(PasteData &data, uint32_t tokenId, bool isFocusedApp) { PasteboardTrace tracer("GetPasteData inner"); @@ -452,6 +494,11 @@ int32_t PasteboardService::SetPasteData(PasteData &pasteData) SetDistributedData(appInfo.userId, pasteData); NotifyObservers(appInfo.bundleName, PasteboardEventStatus::PASTEBOARD_WRITE); SetPasteDataDot(pasteData); + std::lock_guard lck(hintMutex_); + auto hintItem = hints_.find(appInfo.userId); + if (hintItem != hints_.end()) { + hints_.erase(hintItem); + } setting_.store(false); PASTEBOARD_HILOGD(PASTEBOARD_MODULE_SERVICE, "Clips length %{public}d.", static_cast(clips_.size())); return static_cast(PasteboardError::E_OK); diff --git a/services/dialog/PasteboardDialog/entry/src/main/ets/DialogExtensionAbility/DialogExtensionAbility.ts b/services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/DialogExtensionAbility.ts similarity index 92% rename from services/dialog/PasteboardDialog/entry/src/main/ets/DialogExtensionAbility/DialogExtensionAbility.ts rename to services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/DialogExtensionAbility.ts index 5371ae472669ca8e2be8e92b103ea5cababb6924..f4b733d19152f553429c594d9e968036fc722449 100644 --- a/services/dialog/PasteboardDialog/entry/src/main/ets/DialogExtensionAbility/DialogExtensionAbility.ts +++ b/services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/DialogExtensionAbility.ts @@ -33,7 +33,6 @@ class DialogStub extends rpc.RemoteObject { constructor(des: string) { super(des); } - onConnect(code, data, reply, option): void {} } export default class DialogExtensionAbility extends ServiceExtensionAbility { @@ -65,11 +64,16 @@ export default class DialogExtensionAbility extends ServiceExtensionAbility { return new DialogStub('PasteboardDialog'); } - onReconnect(want: Want): void { - hilog.info(0, TAG, 'onReconnect'); + onRequest(want: Want, startId:number): void { + hilog.info(0, TAG, 'onRequest'); this.onConnect(want); } + onDisconnet(): void { + hilog.info(0, TAG, 'onDisconnet'); + this.onDestroy(); + } + onDestroy(): void { hilog.info(0, TAG, 'onDestroy'); globalThis.extensionWin.destroyWindow(); @@ -106,7 +110,6 @@ export default class DialogExtensionAbility extends ServiceExtensionAbility { } windowClass.moveWindowTo(rect.left, rect.top); windowClass.resize(rect.width, rect.height); - windowClass.loadContent('pages/index'); windowClass.setBackgroundColor('#00000000'); windowClass.showWindow(); globalThis.windowNum++; diff --git a/services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/ToastExtensionAbility.ts b/services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/ToastExtensionAbility.ts new file mode 100644 index 0000000000000000000000000000000000000000..75626090519875e9b4b5b817cb40bf8e20ec92f8 --- /dev/null +++ b/services/dialog/PasteboardDialog/entry/src/main/ets/ServiceExtAbility/ToastExtensionAbility.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import rpc from '@ohos.rpc'; +import hilog from '@ohos.hilog'; +import window from '@ohos.window'; +import display from '@ohos.display'; +import ServiceExtensionAbility from '@ohos.app.ability.ServiceExtensionAbility'; +import type Want from '@ohos.application.Want'; + +interface IRect { + left: number; + top: number; + width: number; + height: number; +} + +const DISTANCE_NUMBER = 68; +const TAG = 'ToastExtensionAbility'; + +class ToastStub extends rpc.RemoteObject { + constructor(des: string) { + super(des); + } +} + +export default class ToastExtensionAbility extends ServiceExtensionAbility { + onCreate(want: Want): void { + hilog.info(0, TAG, 'onCreate'); + globalThis.context = this.context; + } + + onConnect(want: Want): ToastStub { + hilog.info(0, TAG, 'onConnect'); + display + .getDefaultDisplay() + .then((display: display.Display) => { + const toastRect = { + left: 0, + top: 0, + width: display.width, + height: display.height, + }; + globalThis.toastInfo = { + fromAppName: want.parameters.fromAppName, + toAppName: want.parameters.toAppName, + displayHeight:display.height / display.densityPixels - DISTANCE_NUMBER, + }; + this.createToastWindow('PasteboardToast' + new Date().getTime(), toastRect); + }) + .catch((err) => { + hilog.info(0, TAG, 'getDefaultDisplay err: ' + JSON.stringify(err)); + }); + return new ToastStub('PasteboardToast'); + } + + onRequest(want: Want, startId:number): void { + hilog.info(0, TAG, 'onRequest'); + this.onConnect(want); + } + + onDisconnet(): void { + hilog.info(0, TAG, 'onDisconnet'); + this.onDestroy(); + } + + onDestroy(): void { + hilog.info(0, TAG, 'onDestroy'); + globalThis.extensionWin.destroyWindow(); + globalThis.context.terminateSelf(); + } + + private async createToastWindow(name: string, rect: IRect): Promise { + hilog.info(0, TAG, 'create toast begin'); + + if (globalThis.windowNum > 0) { + globalThis.windowNum = 0; + this.onDestroy(); + } + let windowClass = null; + let config = { + name, + windowType: window.WindowType.TYPE_FLOAT, + ctx: this.context, + }; + try { + window.createWindow(config, (err, data) => { + if (err.code) { + hilog.error(0, TAG, 'Failed to create the window. Cause: ' + JSON.stringify(err)); + return; + } + windowClass = data; + globalThis.extensionWin = data; + hilog.info(0, TAG, 'Succeeded in creating the window. Data: ' + JSON.stringify(data)); + try { + windowClass.setUIContent('pages/toastIndex', (err) => { + if (err.code) { + hilog.error(0, TAG, 'Failed to load the content. Cause:' + JSON.stringify(err)); + return; + } + windowClass.moveWindowTo(rect.left, rect.top); + windowClass.resize(rect.width, rect.height); + windowClass.setBackgroundColor('#00000000'); + windowClass.setWindowTouchable(false); + windowClass.showWindow(); + globalThis.windowNum++; + hilog.info(0, TAG, 'Create window successfully'); + }); + } catch (exception) { + hilog.error(0, TAG, 'Failed to load the content. Cause:' + JSON.stringify(exception)); + } + }); + } catch (exception) { + hilog.error(0, TAG, 'Failed to create the window. Cause: ' + JSON.stringify(exception)); + } + } +} diff --git a/services/dialog/PasteboardDialog/entry/src/main/ets/pages/toastIndex.ets b/services/dialog/PasteboardDialog/entry/src/main/ets/pages/toastIndex.ets new file mode 100644 index 0000000000000000000000000000000000000000..e4eab3b4deed6a1feb35aa60fe2f19023af07b18 --- /dev/null +++ b/services/dialog/PasteboardDialog/entry/src/main/ets/pages/toastIndex.ets @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Entry +@Component +struct Index { + @State fromAppName: string = 'Unknown' + @State toAppName: string = 'Unknown' + @State toastHeight: number = 0 + + async aboutToAppear() { + this.fromAppName = globalThis.toastInfo.fromAppName + this.toAppName = globalThis.toastInfo.toAppName + this.toastHeight = globalThis.toastInfo.displayHeight + } + + build() { + Row() { + Column() { + Text() { + Span(this.toAppName).fontSize(14) + .fontWeight(FontWeight.Regular).fontColor($r('app.color.white')).fontFamily($r('app.string.typeface')) + Span($r('app.string.read')).fontSize(14) + .fontWeight(FontWeight.Regular).fontColor($r('app.color.white')).fontFamily($r('app.string.typeface')) + Span(this.fromAppName).fontSize(14) + .fontWeight(FontWeight.Regular).fontColor($r('app.color.white')).fontFamily($r('app.string.typeface')) + Span($r('app.string.info')).fontSize(14) + .fontWeight(FontWeight.Regular).fontColor($r('app.color.white')).fontFamily($r('app.string.typeface')) + } + .height(19) + .opacity(0.6) + .margin({ left: 16, right: 16, top:8, bottom:8 }) + } + .height(36) + .backgroundColor($r('app.color.grey')) + .borderRadius(18) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + } + .alignItems(VerticalAlign.Bottom) + .justifyContent(FlexAlign.Center) + .height(this.toastHeight) + .width('100%') + } +} \ No newline at end of file diff --git a/services/dialog/PasteboardDialog/entry/src/main/module.json b/services/dialog/PasteboardDialog/entry/src/main/module.json index dd79c276309cc2bbfe8cca107cf1abba7323c4c8..bb89b3d30151f383181fc4ebb2277cf69e1f7552 100644 --- a/services/dialog/PasteboardDialog/entry/src/main/module.json +++ b/services/dialog/PasteboardDialog/entry/src/main/module.json @@ -16,10 +16,19 @@ "extensionAbilities": [ { "name": "DialogExtensionAbility", - "srcEntrance": "./ets/DialogExtensionAbility/DialogExtensionAbility.ts", - "description": "$string:ExtensionAbility_desc", + "srcEntrance": "./ets/ServiceExtAbility/DialogExtensionAbility.ts", + "description": "$string:DialogExtensionAbility_desc", "icon": "$media:icon", - "label": "$string:ExtensionAbility_label", + "label": "$string:DialogExtensionAbility_label", + "visible": false, + "type": "service" + }, + { + "name": "ToastExtensionAbility", + "srcEntrance": "./ets/ServiceExtAbility/ToastExtensionAbility.ts", + "description": "$string:ToastExtensionAbility_desc", + "icon": "$media:icon", + "label":"$string:ToastExtensionAbility_label", "visible": false, "type": "service" } diff --git a/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/color.json b/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/color.json index 62a137a61b90c14f109ed8c81d9d551ea0a5888a..de73cea3f114075a49f352db08b814b15890cb17 100644 --- a/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/color.json +++ b/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/color.json @@ -3,6 +3,10 @@ { "name": "white", "value": "#FFFFFF" + }, + { + "name": "grey", + "value": "#4D4D4D" } ] } \ No newline at end of file diff --git a/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/string.json b/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/string.json index d4a04bcff24dab86a9669ee6e0a07480610d4328..61159bdce60ef30df0bfe4b307d7dde28b26c4a3 100644 --- a/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/string.json +++ b/services/dialog/PasteboardDialog/entry/src/main/resources/base/element/string.json @@ -6,16 +6,30 @@ }, { "name": "ExtensionAbility_desc", + "name": "DialogExtensionAbility_desc", "value": "Pasteboard dialog extension" }, { "name": "ExtensionAbility_label", + "name": "DialogExtensionAbility_label", "value": "Pasteboard Dialog" }, + { + "name": "ToastExtensionAbility_desc", + "value": "Pasteboard toast extension" + }, + { + "name": "ToastExtensionAbility_label", + "value": "Pasteboard Toast" + }, { "name": "title", "value": "pasting to" }, + { + "name": "typeface", + "value": "HarmonyHeiTi" + }, { "name": "from", "value": "from" @@ -27,6 +41,14 @@ { "name": "cancel", "value": "canceled" + }, + { + "name": "read", + "value": "has read" + }, + { + "name": "info", + "value": "pasteboard information" } ] } \ No newline at end of file diff --git a/services/dialog/PasteboardDialog/entry/src/main/resources/base/profile/main_pages.json b/services/dialog/PasteboardDialog/entry/src/main/resources/base/profile/main_pages.json index feec276e105eeb8d621c20aaf838f318b0a94150..c8bea33300f6e19d6cffbcc1c250729af12b98f9 100644 --- a/services/dialog/PasteboardDialog/entry/src/main/resources/base/profile/main_pages.json +++ b/services/dialog/PasteboardDialog/entry/src/main/resources/base/profile/main_pages.json @@ -1,5 +1,6 @@ { "src": [ - "pages/index" + "pages/index", + "pages/toastIndex" ] } diff --git a/services/dialog/PasteboardDialog/entry/src/main/resources/zh/element/string.json b/services/dialog/PasteboardDialog/entry/src/main/resources/zh/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..0395a5dc680fbef773b9826f5b355034356660ea --- /dev/null +++ b/services/dialog/PasteboardDialog/entry/src/main/resources/zh/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "entry_desc", + "value": "Pasteboard dialog" + }, + { + "name": "DialogExtensionAbility_desc", + "value": "Pasteboard dialog extension" + }, + { + "name": "DialogExtensionAbility_label", + "value": "Pasteboard Dialog" + }, + { + "name": "ToastExtensionAbility_desc", + "value": "Pasteboard toast extension" + }, + { + "name": "ToastExtensionAbility_label", + "value": "Pasteboard Toast" + }, + { + "name": "typeface", + "value": "HarmonyHeiTi" + }, + { + "name": "title", + "value": "正在粘贴到" + }, + { + "name": "from", + "value": "从" + }, + { + "name": "done", + "value": "已粘贴到" + }, + { + "name": "cancel", + "value": "取消" + }, + { + "name": "read", + "value": "已读取" + }, + { + "name": "info", + "value": "剪贴板信息" + } + ] +} \ No newline at end of file