diff --git a/.obsidian/app.json b/.obsidian/app.json index 9e26dfe..0281356 100644 --- a/.obsidian/app.json +++ b/.obsidian/app.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "vimMode": true +} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json index 9e26dfe..a8b1e37 100644 --- a/.obsidian/appearance.json +++ b/.obsidian/appearance.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "theme": "moonstone" +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 3702067..d6c4df8 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -176,7 +176,7 @@ } ], "direction": "horizontal", - "width": 300 + "width": 326.390625 }, "left-ribbon": { "hiddenItems": { @@ -191,6 +191,29 @@ }, "active": "9775ada2a6c4ad3e", "lastOpenFiles": [ + "HarmonyOS 启动性能监控接入方案_20260805.md", + "CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.c1c356d07c03", + "CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.e9cfc48c9755", + "CrashDiagnosticsDemo/entry/build/default/outputs/default/entry-default-signed.hap", + "CrashDiagnosticsDemo/entry/build/default/intermediates/default/temp/signConfigCheckJson.json", + "CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/modules.abc", + "CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-CrashHistory.ets-3362d7b55dd443f6915a35dcdf55398d52857eca3d1670c4e3aca017d6197ce1.msgpack", + "CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/sourceMaps.map", + "CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.e796abd1053c", + "CrashDiagnosticsDemo/entry/src/main/ets/pages/CrashHistory.ets.tmp.64840.d67860e3655a", + "CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/CrashHistory.ets-3362d7b55dd443f6915a35dcdf55398d52857eca3d1670c4e3aca017d6197ce1.msgpack", + "CrashDiagnosticsDemo/CLAUDE.md", + "CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/background.png", + "CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/startIcon.png", + "CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/foreground.png", + "CrashDiagnosticsDemo/entry/src/main/resources/base/media/startIcon.png", + "CrashDiagnosticsDemo/entry/src/main/resources/base/media/foreground.png", + "CrashDiagnosticsDemo/entry/src/main/resources/base/media/background.png", + "CrashDiagnosticsDemo/AppScope/resources/base/media/foreground.png", + "CrashDiagnosticsDemo/AppScope/resources/base/media/background.png", + "report-assets/10-home-first-frame.png", + "report-assets/09-profiler-first-frame-time-check.jpg", + "AGENTS.md", "未命名.md", "HarmonyOS APM分析报告_20260731.md" ] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..825f671 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +Mixed-purpose repository with three distinct areas: + +1. **Root level** — Obsidian-friendly documentation workspace for HarmonyOS APM (Application Performance Management) crash analysis of 同花顺期货 (THS Futures). The main report is `HarmonyOS APM分析报告_20260731.md` with supporting screenshots in `report-assets/`. +2. **`CrashDiagnosticsDemo/`** — Standalone HarmonyOS demo app demonstrating HiAppEvent crash subscription, persistent crash records, and native crash simulation via NAPI. Full project with its own `CLAUDE.md`. +3. **`future_entry_demo/`** — Extracted `entry` HAP module from THS Futures. NOT a full workspace — depends on sibling modules (`biz_common`, `biz_trade`, etc.) via `file:../...`. Has its own `CLAUDE.md` and `AGENTS.md`. + +**When working in a subdirectory**, read that subdirectory's own `CLAUDE.md` first — it contains build commands, architecture details, and constraints specific to that project. This file covers the repo-level conventions and cross-cutting concerns. + +## Build & Development Commands + +There is no top-level build system. Work in the appropriate subdirectory: + +- **CrashDiagnosticsDemo**: Full HarmonyOS project — `devecocli build` from its root. +- **future_entry_demo**: Entry module only — build from the parent workspace with `devecocli build --modules entry@default`. +- **Root documentation**: No build step. Edit Markdown in Obsidian or any CommonMark viewer. + +The `.claude/settings.local.json` pre-approves `devecocli build *`, `devecocli device *`, and `devecocli run *` commands. + +## Cross-Cutting Conventions + +### HarmonyOS / ArkTS + +All HarmonyOS projects in this repo use the **Hvigor** build system with **DevEco Studio** as the IDE. Use the **deveco-cli skill** for build, run, and device operations. ArkTS is a strict subset of TypeScript — load the **arkts-grammar-standards** skill before writing `.ets` files. + +### Documentation (Markdown) + +- UTF-8 Markdown with ATX headings (`#`, `##`), fenced code blocks with language tags, pipe tables with right-aligned numeric columns. +- Name screenshots with a two-digit sequence and short lowercase description (e.g., `08-grafana-native-detail.png`). +- New reports use descriptive filenames with a date (`YYYYMMDD`). Place images in `report-assets/` or a report-specific subdirectory. +- Validate manually: preview all headings/tables/images, recalculate totals against source screenshots, verify no absolute paths or sensitive data leak in. + +### Git + +- Concise, imperative commit subjects (e.g., `docs: clarify OOM crash accounting`, `assets: add native crash evidence`). +- Keep report text and supporting screenshots in the same commit. +- Avoid committing personal Obsidian workspace-state changes unless intentionally shared. diff --git a/CrashDiagnosticsDemo/.gitignore b/CrashDiagnosticsDemo/.gitignore new file mode 100644 index 0000000..d2ff201 --- /dev/null +++ b/CrashDiagnosticsDemo/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/CrashDiagnosticsDemo/CLAUDE.md b/CrashDiagnosticsDemo/CLAUDE.md new file mode 100644 index 0000000..41c4c15 --- /dev/null +++ b/CrashDiagnosticsDemo/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +HarmonyOS (鸿蒙) crash diagnostics demo app — `com.example.crashdiagnosticsdemo`. Targets HarmonyOS 6.1.0 (API 23), Stage Mode, phone devices only. Uses the Hvigor build system with DevEco Studio as the IDE. + +## Build & Development Commands + +This is a HarmonyOS project; use the **deveco-cli skill** for all build, run, and device operations. + +- **Build (debug)**: `devecocli build` or via DevEco Studio +- **Run on device/emulator**: DevEco Studio → Run, or `devecocli run` +- **Lint ArkTS**: `code-linter` is configured via `code-linter.json5` (triggers on `.ets` files, enforces security rules like `@security/no-unsafe-aes`, `@security/no-unsafe-hash`, etc.) +- **CLI docs lookup**: use the `deveco-cli` skill to fetch HarmonyOS API docs and samples + +### Testing + +The project includes `@ohos/hypium` (test framework) and `@ohos/hamock` (mocking) as dev dependencies. Test targets are configured in `entry/build-profile.json5` under the `ohosTest` target. + +## Architecture + +### Entry Point & Page Routing + +``` +EntryAbility (entryability/EntryAbility.ets) + └── loads pages/Index (main page) + └── navigates to pages/CrashHistory (push route) +``` + +- `EntryAbility` initializes `CrashDiagnostics` on `onCreate`, sets color mode, and loads `pages/Index` on `onWindowStageCreate`. +- Page routes are declared in `entry/src/main/resources/base/profile/main_pages.json`. + +### Core Diagnostic Layer — `CrashDiagnostics` (diagnostics/CrashDiagnostics.ets) + +A **static utility class** (never instantiated) that is the backbone of the app. Key responsibilities: + +1. **HiAppEvent subscription**: Registers an `hiAppEvent.Watcher` named `debugCrashWatcher` that listens for system `APP_CRASH` fault events. The watcher is created in `initialize()` — because it reuses the same name, it picks up un-consumed crash events from a previous process exit on the next launch. + +2. **Persistent crash records**: Stores up to 10 crash record strings in `preferences` (key: `recent_hiappevent_crash_records`). New records are prepended; duplicates are filtered out. + +3. **Dismiss-until-new-crash pattern**: Tracks which crash the user dismissed (key: `dismissed_hiappevent_crash_id`). `shouldShowLastCrashDialog()` returns `true` only when the current latest crash differs from the dismissed one. + +4. **Listener callback**: `recordsUpdatedListener` — a single callback set by the active UI page; fires when cached records change. + +5. **Caught error logging**: `recordCaughtError()` logs to hilog only — it does NOT mix caught exceptions into the APP_CRASH history. The history exclusively contains system-generated crash events. + +### Pages + +- **`pages/Index`** — Main debug screen. Displays crash record count/status, buttons to simulate a caught error (logged only) and a native CPP crash (via NAPI, kills the app), and a button to view crash history. On `aboutToAppear`, sets itself as the `recordsUpdatedListener` and optionally shows an alert dialog for the last un-dismissed crash. + +- **`pages/CrashHistory`** — Lists crash records from `CrashDiagnostics.getRecentRecords()`, with detail view, clear-all, and export-to-ZIP (JSON inside ZIP, saved via `DocumentViewPicker`). + +### Native Layer (NAPI) + +``` +entry/src/main/cpp/napi_init.cpp → libentry.so +entry/src/main/cpp/types/libentry/index.d.ts → ArkTS type declarations +``` + +Exports a single function `simulateCppCrash()` that calls `std::abort()`. Used by `pages/Index` to demonstrate a native crash that gets picked up by HiAppEvent on the next launch. Built via CMake (`CMakeLists.txt`) targeting `arm64-v8a` only, linked against `libace_napi.z.so`. + +### Key Data Flow + +``` +App crashes (native/JS) + → HarmonyOS writes APP_CRASH event to HiAppEvent + → Next app launch: EntryAbility.onCreate → CrashDiagnostics.initialize() + → hiAppEvent.addWatcher("debugCrashWatcher") picks up events + → collectFromHolder() formats and caches records to preferences + → recordsUpdatedListener fires → Index page updates UI + → Optionally shows AlertDialog for un-dismissed crash +``` + +## Important Constraints + +- **ArkTS is NOT TypeScript**: ArkTS is a strict subset of TypeScript with its own compiler rules. Use the `arkts-grammar-standards` skill before writing `.ets` files. Key differences: no `any`/`unknown` types in certain contexts, restricted structural typing for `@Component` structs, specific decorator rules (`@State`, `@Entry`, `@Component`). +- **Target ABI is arm64-v8a only** (no x86_64 emulator support in the build config). +- **Obfuscation is disabled** even in release mode (`entry/build-profile.json5`). +- **Strict mode enabled**: `caseSensitiveCheck: true`, `useNormalizedOHMUrl: true`. diff --git a/CrashDiagnosticsDemo/build-profile.json5 b/CrashDiagnosticsDemo/build-profile.json5 new file mode 100644 index 0000000..d8d96fd --- /dev/null +++ b/CrashDiagnosticsDemo/build-profile.json5 @@ -0,0 +1,56 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001AE4AAFBA570B8D487B590836743677A433F6DD14A7A6D0B26155D832D115352AC4E27D625EDE1", + "profile": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.p12", + "storePassword": "0000001A5B9B9B6E48CF67A085861EF0102D3E508E9C395FA0B293A616EFBD3790576635D0A3852EA471" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "6.1.0(23)", + "compatibleSdkVersion": "6.1.0(23)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Pasted image 20260803162753.png b/Pasted image 20260803162753.png new file mode 100644 index 0000000..3208741 Binary files /dev/null and b/Pasted image 20260803162753.png differ diff --git a/Pasted image 20260803162800.png b/Pasted image 20260803162800.png new file mode 100644 index 0000000..d9bb3be Binary files /dev/null and b/Pasted image 20260803162800.png differ diff --git a/android_APM_自定义事件接入文档.pdf b/android_APM_自定义事件接入文档.pdf new file mode 100644 index 0000000..de2b0a8 Binary files /dev/null and b/android_APM_自定义事件接入文档.pdf differ diff --git a/future_entry_demo/.gitignore b/future_entry_demo/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/future_entry_demo/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/future_entry_demo/.hvigor/outputs/build-logs/build.log b/future_entry_demo/.hvigor/outputs/build-logs/build.log new file mode 100644 index 0000000..e69de29 diff --git a/future_entry_demo/AGENTS.md b/future_entry_demo/AGENTS.md new file mode 100644 index 0000000..38300cc --- /dev/null +++ b/future_entry_demo/AGENTS.md @@ -0,0 +1,31 @@ +# `entry` HAP Module + +## Scope and layout + +- This checkout is the HarmonyOS stage-model `entry` HAP module, not the complete application workspace. +- Production ArkTS: `src/main/ets/`. Existing areas include `pages/`, `widget/`, `entryability/`, `communication/`, `initialize/`, and `register/`. +- Resources: `src/main/resources/`. +- Local Hypium tests: `src/test/`; device/ability Hypium tests: `src/ohosTest/`. +- Module metadata: `src/main/module.json5`; build targets/configuration: `build-profile.json5`. +- `hvigorfile.ts` registers the HAP tasks and HMRouter plugin. + +## Workspace and commands + +`oh-package.json5` uses sibling `file:../...` dependencies, and imports resolve across those siblings. Invoke builds and runs only from the complete parent workspace; its exact root is not verified in this checkout. + +```bash +# Parent-workspace commands +devecocli build --modules entry@default +devecocli build --modules entry@ohosTest +devecocli build --modules entry@Official --build-mode release +devecocli device list +devecocli run --module entry +``` + +- Configured targets are `default`, `ohosTest`, `OEM`, `Official`, and `forTest`. +- Device/ability tests require a connected device or emulator. +- No test-runner command is verified here. + +## Generated files + +- `oh-package-lock.json5` is autogenerated; do not edit it manually. diff --git a/future_entry_demo/CLAUDE.md b/future_entry_demo/CLAUDE.md new file mode 100644 index 0000000..c2938da --- /dev/null +++ b/future_entry_demo/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project identity + +This is the HarmonyOS stage-model `entry` HAP module for **同花顺期货 (THS Futures)**, a financial futures trading app. It is NOT a complete workspace — sibling modules (`biz_common`, `biz_trade`, `biz_quote`, etc.) live alongside this directory and are consumed via `file:../...` dependencies. Always invoke builds and runs from the parent workspace, not this directory. + +## Build and run commands + +```bash +# Build specific targets (run from parent workspace) +devecocli build --modules entry@default +devecocli build --modules entry@ohosTest +devecocli build --modules entry@Official --build-mode release + +# List connected devices/emulators +devecocli device list + +# Run on device +devecocli run --module entry +``` + +Configured targets: `default`, `ohosTest`, `OEM`, `Official`, `forTest`. + +- `oh-package-lock.json5` is autogenerated — never edit it. +- Device/ability tests (`src/ohosTest/`) require a connected device or emulator. + +## Architecture overview + +### App lifecycle and initialization chain + +`EntryAbility` (`src/main/ets/entryability/EntryAbility.ets`) is the main `UIAbility` (singleton, launch type `singleton`). Its `onCreate` runs a carefully ordered initialization sequence: + +1. Push message listener (trade daily notifications via `pushService.receiveMessage`) +2. Custom figure registration (`FigureRegistry.registerAll()`) +3. Global context initialization (multiple `ContextUtils` variants for different modules, plus `GlobalContext`, `AppUtil`) +4. Security monitor, gray test init, cloud store log init +5. HMRouter init +6. Analytics/ELK upload, UGC client, auth listener, event subscription +7. RCP HTTP header configuration +8. Future market init chain: `TabConfigManager` → `CommonIndicatorsManage` → code table → `MainContractManager` → `HQTabManager` → `FutureMarketDataManager` → `InactiveContractManager` → `DelayMarketManager` → `TradeInitializor` → `CurveStorage` +9. Ads, AI group client, font loading, want caching + +`onWindowStageCreate` then branches on three conditions: +- **No network** → `loadNetworkAnomalyPage` +- **Privacy not accepted** → `loadPrivacyPage` (LauncherPage) +- **Normal** → `loadIndexPage` (the main Index page) + +### Navigation and page registry + +All pages must be registered in `src/main/resources/base/profile/main_pages.json`. The app uses **HMRouter** (`@hadss/hmrouter`) for navigation. The main UI uses a `Tabs` component in `Index.ets` with configurable tab sets (`IndexTabManager` via `TabManager`). On large screens (`lg` breakpoint), tabs render vertically on the left. + +### Communication layer + +`src/main/ets/communication/` manages long-lived TCP socket connections to the trading backend: + +- **`ConnectionInitiator`** — initializes site configuration (IP/port from `Config.ets`), supports inner-network debug mode, and handles debug-tool server overrides. +- **`ServiceHandlerImpl`** — registers two connection configurations: + - `HqConnectionConfig` (行情): authenticates via `HXAuthManager`, pipes data through `AuthJsonStructObserver`, `RealDataStructObserver`, `SidInstanceIdReceiver` + - `CBASConnectionConfig` (analytics): separate connection for CBAS analytics with trade ELK log observer +- **`HXSocketMonitor`** — monitors socket health across app foreground/background transitions. + +### Group subscription system (real-time sync) + +`GroupSubManager` (from `base_mobilepush`) manages push-based data synchronization. Handlers in `src/main/ets/groupsub/business/` handle specific data domains: + +- **User-scoped** (`addUserGroupHandler`): self-code lists, YYB account lists, trade settings, indicators, draw lines, AI group, SID data, function authority +- **All-user-scoped** (`addAllGroupHandler`): ad strategies, gray test, account analysis, futures companies, stock/variety storage updates, main contract changes + +Each handler receives push notifications and drives local state updates. + +### Service registration pattern + +Services in `src/main/ets/register/` follow a singleton-then-init pattern: +- **`cbasService`** (CbasServiceRegister) — analytics/event capture with `HxCBASAgentProvider` +- **`UgcClient`** — user-generated content client +- **`ElkUploadRegister`** — error log upload +- **`SecurityMonitorInit`** — crash protection + +### Tabs and page structure + +The main UI has four tabs (configurable, some may be hidden): +1. **首页** (FirstPage) — `@b2c/first_page` +2. **行情** (HQPage) — `biz_market`, renders self-codes, futures quotes, option chains +3. **交易** (TradeBiz) — `biz_trade`, conditionally hidden via `HexinVersionControl.hideTrade()` +4. **发现** (DiscoverPage) — `@b2c/biz_discover` + +Each tab has its own `TabManager` sub-manager. Tab visibility is controlled by `IndexTabManager` which extends `TabManager`. + +### Multi-device adaptation + +The module targets phone, tablet, and 2in1. Responsive breakpoints are handled through `updateBreakpoint()` (from `biz_common`) which broadcasts breakpoint changes (`sm`, `md`, `lg`). Window size changes are monitored in `EntryAbility.onWindowStageCreate`. + +### Key sibling dependencies + +All resolved via `file:../...` in `oh-package.json5`: +- `biz_common` — shared utilities, constants, tab management, preferences +- `biz_trade` — trading functionality, account management, notifications +- `biz_quote` — quote/chart rendering, indicators, settings +- `biz_market` — market data, HQ tabs, contracts +- `biz_option`, `biz_forecast`, `biz_indicator` — option/forecast/indicator features +- `service_push`, `service_watchlist`, `service_aigroup` — push, watchlist, AI services +- `biz_debug` — debug tools, server switching +- `@kernel/lib_communication` — TCP socket connection library +- `@kernel/theme_manager` — theming and color mode +- `@hadss/hmrouter` — route management +- `@ohos/hypium` — test framework + +### Debug infrastructure + +In non-release builds, a `DebugFloatingBall` overlay is created via `windowStage.createSubWindow()`. Debug pages live under `pages/debug/` and support server switching, gray test configuration, cloud storage inspection, URL modification, and memory leak monitoring. diff --git a/future_entry_demo/build-profile.json5 b/future_entry_demo/build-profile.json5 new file mode 100644 index 0000000..73161ae --- /dev/null +++ b/future_entry_demo/build-profile.json5 @@ -0,0 +1,40 @@ +{ + "apiType": "stageMode", + "buildOption": { + "arkOptions": { + // "apPath": "./modules.ap" /* Profile used for profile-guided optimization (PGO), a compiler optimization technique to improve app runtime performance. */ + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": true, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + },{ + "name": "OEM", + "output": {"artifactName": "OEM"} + },{ + "name": "Official", + "output": {"artifactName": "futures-Official"} + },{ + "name": "forTest", + "output": {"artifactName": "futures-forTest"} + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/hmrouter-obfuscation-rules.txt b/future_entry_demo/hmrouter-obfuscation-rules.txt new file mode 100644 index 0000000..8f76f14 --- /dev/null +++ b/future_entry_demo/hmrouter-obfuscation-rules.txt @@ -0,0 +1,3 @@ +-keep-file-name +-keep-property-name +-keep-global-name \ No newline at end of file diff --git a/future_entry_demo/hvigorfile.ts b/future_entry_demo/hvigorfile.ts new file mode 100644 index 0000000..ee70e9e --- /dev/null +++ b/future_entry_demo/hvigorfile.ts @@ -0,0 +1,7 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; +import { hapPlugin } from '@hadss/hmrouter-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[hapPlugin()] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/future_entry_demo/obfuscation-rules.txt b/future_entry_demo/obfuscation-rules.txt new file mode 100644 index 0000000..985b2ae --- /dev/null +++ b/future_entry_demo/obfuscation-rules.txt @@ -0,0 +1,18 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope \ No newline at end of file diff --git a/future_entry_demo/oh-package.json5 b/future_entry_demo/oh-package.json5 new file mode 100644 index 0000000..f61770b --- /dev/null +++ b/future_entry_demo/oh-package.json5 @@ -0,0 +1,25 @@ +{ + "license": "", + "devDependencies": {}, + "author": "", + "name": "entry", + "description": "Please describe the basic information.", + "main": "", + "version": "1.0.0", + "dynamicDependencies": {}, + "dependencies": { + "@ohos/hypium": "1.0.18", + "@kernel/theme_manager": "1.0.2-beta.17", + "biz_common": "file:../biz_common", + "biz_forecast": "file:../biz_forecast", + "biz_option": "file:../biz_option", + "biz_indicator": "file:../biz_indicator", + "@b2c-f/billparsemodule": "1.0.0", + "@b2c-f/accountanalysis": "1.3.0", + "biz_debug": "file:../biz_debug", + "biz_tradecondition": "file:../biz_tradecondition", + "service_aigroup": "file:../service_aigroup", + "@b2c-f/lib_fuhm_theme_res": "1.0.9-alpha.2", + "@kernel/ohos_leak_watcher": "1.0.0-beta.8" + } +} diff --git a/future_entry_demo/src/main/ets/DeviceInfoHolder.ets b/future_entry_demo/src/main/ets/DeviceInfoHolder.ets new file mode 100644 index 0000000..4c0672a --- /dev/null +++ b/future_entry_demo/src/main/ets/DeviceInfoHolder.ets @@ -0,0 +1,21 @@ +import identifier from '@ohos.identifier.oaid'; +import hilog from '@ohos.hilog'; +import { BusinessError } from '@ohos.base'; + +export class DeviceInfoHolder { + + init() { + try { + identifier.getOAID().then((data) => { + const oaid: string = data; + hilog.info(0x0000, 'mhy', '%{public}s', `get oaid by promise success, oaid: ${oaid}`); + }).catch((err: BusinessError) => { + hilog.info(0x0000, 'mhy', '%{public}s', `get oaid by promise failed, code: ${err.code}, message: ${err.message}`); + }) + + } catch(err) { + let e: BusinessError = err as BusinessError; + console.error('createDeviceManager errCode:' + e.code + ',errMessage:' + e.message); + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyConvertManager.ets b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyConvertManager.ets new file mode 100644 index 0000000..a411e6e --- /dev/null +++ b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyConvertManager.ets @@ -0,0 +1,301 @@ +/** + * 交易日志转换管理器 + * 负责将推送数据库中的数据转换到交易日志数据库 + */ + +import { TradeDailyPushDataManager } from './TradeDailyPushDataManager'; +import { TradeDailyPushModel } from './TradeDailyPushModel'; +import { TradeDailyDataManager, TradeDailyModel, TradeMsgUniqueIdManager } from 'biz_trade'; +import { PushType } from 'biz_trade/src/main/ets/service/push/PushType'; +import { AppUtil } from '@pura/harmony-utils'; +import { HXLog } from 'biz_common'; +import { CodeTableManager } from 'lib_codetablemanager'; +import { AbilityConstant, ApplicationStateChangeCallback, common, Context } from '@kit.AbilityKit'; + +const TAG = 'TradeDailyConvertManager'; + +/** + * 解析结果接口 + */ +interface ParseResult { + dailyTitle: string; + dailyContent: string; + dailyTime: string; +} + +export class TradeDailyConvertManager { + private static instance: TradeDailyConvertManager; + + private applicationStateChangeCallBack: ApplicationStateChangeCallback = { + onApplicationForeground: (): void => { + this.convertDailyFromPush(); + }, + onApplicationBackground: (): void => { + } + } + + private constructor() { + } + + /** + * 获取单例实例 + */ + static getInstance(): TradeDailyConvertManager { + if (!TradeDailyConvertManager.instance) { + TradeDailyConvertManager.instance = new TradeDailyConvertManager(); + } + return TradeDailyConvertManager.instance; + } + + async registerForeground(context: Context): Promise { + context.getApplicationContext().on('applicationStateChange', this.applicationStateChangeCallBack) + await this.convertDailyFromPush(); + } + + /** + * 应用进入前台时转换推送数据 + * 将推送数据库中的数据转换到交易日志数据库 + */ + async convertDailyFromPush(): Promise { + try { + HXLog.i(TAG, '开始转换推送数据到交易日志'); + + // 查询所有推送日志 + const pushDailys = await TradeDailyPushDataManager.getInstance().queryDaily(); + + if (pushDailys.length === 0) { + HXLog.i(TAG, '没有需要转换的推送数据'); + return; + } + + HXLog.i(TAG, `查询到 ${pushDailys.length} 条推送数据`); + + // 转换每条推送日志 + for (const pushDaily of pushDailys) { + // 过滤放弃行权的推送(单号不变,无法去重) + if (this.shouldSkipPushDaily(pushDaily)) { + HXLog.i(TAG, `跳过放弃行权推送: ${pushDaily.body}`); + continue; + } + + await this.convertPushDailyToTradeDaily(pushDaily); + } + + // 删除已转换的推送日志 + await TradeDailyPushDataManager.getInstance().deleteDaily(pushDailys); + HXLog.i(TAG, '推送数据转换完成'); + } catch (error) { + HXLog.e(TAG, `转换推送数据失败: ${error}`); + } + } + + /** + * 判断是否应该跳过该推送日志 + * @param pushDaily 推送日志 + */ + private shouldSkipPushDaily(pushDaily: TradeDailyPushModel): boolean { + const type = pushDaily.type + + // 放弃行权push不记录(单号不变,无法去重) + const abandonExercise = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_abandon_exercise')); + if ((type == PushType.TY_OPTION_EXEC_WEITUO || + type == PushType.TY_OPTION_EXEC_CHEDAN) && + pushDaily.body.includes(abandonExercise)) { + return true; + } + + return false; + } + + /** + * 将推送日志转换为交易日志 + * @param pushDaily 推送日志 + */ + private async convertPushDailyToTradeDaily(pushDaily: TradeDailyPushModel): Promise { + try { + const isSame = await TradeMsgUniqueIdManager.getInstance().isSameIdentify( + pushDaily.time, + pushDaily.iconType, + pushDaily.type, + pushDaily.orderNo + ); + + if (isSame) { + HXLog.i(TAG, `推送已存在,跳过: ${pushDaily.tradeDate} ${pushDaily.time}`); + return; + } + + const body = pushDaily.body; + const title = pushDaily.title; + + // 解析推送内容 + const parseResult = await this.parsePushContent(body, title, pushDaily); + const dailyTitle = parseResult.dailyTitle; + const dailyContent = parseResult.dailyContent; + const dailyTime = parseResult.dailyTime; + + // 创建交易日志模型 + const dailyModel = TradeDailyModel.createWithDateTime( + pushDaily.tradeDate, + dailyTime, + dailyTitle, + dailyContent + ); + + // 插入到所有匹配的账户表中 + await this.insertToMatchingAccounts(dailyModel, pushDaily.tradeNo, pushDaily.qsid); + + HXLog.i(TAG, `转换推送日志成功: ${pushDaily.tradeDate} ${dailyTime} ${dailyTitle}`); + } catch (error) { + HXLog.e(TAG, `转换推送日志失败: ${error}`); + } + } + + /** + * 解析推送内容 + * @param body 推送内容 + * @param title 推送标题 + * @param pushDaily 推送日志 + */ + private async parsePushContent(body: string, title: string, pushDaily: TradeDailyPushModel): Promise { + let dailyTitle = ''; + let dailyContent = ''; + let dailyTime = pushDaily.tradeTime; + + const iconType = pushDaily.iconType; + const orderNo = this.getOrderNo(pushDaily); + const orderNoPrefix = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_order_no_prefix_with_comma')); + const transactionReport = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_transaction_report')); + + // 错误类型处理(iconType == 3 表示错误) + if (iconType == '3') { + dailyContent = body; + dailyTime = pushDaily.tradeTime; + + // 提取时间 [15:01:30] + const firstIndex = body.indexOf('['); + const lastIndex = body.indexOf(']'); + if (firstIndex > -1 && lastIndex > -1) { + const timeStr = body.substring(firstIndex + 1, lastIndex); + dailyTime = timeStr.replace(/:/g, ''); + // 从 body 末��减去时间部分(包括括号) + dailyContent = body.substring(0, body.length - timeStr.length - 4); + } + + if (orderNo.length > 0) { + dailyContent += orderNoPrefix + orderNo; + } + } else { + // 正常类型处理 + const firstIndex = body.indexOf('('); + const lastIndex = body.indexOf(')'); + + if (firstIndex > -1 && lastIndex > -1) { + // 标题处理 + if (!title.includes(transactionReport)) { + dailyTitle += title; + } + dailyTitle += body.substring(0, firstIndex); + + // 内容处理 + const alertContent = body.substring(firstIndex + 1, lastIndex); + const alertContentArr = alertContent.split(','); + + if (alertContentArr.length > 0) { + const tradeCode = alertContentArr[0]; + const stockInfo = await CodeTableManager.get().getStockInfoByTradeCode(tradeCode); + const stockName = stockInfo?.stockName; + + if (stockName && stockName.length > 0) { + // 将合约代码替换为合约名 + dailyContent += stockName; + for (let i = 1; i < alertContentArr.length; i++) { + dailyContent += ',' + alertContentArr[i]; + } + } else { + dailyContent = alertContent; + } + } else { + dailyContent = alertContent; + } + + if (orderNo.length > 0) { + dailyContent += orderNoPrefix + orderNo; + } + + // 时间处理 + // iOS: timeFromBody = body.substring(loc: last + 2, len: body.count - last - 3) + dailyTime = pushDaily.tradeTime; + if (dailyTime.length === 0 && lastIndex + 2 < body.length && body.length - lastIndex - 3 > 0) { + const startPos = lastIndex + 2; + const length = body.length - lastIndex - 3; + dailyTime = body.substring(startPos, startPos + length); + } + } + } + + // 移除时间中的冒号 + dailyTime = dailyTime.replace(/:/g, ''); + + return { dailyTitle, dailyContent, dailyTime }; + } + + /** + * 获取订单号 + * @param pushDaily 推送日志 + */ + private getOrderNo(pushDaily: TradeDailyPushModel): string { + const type = pushDaily.type; + let orderNo = ''; + + // 期货成交2,期权成交8,需要用orderSysId + if (type == PushType.TY_JIAOYI_CHENGGONG || type == PushType.TY_OPTION_CHEGNJIAO) { + orderNo = pushDaily.orderSysId || pushDaily.orderNo; + } + // 条件单触发,preOrderNo有值则是附加条件单,用母单编号 + else if (type == PushType.TY_CONDITION && pushDaily.preOrderNo) { + orderNo = pushDaily.preOrderNo; + } + else { + orderNo = pushDaily.orderNo; + } + + // 移除空格 + return orderNo.replace(/\s/g, ''); + } + + /** + * 插入到所有匹配的账户表中 + * @param model 交易日志模型 + * @param accountId 账户ID + * @param qsId 券商ID + */ + private async insertToMatchingAccounts( + model: TradeDailyModel, + accountId: string, + qsId: string + ): Promise { + try { + // 获取所有表名 + const tableNames = await TradeDailyDataManager.getInstance().getAllTableNames(); + + // 匹配 trade_daily_{accountId}_{qsId}_{wtId} 格式 + const prefix = `trade_daily_${accountId}_${qsId}_`; + + for (const tableName of tableNames) { + if (tableName.startsWith(prefix)) { + const wtId = tableName.substring(prefix.length); + await TradeDailyDataManager.getInstance().insertDailyToAccount( + model, + accountId, + qsId, + wtId + ); + HXLog.i(TAG, `插入到账户表: ${tableName}`); + } + } + } catch (error) { + HXLog.e(TAG, `插入到匹配账户失败: ${error}`); + } + } +} diff --git a/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushDataManager.ets b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushDataManager.ets new file mode 100644 index 0000000..e15a987 --- /dev/null +++ b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushDataManager.ets @@ -0,0 +1,282 @@ +/** + * 交易推送日志数据管理器 + */ + +import relationalStore from '@ohos.data.relationalStore'; +import { TradeDailyPushModel } from './TradeDailyPushModel'; +import { BusinessError } from '@ohos.base'; +import hilog from '@ohos.hilog'; + +const DB_NAME = 'trade_push_daily.db'; +const TAG = 'TradeDailyPushDataManager' +const DOMAIN = 0xFF01 +const FORMAT = '%{public}s' + +export class TradeDailyPushDataManager { + private static instance: TradeDailyPushDataManager; + private rdbStore: relationalStore.RdbStore | null = null; + private isInitialized: boolean = false; + + private constructor() { + } + + /** + * 获取单例实例 + */ + static getInstance(): TradeDailyPushDataManager { + if (!TradeDailyPushDataManager.instance) { + TradeDailyPushDataManager.instance = new TradeDailyPushDataManager(); + } + return TradeDailyPushDataManager.instance; + } + + /** + * 初始化数据库 + */ + async initDB(context: ESObject): Promise { + if (this.isInitialized && this.rdbStore) { + return; + } + + try { + const config: relationalStore.StoreConfig = { + name: DB_NAME, + securityLevel: relationalStore.SecurityLevel.S1, + customDir: 'trade' + }; + + this.rdbStore = await relationalStore.getRdbStore(context, config); + await this.createTable(); + this.isInitialized = true; + } catch (err) { + const error = err as BusinessError; + hilog.error(DOMAIN, TAG, FORMAT, `初始化数据库失败: ${error.code}, ${error.message}`); + } + } + + /** + * 创建表 + */ + private async createTable(): Promise { + if (!this.rdbStore) { + return; + } + + try { + const sql = TradeDailyPushModel.getTableSQL(); + await this.rdbStore.executeSql(sql); + } catch (err) { + const error = err as BusinessError; + hilog.error(DOMAIN, TAG, FORMAT, `创建表失败: ${error.code}, ${error.message}`); + } + } + + /** + * 插入数据 + */ + async insertDaily(daily: TradeDailyPushModel): Promise { + if (!this.rdbStore) { + hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化'); + return; + } + + try { + const valueBucket = daily.toDbRecord(); + await this.rdbStore.insert(TradeDailyPushModel.getTableName(), valueBucket); + hilog.info(DOMAIN, TAG, FORMAT, '插入数据成功') + } catch (err) { + const error = err as BusinessError; + hilog.error(DOMAIN, TAG, FORMAT, `插入数据失败: ${error.code}, ${error.message}`); + } + } + + /** + * 删除数据 + */ + async deleteDaily(models: TradeDailyPushModel[]): Promise { + if (!this.rdbStore) { + hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化'); + return; + } + + try { + const tableName = TradeDailyPushModel.getTableName(); + + for (const model of models) { + if (!model.uniqueId) { + continue; + } + + const predicates = new relationalStore.RdbPredicates(tableName); + predicates.equalTo('uniqueId', model.uniqueId); + await this.rdbStore.delete(predicates); + } + } catch (err) { + const error = err as BusinessError; + hilog.error(DOMAIN, TAG, FORMAT, `删除数据失败: ${error.code}, ${error.message}`); + } + } + + /** + * 查询所有数据 + */ + async queryDaily(): Promise { + if (!this.rdbStore) { + hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化'); + return []; + } + + let resultSet: relationalStore.ResultSet | null = null; + try { + const tableName = TradeDailyPushModel.getTableName(); + const predicates = new relationalStore.RdbPredicates(tableName); + resultSet = await this.rdbStore.query(predicates, TradeDailyPushModel.getColumns()); + + const models: TradeDailyPushModel[] = []; + + if (resultSet.rowCount === 0) { + return models; + } + + resultSet.goToFirstRow(); + do { + const record: relationalStore.ValuesBucket = {}; + const columns = TradeDailyPushModel.getColumns(); + + for (const column of columns) { + const columnIndex = resultSet.getColumnIndex(column); + if (columnIndex >= 0) { + record[column] = resultSet.getString(columnIndex); + } + } + + const model = TradeDailyPushModel.fromDbRecord(record); + models.push(model); + } while (resultSet.goToNextRow()); + + return models; + } catch (err) { + const error = err as BusinessError; + hilog.error(DOMAIN, TAG, FORMAT, `查询数据失败: ${error.code}, ${error.message}`); + return []; + } finally { + resultSet?.close(); + } + } + + /** + * 将 pushInfo 转换为 TradeDailyPushModel + * @param pushInfo 推送信息 + * @returns 转换后的模型,若转换失败则返回 null + */ + convertToTradeDailyPushModel(pushInfo: ESObject): TradeDailyPushModel | null { + const body = pushInfo['DESC'] as string; + const msg = pushInfo['APPMSG'] as Map; + + if (!msg) { + return null; + } + + const tradeNo = msg['tradeno'] as string; + const qsid = msg['qsid'] as string; + + if (!tradeNo || !qsid) { + return null; + } + + const model = new TradeDailyPushModel(); + model.body = body ?? ''; + model.tradeNo = tradeNo; + model.qsid = qsid; + + const orderNo = msg['orderNumber'] as string; + if (orderNo) { + model.orderNo = orderNo; + } + + // 期货成交2,期权成交8,需要用orderSysId + const orderSysId = msg['orderSysId'] as string; + if (orderSysId) { + model.orderSysId = orderSysId; + } + + // 条件单触发, preOrderNo有值则是附加条件单,用母单编号 + const preOrderNo = msg['preOrderNumber'] as string; + if (preOrderNo) { + model.preOrderNo = preOrderNo; + } + + const uniqueid = msg['uniqueid'] as string; + if (uniqueid) { + model.uniqueId = uniqueid; + } + + const tradeTime = msg['ST2'] as string; + if (tradeTime) { + model.tradeTime = tradeTime; + model.time = tradeTime; + } + + // 日期用收到通知的日期 + const now = new Date(); + model.tradeDate = this.formatDate(now, 'yyyyMMdd'); + + const iconType = msg['IC'] as string; + if (iconType) { + model.iconType = iconType; + } + + const type = msg['TY'] as string; + if (type) { + model.type = type; + // 期货撤单3、期权撤单9、期权行权撤单52,这几种后端给的委托时间,不对,需要用收到回报的时间 + const typeNum = parseInt(type); + if (typeNum === 3 || typeNum === 9 || typeNum === 52) { + model.tradeTime = this.formatDate(now, 'HH:mm:ss'); + // 推送中的已撤单给过滤了,和标题重复,产品不想要 + model.body = body.replace(',已撤单', ''); + } + } + + const title = msg['ST'] as string; + if (title) { + model.title = title; + } + + return model; + } + + /** + * 记录推送日志 + */ + async recordPushDaily(model: TradeDailyPushModel): Promise { + hilog.info(DOMAIN, TAG, FORMAT, `[daily] notification-extension push daily: ${model.tradeDate} ${model.time} ${model.title} ${model.body}`); + + const type = parseInt(model.type); + // 过滤条件单(5)和盈亏(6)类型 + if (type === 5 || type === 6) { + return; + } + + await this.insertDaily(model); + } + + /** + * 格式化日期 + */ + private formatDate(date: Date, format: string): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + if (format === 'yyyyMMdd') { + return `${year}${month}${day}`; + } else if (format === 'HH:mm:ss') { + return `${hours}:${minutes}:${seconds}`; + } + return ''; + } +} diff --git a/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushModel.ets b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushModel.ets new file mode 100644 index 0000000..c064736 --- /dev/null +++ b/future_entry_demo/src/main/ets/NotificationExtension/TradeDaily/TradeDailyPushModel.ets @@ -0,0 +1,132 @@ +/** + * 交易推送日报数据模型 + * Created by wenzhe on 2022/6/6. + * Copyright © 2022 hexin. All rights reserved. + */ + +import relationalStore from '@ohos.data.relationalStore'; + +export class TradeDailyPushModel { + /** 交易账号 */ + tradeNo: string = ''; + /** qsId */ + qsid: string = ''; + /** 订单号 */ + orderNo: string = ''; + /** 订单号(已成交) */ + orderSysId: string = ''; + /** 母单号(附加条件单) */ + preOrderNo: string = ''; + /** 消息唯一号 */ + uniqueId: string = ''; + /** 交易类型 */ + type: string = ''; + /** 交易时间(app显示) */ + tradeTime: string = ''; + /** 交易时间(后端推的时间) */ + time: string = ''; + /** 交易日期 */ + tradeDate: string = ''; + /** 标题 */ + title: string = ''; + /** 消息内容 */ + body: string = ''; + /** icon类型 */ + iconType: string = ''; + + constructor() { + } + + /** + * 获取表名 + */ + static getTableName(): string { + return 'trade_push_daily'; + } + + /** + * 获取表结构SQL + */ + static getTableSQL(): string { + return `CREATE TABLE IF NOT EXISTS ${TradeDailyPushModel.getTableName()} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + body TEXT, + tradeNo TEXT, + qsid TEXT, + orderNo TEXT, + orderSysId TEXT, + preOrderNo TEXT, + uniqueId TEXT, + type TEXT, + tradeTime TEXT, + time TEXT, + tradeDate TEXT, + title TEXT, + iconType TEXT + )`; + } + + /** + * 获取所有列名 + */ + static getColumns(): string[] { + return [ + 'id', + 'body', + 'tradeNo', + 'qsid', + 'orderNo', + 'orderSysId', + 'preOrderNo', + 'uniqueId', + 'type', + 'tradeTime', + 'time', + 'tradeDate', + 'title', + 'iconType' + ]; + } + + /** + * 从数据库记录转换为模型对象 + */ + static fromDbRecord(record: relationalStore.ValuesBucket): TradeDailyPushModel { + const model = new TradeDailyPushModel(); + model.body = (record['body'] as string) ?? ''; + model.tradeNo = (record['tradeNo'] as string) ?? ''; + model.qsid = (record['qsid'] as string) ?? ''; + model.orderNo = (record['orderNo'] as string) ?? ''; + model.orderSysId = (record['orderSysId'] as string) ?? ''; + model.preOrderNo = (record['preOrderNo'] as string) ?? ''; + model.uniqueId = (record['uniqueId'] as string) ?? ''; + model.type = (record['type'] as string) ?? ''; + model.tradeTime = (record['tradeTime'] as string) ?? ''; + model.time = (record['time'] as string) ?? ''; + model.tradeDate = (record['tradeDate'] as string) ?? ''; + model.title = (record['title'] as string) ?? ''; + model.iconType = (record['iconType'] as string) ?? ''; + return model; + } + + /** + * 转换为数据库记录 + */ + toDbRecord(): relationalStore.ValuesBucket { + const record: relationalStore.ValuesBucket = {}; + record['body'] = this.body; + record['tradeNo'] = this.tradeNo; + record['qsid'] = this.qsid; + record['orderNo'] = this.orderNo; + record['orderSysId'] = this.orderSysId; + record['preOrderNo'] = this.preOrderNo; + record['uniqueId'] = this.uniqueId; + record['type'] = this.type; + record['tradeTime'] = this.tradeTime; + record['time'] = this.time; + record['tradeDate'] = this.tradeDate; + record['title'] = this.title; + record['iconType'] = this.iconType; + return record; + } +} diff --git a/future_entry_demo/src/main/ets/PrivacyWithDraw.ets b/future_entry_demo/src/main/ets/PrivacyWithDraw.ets new file mode 100644 index 0000000..423ca90 --- /dev/null +++ b/future_entry_demo/src/main/ets/PrivacyWithDraw.ets @@ -0,0 +1,95 @@ +import { HXLog, PreferenceService } from 'biz_common'; +import { JSBaseEvent } from 'biz_webview'; +import { common } from '@kit.AbilityKit'; + +/** + * 撤回同意隐私政策协议 + */ +export class PrivacyWithDrawEvent extends JSBaseEvent { + + getName(): string { + return "privacyWithdraw"; + } + + getHandler(): Function { + return (data: object, handler: Function) => { + //TODO这里提了ir单确认能否改成自定义弹窗,否则UI效果不统一 + AlertDialog.show( + { + title: '撤回同意将退出同花顺,我们无法再为您提供服务', + //subtitle: 'subtitle', + message: '', + autoCancel: true, + alignment: DialogAlignment.Center, + gridCount: 4, + showInSubWindow: true, + isModal: true, + offset: { dx: 0, dy: -20 }, + buttonDirection: DialogButtonDirection.HORIZONTAL, + buttons: [ + { + value: '撤回并退出', + action: () => { + PreferenceService.putVal('Privacy', 'isUserAgree', false, true); + (getContext(this) as common.UIAbilityContext).terminateSelf(() => {}); + } + }, + { + value: '继续使用', + action: () => { + + } + }, + ], + cancel: () => { + console.info('Closed callbacks') + } + } + ) + } + } + + dialogController: CustomDialogController = new CustomDialogController({ + builder: CustomDialogExample({ + cancel: ()=> { this.onCancel() }, + confirm: ()=> { this.onAccept() }, + }), + }) + + onCancel() { + console.info('Callback when the first button is clicked') + } + + onAccept() { + console.info('Callback when the second button is clicked') + } +} + +@CustomDialog +struct CustomDialogExample { + cancel?: () => void + confirm?: () => void + controller: CustomDialogController + + build() { + Column() { + Text('我是内容').fontSize(20).margin({ top: 10, bottom: 10 }) + Flex({ justifyContent: FlexAlign.SpaceAround }) { + Button('cancel') + .onClick(() => { + this.controller.close() + if (this.cancel) { + this.cancel() + } + }).backgroundColor(0xffffff).fontColor(Color.Black) + Button('confirm') + .onClick(() => { + this.controller.close() + if (this.confirm) { + this.confirm() + } + }).backgroundColor(0xffffff).fontColor(Color.Red) + }.margin({ bottom: 10 }) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/backupExtension/BackupExtension.ets b/future_entry_demo/src/main/ets/backupExtension/BackupExtension.ets new file mode 100644 index 0000000..59bae15 --- /dev/null +++ b/future_entry_demo/src/main/ets/backupExtension/BackupExtension.ets @@ -0,0 +1,15 @@ +import BackupExtensionAbility, {BundleVersion} from '@ohos.application.BackupExtensionAbility'; +import MmkvTransferManager from './MMKVManager' + +const TAG = `FileBackupExtensionAbility`; +export default class BackupExtension extends BackupExtensionAbility { + async onBackup () { + console.info(TAG, `onBackup ok`); + } + + async onRestore (bundleVersion : BundleVersion) { + console.info(TAG, `onRestore ok ${JSON.stringify(bundleVersion)}`); + console.info(TAG, `onRestore end`); + new MmkvTransferManager().transfer(this.context) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/backupExtension/MMKVManager.ets b/future_entry_demo/src/main/ets/backupExtension/MMKVManager.ets new file mode 100644 index 0000000..2e311eb --- /dev/null +++ b/future_entry_demo/src/main/ets/backupExtension/MMKVManager.ets @@ -0,0 +1,231 @@ +import common from '@ohos.app.ability.common'; +import fs from '@ohos.file.fs'; +import { MMKV} from '@ohos/mmkv' +import dataPreferences from '@ohos.data.preferences'; + +// mmkv值类型定义 + +type MmkvValueType = number | Set | string | boolean; + +/** + * 双->单 mmkv文件迁移转换管理 + * + * @since 2024-04-20 + */ +export default class MmkvTransferManager { + /** + * 双->单 mmkv文件转换 + *当前例子中: + * + XXAJmmkvX177): source_mmkv source_ + mmkv. crc + * 单的mmkv文件名为:target_ + mmkv + * + * @param context context上下文 + */ + public async transfer(context: common.Context): Promise { + //第1步,move双框架mmkv文件到单框架指定目录 + let result = await this.moveMmkvFile(context); + console.log("FileBackupExtensionAbility", "startMove") + if (!result) { + console.log("FileBackupExtensionAbility", "moveFailed") + return; + } + // 第2步,解析双的mmkv的内容 + let dataMap = this.parseSourceMmkvInfo(context); + console.log("FileBackupExtensionAbility", "dataMap" + JSON.stringify(dataMap.get("stringData"))); + // 第3步,将解析的双的mmkv中的内容保存到单的mmkv中 + if (!this.insertToTargetMmkv(context, dataMap)) { + console.log("FileBackupExtensionAbility", "failed to insert target") + // 数据插入失败的处理逻辑,自行实现 + return; + } + // 第4步,从单的mmkv中读取数据 + let resultMap = this.getDataFromTarget(context); + let mmkvData = resultMap.get("legacyUserData") + if (typeof mmkvData === 'string') { + + // // 新建并打开文件 + // let file = fs.openSync(context.filesDir + '/userinfo.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + // // 写入一段内容至文件 + // fs.writeSync(file.fd, mmkvData as string); + + let options: dataPreferences.Options = { name: 'backupUserInfo' }; + let preferences = dataPreferences.getPreferencesSync(context.getApplicationContext(), options); + preferences.putSync('data', mmkvData as string); + preferences.flush() + } + // 处理查询结果,根据情况自行实现 + console.log("FileBackupExtensionAbility", "result" + JSON.stringify(resultMap.get("stringData"))) + } + + //双mmkv文件搬迁 + /** + * 移动双框架mmkv文件到单框架指定目录 + * 文件参考@link FileTransferManager.moveFiles + * + * @param context context上下文 + * @return 成功返回true, 否则false + */ + private async moveMmkvFile(context: common.Context): Promise { + try { + if (fs.accessSync("/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable")) { + fs.listFileSync("/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable", { + recursion: true + }).forEach(item => { + console.log("FileBackupExtensionAbility", "name = " + item); + }) + } + // 双升单数据迁移后,mmkv默认在files/mmkv目录下 + let sourceMmkvPath: string = '/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable'; + if (!fs.accessSync(sourceMmkvPath)) { + console.log("FileBackupExtensionAbility", "fsaccess failed") + return false; + } + // 得到源路径下面所有的文件和目录 + let listFileNames: string[] = fs.listFileSync(sourceMmkvPath); + if (!listFileNames || listFileNames.length === 0) { + console.log("FileBackupExtensionAbility", "listFile empty") + return false; + } + + // 单mmkv路径, 根据实际路径指定 + let targetPath: string = context.filesDir + '/mmkv'; + // 目标目录不存在,则需要创建 + if (!fs.accessSync(targetPath)) { + fs.mkdirSync(targetPath); + } + + // 遍历源路径下面的文件或目录,进行移动 + listFileNames.forEach((fileName: string) => { + let srcPath: string = `${sourceMmkvPath}/${fileName}`; + let destPath: string = `${targetPath}/${fileName}`; + if (fs.statSync(srcPath).isFile()) { + fs.moveFileSync(srcPath, destPath); + } else { + fs.moveDirSync(srcPath, targetPath); + } + }); + return true; + } catch (error) { + // 异常处理逻辑,自行实现 + console.log("FileBackupExtensionAbility", "catch error") + return false; + } + } + + /** + * 解析双框架mmkv文件 + * 当前例子中: + * 双的mmkv文件名为:source_mmkv, source_mmkv.crc + * 其中设置的属性有:intData 整型, boolData bool型, stringData 字符串型,floatData 浮点型,doubleData 双精度浮点型, setData Set类型 + * 单的mmkv文件名为:target_mmkv + * + * @param context context上下文 + * @return map 包含了双mmkv中的数据 + */ + private parseSourceMmkvInfo(context: common.Context): Map { + let dataMap = new Map(); + let mmkv: MMKV | null = null; + try { + // 初始化双的mmkv, mmkv文件移动到context.filesDir + '/mmkv' 目录下了 + MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv'); + // mmkv文件名 + let files = this.findDeseriredFile(context); + let mmapID: string = files[0]; + console.log("FileBackupExtensionAbility", "mmapId = " + mmapID); + // 秘钥,根据实际情况指定秘钥, 当前未设置秘钥 + let crpKey: string = ''; + mmkv = MMKV.getBackedUpMMKVWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, ''); + // 注:当前mmkv对于双框架mmkv设置的int long float的类型暂不支持,待后续更新 + dataMap.set('stringData', mmkv.decodeString('key_history_user_info', '')); + dataMap.forEach((value, key, map) => { + console.log("FileBackupExtensionAbility", "datamap key = " + key + "value = " + value.toString()); + }) + } catch (error) { + // 异常处理逻辑,自行实现 + } finally { + mmkv?.close(); + } + return dataMap; + } + + /** + * 从mmkv_migratable目录中找到所有的文件名 + */ + private findDeseriredFile(context: common.Context) : string[] { + let result : string[] = []; + let mmkvFilePath = context.filesDir + '/mmkv'; + let listFileNames: string[] = fs.listFileSync(mmkvFilePath); + for (let index = 0; index < listFileNames.length; index++) { + const element = listFileNames[index]; + console.log("FileBackupExtensionAbility", "mmkv files are = " + element) + if (element.endsWith(".crc")) { + const filePrefix = element.split(".crc")[0]; + result.push(filePrefix); + } + } + return result; + } + + /** + * 插入数据到单的mmkv中 + * + * @param context context上下文 + * @param dataMap 待插入的数据 + * @return 成功返回true, 否则false + */ + private insertToTargetMmkv(context: Context, dataMap: Map): boolean { + let mmkv: MMKV | null = null; + try { + // 初始化单框架mmkv, 获取mmkv的实例可以根据实际情况自行封装成工具类调用 + MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv'); + // mmkv文件名 + let mmapID: string = 'history_user_info'; + // 秘钥,根据实际情况指定秘钥, 当前未设置秘钥 + let crpKey: string = ''; + mmkv = MMKV.getMMKVWithMMapID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, ''); + // // 设置请求参数中的值 + mmkv.encodeString('history_user_info', dataMap.get('stringData') as string); + } catch (error) { + console.log("FileBackupExtensionAbility", "insertError" + JSON.stringify(error)) + // 异常处理逻辑,自行实现 + return false; + } finally { + mmkv?.close(); + } + return true; + } + + + /** + * 获取单框架中mmkv中的数据 + * + * @param context context上下文 + * @return map 包含单框架mmkv中的数据 + */ + private getDataFromTarget(context: common.Context): Map { + let dataMap = new Map(); + let mmkv: MMKV | null = null; + try { + // 初始化单框架mmkv + MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv'); + // mmkv文件名 + let files = this.findDeseriredFile(context); + let mmapID: string = "history_user_info"; + console.log("FileBackupExtensionAbility", "mmapId = " + mmapID); + // 秘钥,根据实际情况指定秘钥, 当前未设置秘钥 + let crpKey: string = ''; + mmkv = MMKV.getMMKVWithMMapID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, ''); + + // 取值,根据mmkv中实际存在的key来取值 + dataMap.set('legacyUserData', mmkv.decodeString('history_user_info', '')); + } catch (error) { + // 异常处理逻辑,自行实现 + } finally { + mmkv?.close(); + } + return dataMap; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/communication/Config.ets b/future_entry_demo/src/main/ets/communication/Config.ets new file mode 100644 index 0000000..bb3dacd --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/Config.ets @@ -0,0 +1,9 @@ + +export const DEFAULT_CONNECT_SITE = '116.63.213.206'; +export const DEFAULT_CONNECT_SITE_PORT = 9528; + +export const TEST_ENV_CONNECT_SITE = '121.52.252.9'; +export const TEST_ENV_CONNECT_SITE_PORT = 9628; + +export const TEST_ENV_SITE = '101.71.41.211'; +export const TEST_ENV_PORT = 9628; diff --git a/future_entry_demo/src/main/ets/communication/ConnectionInitiator.ets b/future_entry_demo/src/main/ets/communication/ConnectionInitiator.ets new file mode 100644 index 0000000..0d85d62 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/ConnectionInitiator.ets @@ -0,0 +1,63 @@ +import { ConnectionServiceHelper, SiteEntity, SiteManager } from "@kernel/lib_communication"; +import { AppInfoConfig, PreferenceService } from "biz_common"; +import { DebugToolUtil } from "biz_debug"; +import { DEBUG_TOOL, ENABLE_FIX_SERVER, + SERVER_FIX_ADDRESS, + SERVER_FIX_PORT} from "biz_debug/src/main/ets/debug/servertool/constant"; +import WTInit from "biz_trade/src/main/ets/utils/WTInit"; +import BuildProfile from "BuildProfile"; +import { ConfigInitializer } from "../initialize/ConfigInitializor"; +import { TEST_ENV_PORT, TEST_ENV_SITE } from "./Config"; +import { ServiceHandlerImpl } from "./impl/ServiceHandlerImpl"; +import deviceInfo from "@ohos.deviceInfo"; + +export class ConnectionInitiator { + + /** + * 初始化连接库相关配置 + */ + static initConnectionService(context : Context) { + ConfigInitializer.init(); + + // SiteManager.getInstance().init(TEST_ENV_CONNECT_SITE, TEST_ENV_CONNECT_SITE_PORT) + //SiteManager.getInstance().init(DEFAULT_CONNECT_SITE, DEFAULT_CONNECT_SITE_PORT) + //SiteManager.getInstance().setDomainHost(AppInfoConfig.FUTURES_DOMAIN_HOST); + + const initEntity = SiteManager.getInstance().getHQSiteEntity() + initEntity?.setDomainHost(AppInfoConfig.FUTURES_DOMAIN_HOST) + initEntity?.setSitePort(AppInfoConfig.FUTURES_HOST_PORT) + if (DebugToolUtil.isInnerNetSimulateMode()) { + // debug模拟器自动切内网环境 + ConnectionInitiator.updateServerOnDebugMode(); + } + ConnectionInitiator.updateServerByDebugTool(); + ConnectionServiceHelper.getInstance().init(new ServiceHandlerImpl()); + WTInit.init(context.getApplicationContext()); + + } + + private static updateServerOnDebugMode() { + if (isDebugMode()) { + let siteEntity = new SiteEntity(); + siteEntity.init(TEST_ENV_SITE, TEST_ENV_PORT); + SiteManager.getInstance().updateHQSiteEntity(siteEntity); + } + } + + /** + * 根据调试工具保存的站点信息初始化行情站点 + */ + private static updateServerByDebugTool() { + if (PreferenceService.getBooleanValueSync(DEBUG_TOOL, ENABLE_FIX_SERVER, false)) { + let siteEntity = new SiteEntity(); + const siteIp = PreferenceService.getStringValueSync(DEBUG_TOOL, SERVER_FIX_ADDRESS, TEST_ENV_SITE) + const sitePort = PreferenceService.getNumberValueSync(DEBUG_TOOL, SERVER_FIX_PORT, TEST_ENV_PORT); + siteEntity.init(siteIp, sitePort); + SiteManager.getInstance().updateHQSiteEntity(siteEntity); + } + } +} + +export function isDebugMode() { + return BuildProfile.BUILD_MODE_NAME.toString() === 'debug' +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/communication/impl/ServiceHandlerImpl.ets b/future_entry_demo/src/main/ets/communication/impl/ServiceHandlerImpl.ets new file mode 100644 index 0000000..0b2cb74 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/impl/ServiceHandlerImpl.ets @@ -0,0 +1,210 @@ +import { + CBASConnectionConfig, + HqConnectionConfig, + ITcpApmMonitorProxy, + SocketConnection, + TcpApmMonitorProxyHolder, + TcpLogConnectionInfo +} from '@kernel/lib_communication'; +import { + IConnectionService, + IServiceHandler, + ServiceConfig +} from '@kernel/lib_communication/src/main/ets/connection/IConnectionService'; +import hilog from '@ohos.hilog'; +// import TradeJsonStructObserver from 'biz_trade/src/main/ets/features/wtmodule/TradeJsonStructObserver' +import { AuthJsonStructObserver } from '../interceptors/AuthJsonStructObserver'; +import { HXAuthManager } from 'biz_auth'; +import { RealDataStructObserver } from '../../groupsub/RealDataStructObserver'; +import { SelfCodeChangeObserver } from '../interceptors/SelfCodeChangeObserver'; +import { SidInstanceIdReceiver } from '../interceptors/SidInstanceIdReceiver'; +import { BaseTcpLogInfo, getTcpService, TCPConnectLogInfo } from '@kernel/app_monitor_tcp'; +import { DEFAULT_SITE } from '@kernel/lib_communication/src/main/ets/site/SiteConstants'; +import connection from '@ohos.net.connection'; +import { TradeELKLogStructObserver } from 'biz_trade'; +import { HXEventMonitor } from '@kernel/app_monitor_event'; +import { Action, HxCBASAgentProvider, logMapValue } from '@b2c-spi/hx_cbas_api'; +import { HXLog } from 'biz_common'; + + +export class ServiceHandlerImpl implements IServiceHandler { + onConfig(serviceConfig: ServiceConfig): void { + let hqConnectionConfig = new HqConnectionConfig(); + hqConnectionConfig.setAuth(new HXAuthManager()); + hqConnectionConfig.addStructReceiver(new SidInstanceIdReceiver()); + hqConnectionConfig.addStructReceiver(new AuthJsonStructObserver()); + // hqConnectionConfig.addStructReceiver(new TradeJsonStructObserver()); + hqConnectionConfig.addStructReceiver(new RealDataStructObserver()); + //hqConnectionConfig.addStructReceiver(new SelfCodeChangeObserver()); + serviceConfig.registerConnection(hqConnectionConfig); + + let cbasConnectionConfig = new CBASConnectionConfig(); + cbasConnectionConfig.setAuth(new HXAuthManager()); + cbasConnectionConfig.addStructReceiver(new TradeELKLogStructObserver()); + serviceConfig.registerConnection(cbasConnectionConfig); + + this.registerTcpApmProxy() + } + + /** + * 注册tcp的APM监控代理 + */ + registerTcpApmProxy() { + TcpApmMonitorProxyHolder.get().tcpLogMonitorProxy = new HXTcpApmMonitorProxy() + } + + onPreInit(connService: IConnectionService): void { + initCommonConfig() + } + + onInitEnd(connService: IConnectionService): void { + hilog.info(0xff11, "ServiceHandlerImpl", "onInitEnd") + } + + onDestroy(connService: IConnectionService): void { + hilog.info(0xff11, "ServiceHandlerImpl", "onDestroy") + } +} + +function initCommonConfig() { + +} + +/** + * tcp APM性能监控代理类 + */ +class HXTcpApmMonitorProxy implements ITcpApmMonitorProxy { + private TCP_LOG_MONITOR_MODULE = 'tcp-log'; + + /** + * 开始解析dns + * @param domainName + */ + dnsStart(domainName: string): void { + const logMap: Map = new Map() + logMap.set('result', domainName) + HxCBASAgentProvider.getInstance().doEvent("mob_systemgeneral_dnsresolve_start", Action.SHOW, logMap) + HXLog.d("HXDNS", `dnsStart,domainName=${domainName}`) + } + + /** + * dns解析结束 + */ + dnsEnd(isSuccess: boolean, domainName: string, ipList: connection.NetAddress[] | null, cost: number, + errorMsg: string): void { + const logMap: Map = new Map() + logMap.set('type', isSuccess ? "1" : "0") + //dns解析结果 + const result = `domainName=${domainName}^ipList=${this.convertNetAddressToIpListString(ipList)}^cost=${cost}^errorMsg=${errorMsg}` + logMap.set('result', result) + HxCBASAgentProvider.getInstance().doEvent("mob_systemgeneral_dnsresolve_result", Action.SHOW, logMap) + HXLog.d("HXDNS", `dnsEnd,success=${isSuccess},result=${result}`) + } + + /** + * 转换ip列表字符串格式 + */ + private convertNetAddressToIpListString(ipList: connection.NetAddress[] | null): string { + if (ipList == null || ipList.length == 0) { + return "" + } + + const ipArray = ipList.map((addr) => addr.address) + //去重 + const uniqueIps = Array.from(new Set(ipArray)) + return uniqueIps.join(",") + } + + setSas(sas: string | null): void { + getTcpService()?.setSas(sas) + } + + sendTcpConnectLog(info: TcpLogConnectionInfo): void { + let cost = info.finishTime - info.startTime + if (cost < 0) { + cost = 0 + } + //这里需要转换一下 + const logInfo = new TCPConnectLogInfo( + cost, //连接耗时 + this.getTcpConnectResult(info.state, cost), //统一result字段 + this.getTcpConnectErrorCode(info.state, cost), //统一errorCode字段 + info.errorCode.toString(), //errorMsg + 0, //连接类型 目前没有竞速,默认0 + info.siteEntity.getSiteAddress(), //连接的服务器地址 + 0, //网络栈,目前没有判断 + false, //是否ipv6 + false, //是否v6优先连接 + info.connectSeq, //站点类型 + 0, + info.siteEntity.getSiteAddress() === DEFAULT_SITE ? 1 : 0// 是否为默认站点 + ) + getTcpService()?.getTcpConnectLogMonitor()?.addUploadInfo(logInfo) + } + + getTcpConnectErrorCode(state: SocketConnection, cost: number): number | undefined { + switch (state) { + case SocketConnection.CONNECTED: + return 4 + case SocketConnection.ERROR: + if (cost > 10000) { + return 10 + } else { + return 8 + } + default: + return -1 + } + } + + getTcpConnectResult(state: SocketConnection, cost: number): string | null | undefined { + switch (state) { + case SocketConnection.CONNECTED: + return BaseTcpLogInfo.SUCCESS + case SocketConnection.ERROR: + if (cost > 10000) { + return BaseTcpLogInfo.TIMEOUT + } else { + return BaseTcpLogInfo.FAIL + } + default: + return BaseTcpLogInfo.FAIL + } + } + + buildRequest(frameId: number, packageId: number, traceId: string | null, + pageIdMappingInstanceId: Map | null): void { + getTcpService()?.getTcpReqMonitor()?.buildRequest(frameId, packageId, traceId, pageIdMappingInstanceId) + } + + addRequestToQueue(packageId: number): void { + getTcpService()?.getTcpReqMonitor()?.addRequestToQueue(packageId) + } + + sendBegin(packageId: number): void { + getTcpService()?.getTcpReqMonitor()?.sendBegin(packageId) + } + + sendEnd(packageId: number): void { + getTcpService()?.getTcpReqMonitor()?.sendEnd(packageId) + } + + receiveResponse(packageId: number, instanceId: number, rst: number): void { + getTcpService()?.getTcpReqMonitor()?.receiveResponse(packageId, instanceId, rst) + } + + addRspToQueue(packageId: number, instanceId: number): void { + getTcpService()?.getTcpReqMonitor()?.addRspToQueue(packageId, instanceId) + } + + processReceiveData(packageId: number, instanceId: number): void { + getTcpService()?.getTcpReqMonitor()?.processReceiveData(packageId, instanceId) + } + + sendErrorLog(tag: string, msg: string): void { + //在这里将tcp的错误日志发送到apm + HXEventMonitor.onEvent(this.TCP_LOG_MONITOR_MODULE, tag, Object({ + errMsg: msg + })); + } +} diff --git a/future_entry_demo/src/main/ets/communication/interceptors/AuthJsonStructObserver.ets b/future_entry_demo/src/main/ets/communication/interceptors/AuthJsonStructObserver.ets new file mode 100644 index 0000000..077ad96 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/interceptors/AuthJsonStructObserver.ets @@ -0,0 +1,98 @@ +import { getConnectionManager, HXLog } from 'biz_common'; +import { + IStructReceiver, + ProtocolDef, + StuffBaseStruct, + StuffResourceStruct, + ConnectionServiceHelper +} from '@kernel/lib_communication'; +import { StringUtils } from 'hxutil'; +import { ConnectionSeq } from '@kernel/lib_communication'; +import { ServerResourceManager, ServerTimeProvider } from 'biz_hxservice'; +import { getTcpService } from '@kernel/app_monitor_tcp'; +import { APMHelper } from '../../monitor/APMHelper'; + +/** + * 认证包拦截器 + * 这个拦截器用于接受认证后的session数据包以及事件包 + * @author menghuiyuan@myhexin.com + */ +export class AuthJsonStructObserver implements IStructReceiver { + constructor() { + } + + onIntercept(stuffBaseStruct: StuffBaseStruct): boolean { + if (stuffBaseStruct == null) { + return false; + } + const headId = stuffBaseStruct.packageId; + if (headId == ProtocolDef.DATAID_NEW_REALTIME) { + const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK; + if (dataType === ProtocolDef.DATA_TYPE_JSON) { + return true; + } + } + return false; + } + + handleStruct(stuffBaseStruct: StuffBaseStruct): boolean { + if (!(stuffBaseStruct instanceof StuffResourceStruct)) { + return false; + } + let tmpBuffer = stuffBaseStruct.buffer; + const decodedStr = StringUtils.decode(tmpBuffer, "GBK") + + try { + const parsedObj: AuthStruct = JSON.parse(decodedStr) + ServerResourceManager.getInstance().parseResource(decodedStr); + if (parsedObj.srvTime && (typeof parsedObj.srvTime === 'number')) { + ServerTimeProvider.getInstance().initServiceTime(parseInt(parsedObj.srvTime) * 1000) + } + const hasSessionId = StringUtils.isNotEmpty(parsedObj.sessionId); + //处理sessionId + if (hasSessionId) { + ConnectionServiceHelper.getInstance() + .getConnectionService() + .setSessionId(ConnectionSeq.HQ, parsedObj.sessionId) + + //设置tcp监控的sas + getTcpService()?.setSas(`${parsedObj.sessionId}_${getConnectionManager()?.getConnectedIp()}`) + } + const hasClientIP = StringUtils.isNotEmpty(parsedObj.clientIp); + if (hasClientIP) { + APMHelper.clientIp = parsedObj.clientIp + AppStorage.setOrCreate('clientIp', parsedObj.clientIp) + } + if (hasSessionId || hasClientIP) { + //WTLoginAdapterService.receiveSessionIdHandle() + } + //todo 处理账户事件,如强制下线等 + if (StringUtils.isNotEmpty(parsedObj.events)) { + HXLog.i("AuthJSONStruct", "error parsing") + } + } catch (e) { + HXLog.e("AuthJSONStruct", "Error parsing " + JSON.stringify(e)) + } + + return false; + + } + + getReceiverId(): string { + return "AuthJsonStructObserver" + } +} + +/** + * 认证包结构 + */ +export class AuthStruct { + srvTime: string = ""; + clientIp: string = ""; + clientPort: string = ""; + serverIp: string = ""; + releaseName: string = ""; + AuthCountryCodeMd5: string = ""; + events: string = ""; + sessionId: string = ""; +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/communication/interceptors/SelfCodeChangeObserver.ets b/future_entry_demo/src/main/ets/communication/interceptors/SelfCodeChangeObserver.ets new file mode 100644 index 0000000..eaec0d6 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/interceptors/SelfCodeChangeObserver.ets @@ -0,0 +1,72 @@ +import { StringUtils } from 'hxutil'; +import { IStructReceiver, ITCPClient, ProtocolDef, StuffBaseStruct, StuffResourceStruct } from '@kernel/lib_communication'; +import { SelfCodeEvent } from '../../selfcode/SelfCodeEvents'; +import { UgcClient } from '../../register/ugcregister/UgcClient'; + +/** + * 自选股更新订阅拦截 + * @author menghuiyuan@myhexin.com + */ +export class SelfCodeChangeObserver implements IStructReceiver { + + constructor() { } + + onIntercept(stuffBaseStruct: StuffBaseStruct): boolean { + if (stuffBaseStruct == null) { + return false; + } + const headId = stuffBaseStruct.packageId; + if (headId == ProtocolDef.DATAID_NEW_REALTIME) { + const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK; + if (dataType === ProtocolDef.DATA_TYPE_JSON) { + return true; + } + } + return false; + } + + //自选股版本变更推送id + private readonly UPDATE_SIG_VERSION = '00000009'; + //自选股更新强制推送id + private readonly UPDATE_SIG_FORCE = '00000010'; + + handleStruct(stuffBaseStruct: StuffBaseStruct): boolean { + if (!(stuffBaseStruct instanceof StuffResourceStruct)) { + return false; + } + const resStruct = stuffBaseStruct as StuffResourceStruct; + const tmpBuffer = resStruct.buffer; + const decodedStr = StringUtils.decode(tmpBuffer, "GBK") + try { + const parsedObj : SelfCodeEvent = JSON.parse(decodedStr); + if (parsedObj) { + const eventArrayLen = parsedObj.events.length; + for (let index = 0; index < eventArrayLen; index++) { + const eventObj = parsedObj.events[index]; + const eventId = eventObj.eventID; + switch (eventId) { + //版本号推送 + case this.UPDATE_SIG_VERSION: + const selfCodeVersionData = eventObj.eventData; + //TODO 推送版本号: + UgcClient.getInstance().onSelfCodeRemoteVersion(Number(selfCodeVersionData.version)) + return true; + case this.UPDATE_SIG_FORCE: + //TODO 推送强制更新版本 + const forceUpdateVersionData = JSON.stringify(eventObj.eventData); + return true; + default: + return false; + } + } + } + } catch (e) { + return false; + } + return false; + } + + getReceiverId(): string { + return "SelfCodeChangeObserver"; + } +} diff --git a/future_entry_demo/src/main/ets/communication/interceptors/SidInstanceIdReceiver.ets b/future_entry_demo/src/main/ets/communication/interceptors/SidInstanceIdReceiver.ets new file mode 100644 index 0000000..2247587 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/interceptors/SidInstanceIdReceiver.ets @@ -0,0 +1,70 @@ +import { + IStructReceiver, + RequestQueueManagement, + StuffBaseStruct, + StuffResourceStruct +} from '@kernel/lib_communication'; +import { SidPermissionRefreshClient } from 'biz_auth'; + +const USER_RESPONSE_TYPE_NOTHING = 0; + +/** + * SID 权限刷新特殊包接收器。 + * + * 对齐 Android SIDInstanceIdReicever:主动刷新 SID 的回包使用固定 instanceId=13, + * 服务端可能按 type=0 返回普通 body,连接库会先解成 StuffBaseStruct。 + * 这里只按包头条件识别该特殊包,把原始 body 包装成 LOGIN_AUTH 资源后继续投递给请求 client。 + */ +export class SidInstanceIdReceiver implements IStructReceiver { + onIntercept(stuffBaseStruct: StuffBaseStruct): boolean { + return this.isSidRefreshBaseStruct(stuffBaseStruct); + } + + handleStruct(stuffBaseStruct: StuffBaseStruct): boolean { + if (!this.isSidRefreshBaseStruct(stuffBaseStruct)) { + return false; + } + + // IStructReceiver 返回 true 后会截断原始 StuffBaseStruct 的普通分发流程, + // 这里需要把包装后的 LOGIN_AUTH 资源主动投递给注册在 REFRESH_SID 上的请求 client。 + const sidStruct = this.createSidResourceStruct(stuffBaseStruct); + const clients = RequestQueueManagement.getInstance().getNetworkClient(SidPermissionRefreshClient.REFRESH_SID); + if (!clients || clients.length === 0) { + return false; + } + + for (const client of clients) { + client?.receive(sidStruct); + } + return true; + } + + getReceiverId(): string { + return 'SidInstanceIdReceiver'; + } + + private isSidRefreshBaseStruct(stuffBaseStruct: StuffBaseStruct | null): boolean { + if (!stuffBaseStruct || stuffBaseStruct instanceof StuffResourceStruct) { + return false; + } + // 对齐 Android filter:只通过包头 type=0 和固定 instanceId=13 识别 SID 刷新特殊包。 + // SID 内容不参与判断,避免把权限字段格式差异误判为客户端解析问题。 + const rawBuffer = stuffBaseStruct.rawBuffer; + return stuffBaseStruct.instanceId === SidPermissionRefreshClient.REFRESH_SID && + stuffBaseStruct.headType === USER_RESPONSE_TYPE_NOTHING && + rawBuffer !== null && + rawBuffer.length > 0; + } + + private createSidResourceStruct(stuffBaseStruct: StuffBaseStruct): StuffResourceStruct { + // 这里不解码、不解析 SID,只复制主站原始 body 并包装成 LOGIN_AUTH。 + // 后续 SID 字符串解析和权限更新仍交给 SidPermissionRefreshClient 统一处理。 + const rawBuffer = stuffBaseStruct.rawBuffer as Uint8Array; + const sidBuffer = rawBuffer.slice(0, rawBuffer.length); + const sidStruct = new StuffResourceStruct(StuffResourceStruct.LOGIN_AUTH, sidBuffer); + stuffBaseStruct.copyHead(sidStruct); + sidStruct.connectionSeq = stuffBaseStruct.connectionSeq; + sidStruct.rawBuffer = sidBuffer; + return sidStruct; + } +} diff --git a/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionCheckImpl.ets b/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionCheckImpl.ets new file mode 100644 index 0000000..6e3ef44 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionCheckImpl.ets @@ -0,0 +1,27 @@ +import { HXLog } from 'biz_common'; +import { ICheckCallback } from '@kernel/lib_communication/src/main/ets/connection/ICheckCallback'; + +/** + * 连接库检查回调类 + * @author menghuiyuan@myhexin.com + */ +export class ConnectionCheckImpl implements ICheckCallback { + + private readonly TAG = "SocketConnectionCheckImpl" + onChannelOk(): void { + HXLog.i(this.TAG, "onChannelOk"); + } + + onChannelBad(badType: number): void { + HXLog.i(this.TAG, "onChannelBad" + badType); + } + + /** + * 是否需要重新连接 + * @returns true,默认开启重连 + */ + needReconnect(): boolean { + return true; + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionServiceUtil.ets b/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionServiceUtil.ets new file mode 100644 index 0000000..7958cb2 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/socketmonitor/ConnectionServiceUtil.ets @@ -0,0 +1,12 @@ +import { getConnectionManager } from 'biz_common'; +import { UgcClient } from '../../register/ugcregister/UgcClient'; + +/** + * 主动关闭当前的socket连接 + */ +export function closeSocketConnection() : void { + if (getConnectionManager()) { + getConnectionManager()?.disconnect(false); + UgcClient.getInstance().onHXSocketClose() + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/communication/socketmonitor/HXSocketMonitor.ets b/future_entry_demo/src/main/ets/communication/socketmonitor/HXSocketMonitor.ets new file mode 100644 index 0000000..362ef22 --- /dev/null +++ b/future_entry_demo/src/main/ets/communication/socketmonitor/HXSocketMonitor.ets @@ -0,0 +1,31 @@ +import { getConnectionManager, HXLog, RuntimeStatus } from 'biz_common'; +import { ConnectionCheckImpl } from './ConnectionCheckImpl'; +import { closeSocketConnection } from './ConnectionServiceUtil'; + +/** + * socket的监控管理 + */ +export class HXSocketMonitor { + + private readonly TAG : string = "HXSocketMonitor"; + + //当前App是否处于前台 + private isAppForeground = false; + + onAppForeground() { + HXLog.i(this.TAG, "onAppForeground") + this.isAppForeground = true; + if (getConnectionManager()) { + HXLog.i(this.TAG, "onAppForeground checkConnection") + getConnectionManager()?.checkConnection(new ConnectionCheckImpl()); + } + } + + onAppBackground() { + HXLog.i(this.TAG, "onAppBackground") + this.isAppForeground = false; + closeSocketConnection() + RuntimeStatus.getInstance().setTcpDisConnected() + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/constant/EntryConstant.ets b/future_entry_demo/src/main/ets/constant/EntryConstant.ets new file mode 100644 index 0000000..598b397 --- /dev/null +++ b/future_entry_demo/src/main/ets/constant/EntryConstant.ets @@ -0,0 +1,4 @@ +export class EntryConstant { + //微信分享id + static readonly WX_SHARE_ID = 'wx6ba37430b7916a86'; +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/entryability/EntryAbility.ets b/future_entry_demo/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..b1cc948 --- /dev/null +++ b/future_entry_demo/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,480 @@ +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import hilog from '@ohos.hilog'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import Want from '@ohos.app.ability.Want'; +import window from '@ohos.window'; +import { + AppConfigManager, + buildHeader, + CodeTableMonitor, + GlobalContext, + HXLog, + jumpToQuote, + PreferenceService, + RuntimeStatus, + ScreenUtil, + StartManager, + updateBreakpoint, + updateCurrentWindowWith, + WebJumpLinkWhiteManager +} from 'biz_common'; +import { AuthProviderService } from 'biz_auth'; +import { ContextUtils, StringUtils } from 'hxutil'; +import { ContextUtils as ContextUtils2b } from '@b2b/hxutil'; +import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants'; +import { SelfCodeGroupHandler } from '../groupsub/business/SelfCodeGroupHandler'; +import { HXSocketMonitor } from '../communication/socketmonitor/HXSocketMonitor'; +import { Configuration, ConfigurationConstant, wantConstant } from '@kit.AbilityKit'; +import { UgcClient } from '../register/ugcregister/UgcClient'; +import { ContinuousDataHelper, CurveNaviModel } from 'biz_quote/src/main/ets/data/ContinuousDataHelper'; +import { DelayMarketManager, FutureMarketDataManager, HQTabManager, InactiveContractManager, + MainContractGroupHandler } from 'biz_market'; +import { MainContractManager } from 'biz_market/src/main/ets/manager/MainContractManager'; +import { SelfCodeMainListHandler } from '../groupsub/business/SelfCodeMainListHandler'; +import { UgcAuthListener } from '../register/ugcregister/UgcAuthListener'; +import { AppUtil } from '@pura/harmony-utils'; +import { ElkUploadRegister } from '../register/elkregister/ElkUploadRegister'; +import { HexinVersionControl } from 'biz_trade/src/main/ets/utils/HexinVersionControl'; +import { SecurityMonitorInit } from '../monitor/SecurityMonitorInit'; +import { TabConfigManager } from '@b2c/biz_discover'; +import { ShareConfig } from '@b2c-spi/hx_share_api/src/main/ets/config/ShareConfig'; +import { HMRouterMgr } from '@hadss/hmrouter'; +import { EntryConstant } from '../constant/EntryConstant'; +import { ConnectionInitiator } from '../communication/ConnectionInitiator'; +import { APMHelper } from '../monitor/APMHelper'; +import { cbasService } from '../register/cbasregister/CbasServiceRegister'; +import { eventSubscription } from '../monitor/AppEvent'; +import { StockGroupConstants } from 'biz_selfcode'; +import { StockGroupHandler } from '../groupsub/business/StockGroupHandler'; +import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler'; +import { SidGroupHandler } from '../groupsub/business/SidGroupHandler'; +import { FuturesCompanyGroupHandler, YYBListGroupHandler, TradeSettingGroupHandler, TradeCloudSyncConfig, + TradeMsgUniqueIdManager, + TradeNotificationTrigger } from 'biz_trade'; +import { GroupSubManager } from 'base_mobilepush'; +import { CodeTableManager } from 'lib_codetablemanager'; +import BuildProfile from 'BuildProfile'; +import { GrayTestPushHandler } from '../groupsub/business/GrayTestPushHandler'; +import { AccountAnalysisLuaPushHandler } from '../groupsub/business/AccountAnalysisLuaPushHandler'; +import { QhStrategyAdPushHandler } from '../groupsub/business/QhStrategyAdPushHandler'; +import { GrayTestInitializor } from '../initialize/GrayTestInitializor'; +import { CloudStoreLogInitializor } from '../initialize/CloudStoreLogInitializor'; +import { HXPushService } from 'service_push'; +import { WantCacheManager } from './WantManager'; +import { pushService } from '@kit.PushKit'; +import { HXNetwork } from '../network/HXNetwork'; +import { AdvertisingMonitorService, KaihuFlagManager, ServerTimeProvider, UserSourceManager } from 'biz_hxservice'; +import { UnifyUrlManager } from '@b2c-f/url-modify-config'; +import { FontManager } from '../font/FontManager'; +import { BizType, IndicatorGroupHandler, CommonIndicatorsManage, IndicatorParamsManage } from 'biz_indicator'; +import { FigureRegistry } from 'biz_chart_data_processor'; +import { TradeInitializor } from '../initialize/TradeInitializor'; +import { AdsInitializer } from '../initialize/AdsInitializor'; +import { AIGroupClient } from 'service_aigroup' +import { AIGroupPushHandler } from '../groupsub/business/AIGroupPushHandler'; +import { DrawLineGroupHandler } from 'biz_quote'; +import { CurveStorage } from 'biz_quote'; +import { OpenAccountManager } from 'biz_openaccount/src/main/ets/manager/OpenAccountManager'; +import { HttpUtils } from '@kernel/rcp'; +import { DebugToolUtil } from 'biz_debug'; +import { TradeDailyPushDataManager } from '../NotificationExtension/TradeDaily/TradeDailyPushDataManager'; +import { TradeDailyConvertManager } from '../NotificationExtension/TradeDaily/TradeDailyConvertManager'; +import { PushMessageManager } from 'biz_trade/src/main/ets/service/push/PushMessageManager'; +import { ColourModeStatus } from '@kernel/theme_manager'; +import { VarietyCodeTransformManager } from 'biz_chart_data_processor/src/main/ets/utils/VarietyCodeTransformManager'; +import { ContextUtils as ContextUtils2c } from '@b2c-m/ohos_hx_utils'; +import { FunctionAuthorityGroupHandler } from '../groupsub/business/FunctionAuthorityGroupHandler'; +import { FunctionAuthorityRequestManager } from '../functionauthority/FunctionAuthorityRequester'; +import { QhStrategyAdPagePushHandler } from '../groupsub/business/QhStrategyAdPagePushHandler'; + +/** + * 应用的主Ability + * @author menghuiyuan@myhexin.com + */ +export default class EntryAbility extends UIAbility { + private readonly tag: string = "EntryAbility" + private windowObj?: window.Window + private curBp: string = '' + //socket连接监控,需要接收应用前后台事件 + private socketMonitor: HXSocketMonitor = new HXSocketMonitor(); + private curNaviModel: CurveNaviModel | null = null; + + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + + pushService.receiveMessage('IM', this, (payload) => { + // todo 应用前台弹窗接收push逻辑,待补充。 + try { + const data = payload.data; + const pushInfo: string = JSON.parse(data)?.data + HXLog.d(this.tag, `Succeeded in getting notification,data=${JSON.stringify(pushInfo)}`); + + const pushModel = TradeDailyPushDataManager.getInstance().convertToTradeDailyPushModel(JSON.parse(pushInfo)) + if (!pushModel) { + return + } + // 推送去重判断 + TradeMsgUniqueIdManager.getInstance().isSameIdentify( + pushModel.time, + pushModel.iconType, + pushModel.type, + pushModel.orderNo + ).then((hasSame) => { + if (!hasSame) { + TradeDailyPushDataManager.getInstance().recordPushDaily(JSON.parse(pushInfo)).then(() => { + TradeDailyConvertManager.getInstance().convertDailyFromPush() + }) + PushMessageManager.getInstance().processReceiveHXPushMessage(pushInfo, false) + } + }) + } catch (error) { + HXLog.e(this.tag, `Parse push data failed: ${error}`) + } + }) + + hilog.info(0x0000, this.tag, '%{public}s', 'Ability onCreate'); + + // ========== 注册自定义 Figure ========== + // 注册所有 iOS 迁移过来的自定义 Figure + // 包括:bar, barDouble, polygon, tower, american, trade, yAxisRange + FigureRegistry.registerAll() + hilog.info(0x0000, this.tag, 'Custom Figures registered successfully'); + // ===================================== + + DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME + DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME + DebugToolUtil.BUILD_MODE_NAME = BuildProfile.BUILD_MODE_NAME + // Context初始化前移 + GlobalContext.initContext(this.context.getApplicationContext()) + ContextUtils.initContext(this.context.getApplicationContext()) + ContextUtils2c.initContext(this.context.getApplicationContext()) + ContextUtils2b.initContext(this.context.getApplicationContext()) + AppUtil.init(this.context) + // 交易通知触发器初始化 + TradeNotificationTrigger.getInstance().init(this.context) + //崩溃保护代码 + SecurityMonitorInit.getInstance().init(); + GrayTestInitializor.initGrayTest() + CloudStoreLogInitializor.initCloudStoreLog() + HMRouterMgr.init({ context: this.context }); + UnifyUrlManager.getInstance().init(this.context); + // 启动管理:是否为安装后第一次进入APP + StartManager.getInstance().init() + RuntimeStatus.getInstance().init() + ElkUploadRegister.getInstance().init(); + UgcClient.getInstance().init() + AuthProviderService.getInstance().setUgcAuthListener(UgcAuthListener.getInstance()) + this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE, (result) => { + console.info(`setMissionContinueState: ${JSON.stringify(result)}`); + }); + eventSubscription() + this.initRcp() + ShareConfig.setAppId(EntryConstant.WX_SHARE_ID); + this.initFuture() + AdsInitializer.initAds() + // 初始化 AI 分组客户端 + AIGroupClient.getInstance().init(DebugToolUtil.isDebugMode()) + WantCacheManager.getInstance().stashWant(want) + + FontManager.getInstance().loadTHSFonts() + HXLog.init() + OpenAccountManager.getInstance().upCfmmcRecord() + UserSourceManager.getInstance().initUserSource() + } + + /** + * 初始化Rcp库配置 + */ + initRcp() { + HttpUtils.setGlobalHeaders((url) => { + return buildHeader(true) + }) + } + + initFuture() { + //大内容tab配置初始化 + TabConfigManager.getInstance().initDiscoverTabConfig() + // 初始化常用指标管理(参考 iOS MePool.mm:338 userIdLoginSuccess) + CommonIndicatorsManage.getInstance().init(this.context) + // 初始化指标参数管理(参考 iOS MePool.mm:340) + IndicatorParamsManage.getInstance().init(this.context) + FunctionAuthorityRequestManager.requestAuthorityDataAfterLocalLoad() + //初始化码表 + const codeTableInstance = CodeTableManager.get() + codeTableInstance.setInnerDebugMode(DebugToolUtil.isInnerNetSimulateMode()) + codeTableInstance.initCodeTable(this.context, HexinVersionControl.disableCodeTableIncrementUpdate(), + CodeTableMonitor.getInstance(), () => { + return ServerTimeProvider.getInstance().getServerTime() + }, (isJson?: boolean) => { + return buildHeader(isJson) + }).then(() => { + //主力合约初始化 + MainContractManager.get().init() + //初始化期货行情页面的tab配置 + HQTabManager.get().init() + }) + //期货市场配置 + FutureMarketDataManager.get().init() + //不活跃合约 + InactiveContractManager.get().init() + // 初始化延时市场配置 + DelayMarketManager.get().init() + // 初始化品种码转换映射表(如果还没加载) + VarietyCodeTransformManager.init() + // 交易管理初始化 + TradeInitializor.init(this.context) + // 分时持久化存储UI状态存储初始化 + CurveStorage.getInstance().initPersistentStorage() + } + + onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { + HXLog.i(this.tag, `onNewWant ${AbilityConstant.LaunchReason.CONTINUATION}`) + WantCacheManager.getInstance().stashWant(want) + if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) { + // 将上述的保存的数据取出恢复 + let continueInput = ''; + if (want.parameters != undefined) { + continueInput = want.parameters.curveData as string; + + HXLog.i(this.tag, "onNewWant continue input = " + continueInput) + this.curNaviModel = JSON.parse(continueInput) as CurveNaviModel; + } else { + HXLog.i(this.tag, "onNewWant continue input is empty") + } + } + } + + onDestroy() { + OpenAccountManager.getInstance().upCfmmcRecord() + PreferenceService.putVal('Install', 'isFirstTimeEntering', false, true); + hilog.info(0x0000, this.tag, '%{public}s', 'Ability onDestroy'); + } + + onContinue(wantParam: Record): AbilityConstant.OnContinueResult | Promise { + let versionDst = wantParam.version; // 获取迁移对端应用的版本号 + let versionSrc: number = 1000600; // 获取迁移源端即本端应用的版本号 + HXLog.i(this.tag, "versionDst = " + versionDst + "versionSrc = " + versionSrc) + if (versionDst < versionSrc) { // 兼容性校验 + // 兼容性校验不满足 + HXLog.w(this.tag, "onContinue, dst not match the minumum version, abort continuation process") + return AbilityConstant.OnContinueResult.MISMATCH; + } + if (ContinuousDataHelper.getInstance().getCurrentNavi()) { + hilog.info(0x0000, this.tag, "onContinue set success = "); + wantParam["curveData"] = JSON.stringify(ContinuousDataHelper.getInstance().getCurrentNavi()!); + } + wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] = false; + return AbilityConstant.OnContinueResult.AGREE; + } + + onWindowStageCreate(windowStage: window.WindowStage) { + //存储程序当前的 windowStage,用于创建子窗口。 + AppStorage.setOrCreate('EntryAbilityWindowStage', windowStage); + // 埋点调试工具需要使用的 windowStage + AppStorage.setOrCreate('windowStage', windowStage); + + //初始化App的一些公共资源和窗口设置 + let windowClass: window.Window = windowStage.getMainWindowSync(); + let avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); + GlobalContext.initNavigationBarHeight(avoidArea.bottomRect.height) + + + let statusBarHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height; + GlobalContext.initStatusBarHeight(statusBarHeight); + // Main window is created, set main page for this ability + hilog.info(0x0000, this.tag, '%{public}s', 'Ability onWindowStageCreate'); + windowStage.getMainWindow().then((windowObj) => { + this.windowObj = windowObj + // 获取应用启动时的窗口尺寸 + this.curBp = updateBreakpoint(windowObj.getWindowProperties().windowRect.width, this.curBp) + // 注册回调函数,监听窗口尺寸变化 + windowObj.on('windowSizeChange', (windowSize) => { + this.curBp = updateBreakpoint(windowSize.width, this.curBp) + updateCurrentWindowWith(windowSize.width) + }) + }); + this.registerGroupHandlers(); + ColourModeStatus.initColourModeSetting(this.context) + HexinVersionControl.recordFirstInstallVersion(); + HexinVersionControl.getInstance().requestVersionConfig(); + + // 网络不可用 + const netAvailable = HXNetwork.getInstance().detectNet(); + if (!netAvailable) { + this.loadNetworkAnomalyPage(windowStage); + return; + } + + this.initializeCbasService().then(() => { + // 正常流程 + }).catch((error: Error) => { + // 即使失败也记录日志,但继续加载页面 + HXLog.e(this.tag, 'cbasService init failed: %{public}s'); + }).finally(() => { + // 确保页面一定会加载 + if (!PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false)) { + this.loadPrivacyPage(windowStage); + } else { + this.loadIndexPage(windowStage); + } + }); + + } + + onWindowStageRestore(windowStage: window.WindowStage): void { + //应用窗口恢复时,如果有接续的数据就说明当前为接续开启应用,打开分时页 + HXLog.i(this.tag, "onWindowStageRestore") + let parsedData: CurveNaviModel; + if (this.curNaviModel) { + HXLog.i(this.tag, "onWindowStageRestore data is valid") + parsedData = this.curNaviModel; + jumpToQuote(parsedData.code, parsedData.marketId, parsedData.codeList, parsedData.marketIdList); + this.curNaviModel = null; + } + } + + private registerGroupHandlers() { + GroupSubManager.getInstance().addUserGroupHandler(SelfCodeMainListHandler.GROUP_HANDLE_NAME, new SelfCodeMainListHandler()); + GroupSubManager.getInstance().addUserGroupHandler(SelfCodeGroupHandler.GROUP_HANDLE_NAME, new SelfCodeGroupHandler()); + GroupSubManager.getInstance().addUserGroupHandler(YYBListGroupHandler.GROUP_HANDLE_NAME,new YYBListGroupHandler()) + GroupSubManager.getInstance().addUserGroupHandler(TradeCloudSyncConfig.FILE_NAME, new TradeSettingGroupHandler()) + const indicatorChannels: string[] = [ + BizType.COMMON_INDICATORS, + BizType.CUSTOM_INDICATORS, + BizType.INDICATOR_PARAMS, + BizType.SQUARE_INDICATORS + ] + for (const channel of indicatorChannels) { + GroupSubManager.getInstance().addUserGroupHandler(channel, new IndicatorGroupHandler(channel)) + } + // 接入画线同步 push 分组,收到远端画线后驱动本地刷新与重绘。 + GroupSubManager.getInstance().addUserGroupHandler(DrawLineGroupHandler.GROUP_KEY, new DrawLineGroupHandler()) + GroupSubManager.getInstance().addUserGroupHandler(AIGroupPushHandler.GROUP_HANDLE_NAME, new AIGroupPushHandler()) + GroupSubManager.getInstance().addUserGroupHandler(SidGroupHandler.GROUP_HANDLE_NAME, new SidGroupHandler()) + GroupSubManager.getInstance().addUserGroupHandler( + FunctionAuthorityGroupHandler.GROUP_HANDLE_NAME, + new FunctionAuthorityGroupHandler() + ) + GroupSubManager.getInstance().addUserGroupHandler(QhStrategyAdPushHandler.GROUP_HANDLE_NAME, new QhStrategyAdPushHandler()) + GroupSubManager.getInstance().addAllGroupHandler(QhStrategyAdPagePushHandler.GROUP_HANDLE_NAME, new QhStrategyAdPagePushHandler()) + GroupSubManager.getInstance().addAllGroupHandler(GrayTestPushHandler.GROUP_HANDLE_NAME, new GrayTestPushHandler()); + GroupSubManager.getInstance().addAllGroupHandler(AccountAnalysisLuaPushHandler.GROUP_HANDLE_NAME, new AccountAnalysisLuaPushHandler()); + GroupSubManager.getInstance().addAllGroupHandler(FuturesCompanyGroupHandler.GROUP_HANDLE_NAME,new FuturesCompanyGroupHandler()) + GroupSubManager.getInstance().addAllGroupHandler(StockGroupConstants.KEY_STOCK_STORAGE_UPDATE, new StockGroupHandler()) + GroupSubManager.getInstance().addAllGroupHandler(StockGroupConstants.KEY_VARIETY_STORAGE_UPDATE, new VarietyGroupHandler()) + GroupSubManager.getInstance().addAllGroupHandler(MainContractGroupHandler.GROUP_HANDLE_NAME, new MainContractGroupHandler()) + + } + + onWindowStageDestroy() { + hilog.info(0x0000, this.tag, '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground() { + // Ability has brought to foreground + AppStorage.setOrCreate('isAbilityForeGround', true); + cbasService.onForeground() + this.socketMonitor.onAppForeground(); + HXPushService.getInstance().checkPushPermissionStatus() + ColourModeStatus.updateSystemColourModeOnAbilityForeground(this.context) + } + + onConfigurationUpdate(newConfig: Configuration): void { + ColourModeStatus.onSystemConfigurationUpdate(newConfig.colorMode) + } + + onBackground() { + // Ability has back to background + AppStorage.setOrCreate('isAbilityForeGround', false); + cbasService.onBackground() + this.socketMonitor.onAppBackground(); + } + + /** + * 加载应用主页面 + * @param windowStage + */ + private loadIndexPage(windowStage: window.WindowStage) { + //三方标题栏风险提示,白名单 + WebJumpLinkWhiteManager.getInstance().loadWhiteJumpLink() + + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, this.tag, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + onAppStartLoading(this.context,true); + hilog.info(0x0000, this.tag, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + + /** + * 加载隐私政策页面 + * @param windowStage + */ + private loadPrivacyPage(windowStage: window.WindowStage) { + windowStage.loadContent('pages/LauncherPage', (err, data) => { + if (err.code) { + hilog.error(0x0000, this.tag, 'Failed to load loadPrivacyPage. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + onAppStartLoading(this.context); + hilog.info(0x0000, this.tag, 'Succeeded in loadPrivacyPage. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + + /** + * 加载网络异常页面 + * @param windowStage + */ + private loadNetworkAnomalyPage(windowStage: window.WindowStage) { + windowStage.loadContent('network/NetworkAnomalyPage', (err, data) => { + if (err.code) { + hilog.error(0x0000, this.tag, 'Failed to load NetworkAnomalyPage. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + onAppStartLoading(this.context); + hilog.info(0x0000, this.tag, 'Succeeded in NetworkAnomalyPage. Data: %{public}s', JSON.stringify(data) ?? ''); + }); + } + + /** + * 加载埋点初始化 + */ + private async initializeCbasService() { + try { + await AppConfigManager.getInstance().fetchIMEI() + let udid = AppConfigManager.getInstance().getUdidSync() + if(!StringUtils.isNotEmpty(udid)) { + udid = AppConfigManager.UDID_NONE + } + await cbasService.init(udid) + HXLog.i(this.tag, 'cbasService initialized successfully'); + } catch (error) { + HXLog.i( this.tag, 'cbasService init failed'); + } + } + + + +} + +function onAppStartLoading(context:Context,isAgreePrivacy:boolean = false) { + ConnectionInitiator.initConnectionService(context) + AppConfigManager.getInstance().fetchIMEI().then((isSuccess: boolean) => { + if (isSuccess) { + AuthProviderService.getInstance().sendAuthRequest(ConnectionSeq.HQ); + } + let udid = AppConfigManager.getInstance().getUdidSync() + if(!StringUtils.isNotEmpty(udid)) { + udid = AppConfigManager.UDID_NONE + } + cbasService.init(udid) + if (isAgreePrivacy) { + AdvertisingMonitorService.getInstance().init(context) + } + }); + //初始化APM + APMHelper.init(context); + //初始化窗口工具 + ScreenUtil.get().init(); +} diff --git a/future_entry_demo/src/main/ets/entryability/ProtocolJumpTest.ets b/future_entry_demo/src/main/ets/entryability/ProtocolJumpTest.ets new file mode 100644 index 0000000..9ab4671 --- /dev/null +++ b/future_entry_demo/src/main/ets/entryability/ProtocolJumpTest.ets @@ -0,0 +1,57 @@ +import { MDataModel, AppPushMessage } from "service_push"; + +export function ProtocolJumpTest() { + let appMsg1: MDataModel = { + 'A': '10', + 'F': '2205', + 'C': 'FG2601', + 'ACT': '70', + 'TY': '20', + 'ST': '预警通知' + } + + let appMsg2: MDataModel = { + 'A': '10', + 'ACT': '70', + "F": "2804", + "U": "https://news.10jqka.com.cn/20251111/c672408397.shtml" + } + + let appMsg3: MDataModel = { + // 跳转个股 + 'A': '10', + 'F': '2205', + 'C': 'USDIND', + 'ACT': '60', + 'ST': '美元指数预警' + } + + let appMsg4: MDataModel = { + // 跳转外部浏览器 + 'A': '30', + 'ACT': '70', + "F": "2804", + "U": "https://news.10jqka.com.cn/20251111/c672408397.shtml" + } + + let appMsg5: MDataModel = { + // 拉起其他应用 + 'A': '40', + 'ACT': '70', + 'PN':'com.hexin.hmn.sjcg', + 'CN':'EntryAbility', + } + + let protocol1: AppPushMessage = { + APPID: '207', + ID: '604900', + TIPTYPE: '1', + TITLE: '标题', + DESC: '内容', + APPMSG: appMsg1 + } + + let data: Record = {}; + data['payload'] = JSON.stringify(protocol1); + return data; +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/entryability/RemoteNotificationExtAbility.ets b/future_entry_demo/src/main/ets/entryability/RemoteNotificationExtAbility.ets new file mode 100644 index 0000000..a616885 --- /dev/null +++ b/future_entry_demo/src/main/ets/entryability/RemoteNotificationExtAbility.ets @@ -0,0 +1,26 @@ +import { RemoteNotificationExtensionAbility, pushCommon } from '@kit.PushKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { TradeDailyPushDataManager } from '../NotificationExtension/TradeDaily/TradeDailyPushDataManager'; + +const DOMAIN = 0x0000; + +export default class RemoteNotificationExtAbility extends RemoteNotificationExtensionAbility { + async onReceiveMessage(remoteNotificationInfo: pushCommon.RemoteNotificationInfo): + Promise { + const data = remoteNotificationInfo.data + try { + const pushInfo: string = JSON.parse(data)?.data + await TradeDailyPushDataManager.getInstance().initDB(this.context) + const pushModel = TradeDailyPushDataManager.getInstance().convertToTradeDailyPushModel(JSON.parse(pushInfo)) + if (pushModel) { + hilog.info(DOMAIN, 'RemoteNotification', 'info: %{public}s', pushModel.body); + await TradeDailyPushDataManager.getInstance().recordPushDaily(pushModel) + } + } catch (e) { + hilog.error(DOMAIN, 'RemoteNotification', 'error: %{public}s', e); + } + + await Promise.resolve(); + return {}; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/entryability/WantManager.ets b/future_entry_demo/src/main/ets/entryability/WantManager.ets new file mode 100644 index 0000000..ff575d5 --- /dev/null +++ b/future_entry_demo/src/main/ets/entryability/WantManager.ets @@ -0,0 +1,219 @@ +import Want from '@ohos.app.ability.Want'; +import { Queue } from '@kit.ArkTS'; +import { ClipUtils, GlobalContext, HXLog, jumpToWebPage } from 'biz_common'; +import { AppPushMessage, DoPushCbas, PushConstants, PushMessageManager } from 'service_push'; +import { ShareManager } from '@b2c-spi/hx_share_api/src/main/ets/manager/ShareManager'; +import { ELK_LEVEL, sendElk } from 'service_monitor'; + +const TAG: string = 'WantCacheManager' +const BACK_ACTION: string = 'ohos.want.action.viewData' +const BACK_ENTITY: string = 'entity.system.browsable' + + +export const FIRST_PAGE_ONAPPEAR_EVENT : string = 'FirstPage OnAppear' +/** + * WantCacheManager 类用于管理 Want 对象的缓存和处理。 + * 采用单例模式,确保整个应用中只有一个实例。 + * 该管理器会缓存接收到的 Want 对象,并在合适的时机进行处理。 + */ +export class WantCacheManager { + // 单例模式下的唯一实例 + private static instance: WantCacheManager + + private pushMessageManager : PushMessageManager = PushMessageManager.getInstance(); + private clipUtils: ClipUtils = ClipUtils.getInstance() + + /** + * 获取 WantCacheManager 的单例实例。 + * 如果实例不存在,则创建一个新实例。 + * @returns WantCacheManager 的单例实例 + */ + public static getInstance(): WantCacheManager { + if (!WantCacheManager.instance) { + WantCacheManager.instance = new WantCacheManager() + } + return WantCacheManager.instance + } + + // 标志位,指示是否可以处理缓存的 Want 对象 + private enabled: boolean = false + /** + * 首页加载完成的回调函数。 + * 当首页加载完成后,将 enabled 标志位设置为 true,并尝试处理缓存的 Want 对象。 + */ + private firstPageFinishedCb = () => { + this.enabled = true + this.popWant() + } + + /** + * 私有构造函数,确保只能通过 getInstance 方法获取实例。 + * 注册首页加载完成的事件监听器。 + */ + private constructor() { + // 监听首页加载完成事件,当事件触发时调用 firstPageFinishedCb 回调函数 + GlobalContext.get().eventHub.on(FIRST_PAGE_ONAPPEAR_EVENT, this.firstPageFinishedCb) + } + + // 存储 Want 对象的队列 + private wantQueue: Queue = new Queue() + + /** + * 存储 Want 对象到队列中。 + * 如果 enabled 标志位为 true,则尝试处理队列中的 Want 对象。 + * @param want 要存储的 Want 对象 + */ + stashWant(want: Want) { + // 将 Want 对象添加到队列中 + this.wantQueue.add(want) + // 记录日志,提示接收到新的 Want 对象 + HXLog.i(TAG, ' New Want received, want.param : ') + // 如果可以处理 Want 对象,则尝试处理队列中的元素 + if (this.enabled) { + this.popWant() + } + } + + /** + * 从队列中取出并处理一个 Want 对象。 + * 如果队列不为空,则调用不同业务方进行处理,业务方处理保证不更改原有对象 + */ + private popWant() { + if (this.wantQueue.length > 0) { + const w = this.wantQueue.pop() + + // push协议 + this.handlePayload(w) + + // 微信分享 + this.handleWeChat(w) + + // 浏览器回流 + this.handleBackWash(w) + + // appLinking回流 + this.handleBackWashByBrowsable(w) + + this.handleReflow(w) + } + + } + + /** + * 处理推送协议对象。 + * 根据want中解析到的推送协议的来源,获取相应的策略并处理。 + * @param want 要处理的want + */ + private handlePayload(want : Want) { + if (want?.action === 'com.push.action') { + const param = want?.parameters + if (param) { + const payloadJson = param['payload'] as string + HXLog.i(PushConstants.AM_PUSH, payloadJson) + const payload = JSON.parse(payloadJson) as AppPushMessage + DoPushCbas.pushArriveCbas(payload) + DoPushCbas.pushShowCbas(payload) + this.pushMessageManager.handlerPush(payload) + } + } + } + /** + * 处理wechat分享协议对象。 + * 根据want中解析到的分享协议,进行处理 + * @param want 要处理的want + */ + private handleWeChat(want: Want) { + // 判断service是否有效 + const shareService = ShareManager.getShareService() + if (!shareService) { + HXLog.e("HXShare", 'shareService is null'); + return + } + shareService.getWXApi().handleWant(want, ShareManager.getShareService()?.getWXEventHandler()) + } + + + /** + * 端外跳转至期货通 + */ + private async handleBackWash(want: Want) { + if (want.action === BACK_ACTION && want.uri && want.entities?.length === 0) { + const params = this.extractUrlParams(want.uri) + if (params['url']) { + jumpToWebPage(params) + } + await this.dealPasteboard() + } + } + + + private async dealPasteboard(){ + if (this.clipUtils.getClipBoardSwitchState()) { + // 请求剪切板权限后再读取 + const hasPermission = await this.clipUtils.checkAndRequestPermission() + if (hasPermission) { + const clipboardContent = this.clipUtils.getClipboardContent() + if (this.clipUtils.isValidClipPassword(clipboardContent)) { + this.clipUtils.parseClipboard(clipboardContent) + } + } + } + } + + + + + /** + * 浏览器跳转至期货通 + */ + private handleBackWashByBrowsable(want: Want) { + if (want.action === BACK_ACTION && want.uri && want.entities && + want.entities[0] === BACK_ENTITY) { + const params = this.extractUrlParams(want.uri) + if (params['url']) { + jumpToWebPage(params) + } + this.dealPasteboard() + } + } + + /** + * 处理回流埋点。 + * @param want 要处理的want + */ + private handleReflow(want: Want) { + if (want.parameters) { + const callerBundleName = want.parameters['ohos.aafwk.param.callerBundleName'] as string + if (callerBundleName === 'com.hexin.hmn.sjcg') { + // 手抄跳转 + sendElk(TAG, `callerBundleName ${callerBundleName}`, ELK_LEVEL.INFO, true) + } + } + } + + private extractUrlParams(input: string): Record { + const result: Record = {}; + // 提取所有形如 key=value 的参数(以 &、^ 或字符串结尾为边界) + const matches = Array.from(input.matchAll(/([a-zA-Z_][a-zA-Z0-9_]*)=([^&\^]*)/gi)); + if (matches.length === 0) { + return result; + } + + // 迭代解码(兼容含 & 的转义值 %26) + matches.forEach(match => { + const key = match[1]; + let decoded = match[2]; + let prev = ''; + do { + prev = decoded; + try { + decoded = decodeURIComponent(decoded); + } catch { + break; // 非法编码终止 + } + } while (decoded !== prev && /%[0-9A-Fa-f]{2}/.test(decoded)); + result[key] = decoded; + }); + return result; + } +} diff --git a/future_entry_demo/src/main/ets/font/FontManager.ets b/future_entry_demo/src/main/ets/font/FontManager.ets new file mode 100644 index 0000000..1e9e272 --- /dev/null +++ b/future_entry_demo/src/main/ets/font/FontManager.ets @@ -0,0 +1,54 @@ +import { TextMetricsMeasurer } from "@b2b/hq_table"; +import { text } from "@kit.ArkGraphics2D"; +import { HXLog } from "biz_common"; + +/** + * author:liuqingliang + * created on 2025/12/19 + * desc:全局字体管理 + */ +export class FontManager { + private static readonly TAG = "FontManager" + private static instance: FontManager + + public static getInstance(): FontManager { + if (!FontManager.instance) { + FontManager.instance = new FontManager() + } + return FontManager.instance + } + + registerFont(uiContext: UIContext) { + try { + let font = uiContext.getFont(); + font.registerFont({ + familyName: 'bold', + familySrc: $rawfile('fonts/THSJinRongTi_bold.otf') + }); + font.registerFont({ + familyName: 'Regular', + familySrc: $rawfile('fonts/THSJinRongTi_Regular.otf') + }); + font.registerFont({ + familyName: 'Medium', + familySrc: $rawfile('fonts/THSJinRongTi_Medium.otf') + }); + + TextMetricsMeasurer.getInstance().init() + } catch (e) { + HXLog.e(FontManager.TAG, "registerFont failed:" + JSON.stringify(e)) + } + + } + + private loadFontSync(fontFamily: 'bold' | 'Regular' | 'Medium') { + let fontCollection = text.FontCollection.getGlobalInstance() + fontCollection.loadFontSync(fontFamily, $rawfile(`fonts/THSJinRongTi_${fontFamily}.otf`)) + } + + loadTHSFonts() { + this.loadFontSync('bold'); + this.loadFontSync('Regular'); + this.loadFontSync('Medium'); + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/functionauthority/FunctionAuthorityRequester.ets b/future_entry_demo/src/main/ets/functionauthority/FunctionAuthorityRequester.ets new file mode 100644 index 0000000..6d48255 --- /dev/null +++ b/future_entry_demo/src/main/ets/functionauthority/FunctionAuthorityRequester.ets @@ -0,0 +1,114 @@ +import { + CookieManager, + FunctionAuthorityRequester, + HxServiceExportConstants, + HXUserService, + ProductAuthorityManager +} from 'biz_hxservice' +import { AppConfigManager } from 'biz_common' +import { http } from '@kit.NetworkKit' +import { JSON } from '@kit.ArkTS' +import { UnifyUrlManager } from '@b2c-f/url-modify-config' +import emitter from '@ohos.events.emitter' + +interface EmptyFunctionAuthorityResponse { + code: number + data: Object[] +} + +class AppFunctionAuthorityRequester implements FunctionAuthorityRequester { + + async requestAuthorityData(): Promise { + if (HXUserService.getInstance().isTemporaryUser()) { + const response: EmptyFunctionAuthorityResponse = { code: 0, data: [] } + return response + } + + const httpRequest = http.createHttp() + try { + const requestOptions: http.HttpRequestOptions = { + method: http.RequestMethod.GET, + header: this.buildHeaders(), + connectTimeout: 30000, + readTimeout: 30000, + expectDataType: http.HttpDataType.STRING + } + const response = await httpRequest.request(UnifyUrlManager.getRealUrl('custom_authority_url'), requestOptions) + if (response.responseCode !== http.ResponseCode.OK) { + throw new Error(`http error: ${response.responseCode}`) + } + const parseOptions: JSON.ParseOptions = { + bigIntMode: JSON.BigIntMode.PARSE_AS_BIGINT + } + return JSON.parse(response.result as string, null, parseOptions) as object + } finally { + httpRequest.destroy() + } + } + + private buildHeaders(): Record { + const headers: Record = {} + headers['Content-Type'] = 'application/json' + const cookie = CookieManager.getInstance().getCookieSync() + if (cookie) { + headers['Cookie'] = cookie + } + return headers + } +} + +export class FunctionAuthorityRequestManager { + private static initialized: boolean = false + private static cookieUpdateListener?: () => void + private static requestingAfterCookieUpdate: boolean = false + private static userId: string = '' + + static loadAuthorityFromLocal(): boolean { + FunctionAuthorityRequestManager.ensureInitialized() + return ProductAuthorityManager.getInstance().loadAuthorityFromLocal() + } + + static requestAuthorityDataAfterLocalLoad(): Promise { + FunctionAuthorityRequestManager.loadAuthorityFromLocal() + return FunctionAuthorityRequestManager.requestAuthorityData() + } + + static requestAuthorityData(): Promise { + FunctionAuthorityRequestManager.ensureInitialized() + return ProductAuthorityManager.getInstance().requestAuthorityData() + } + + static requestAuthorityDataForPush(pushTimestamp: number): Promise { + FunctionAuthorityRequestManager.ensureInitialized() + return ProductAuthorityManager.getInstance().requestAuthorityDataForPush(pushTimestamp) + } + + private static ensureInitialized(): void { + if (FunctionAuthorityRequestManager.initialized) { + return + } + ProductAuthorityManager.getInstance().setRequester(new AppFunctionAuthorityRequester()) + FunctionAuthorityRequestManager.registerCookieUpdateListener() + FunctionAuthorityRequestManager.initialized = true + } + + private static registerCookieUpdateListener(): void { + if (FunctionAuthorityRequestManager.cookieUpdateListener) { + return + } + FunctionAuthorityRequestManager.cookieUpdateListener = () => { + const newUserId = HXUserService.getInstance().getUserIdFromLocal() + if (FunctionAuthorityRequestManager.requestingAfterCookieUpdate || + FunctionAuthorityRequestManager.userId == newUserId) { + return + } + FunctionAuthorityRequestManager.userId = newUserId + FunctionAuthorityRequestManager.requestingAfterCookieUpdate = true + FunctionAuthorityRequestManager.requestAuthorityDataAfterLocalLoad() + .finally(() => { + FunctionAuthorityRequestManager.requestingAfterCookieUpdate = false + }) + } + emitter.on(HxServiceExportConstants.COOKIE_UPDATE, FunctionAuthorityRequestManager.cookieUpdateListener) + } +} diff --git a/future_entry_demo/src/main/ets/groupsub/RealDataStructObserver.ets b/future_entry_demo/src/main/ets/groupsub/RealDataStructObserver.ets new file mode 100644 index 0000000..1376c2b --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/RealDataStructObserver.ets @@ -0,0 +1,68 @@ +import { StringUtils } from "@b2c-m/ohos_hx_utils"; +import { IStructReceiver, ProtocolDef, StuffBaseStruct, StuffResourceStruct } from '@kernel/lib_communication'; +import { GroupSubManager, L2DowngradeManager } from 'base_mobilepush'; +import { HashMap } from '@kit.ArkTS'; +import { emitter } from '@kit.BasicServicesKit'; +import { EmitterConstants } from 'biz_common' +import { SubscribedPushManager } from "biz_trade"; + +export class RealDataStructObserver implements IStructReceiver { + constructor() { + } + + onIntercept(stuffBaseStruct: StuffBaseStruct): boolean { + if (stuffBaseStruct == null) { + return false; + } + const headId = stuffBaseStruct.packageId; + if (headId == ProtocolDef.DATAID_NEW_REALTIME) { + const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK; + if (dataType === ProtocolDef.DATA_TYPE_JSON) { + return true; + } + } + return false; + } + + handleStruct(stuffBaseStruct: StuffBaseStruct): boolean { + if (!(stuffBaseStruct instanceof StuffResourceStruct)) { + return false; + } + const resStruct = stuffBaseStruct as StuffResourceStruct; + const tmpBuffer = resStruct.buffer; + const decodedStr = StringUtils.decode(tmpBuffer, "GBK") + try { + const jsonObject: ESObject = JSON.parse(decodedStr); + //解析到分组订阅数据 + const parsedObj: ESObject = jsonObject["groupdata"]; + if (parsedObj) { + GroupSubManager.getInstance().onReceiveGroupData(JSON.stringify(parsedObj), true); + return true; + } + + // 解析到Push订阅数据 + if (jsonObject[SubscribedPushManager.PARAM_CHANNEL]) { + SubscribedPushManager.getInstance().onReceivePushData(decodedStr) + } + + const parsedEvents: HashMap[] = jsonObject["events"] + if (parsedEvents && parsedEvents.length > 0) { + for (let event of parsedEvents) { + if (event["eventID"] == "00000011") { + L2DowngradeManager.getInstance().onReceiveGroupData(JSON.stringify(event)); + emitter.emit({ eventId: EmitterConstants.EVENT_ID_GET_PERMISSIONSID }) + return true; + } + } + return false + } + } catch (e) { + return false; + } + return false; + } + + getReceiverId(): string { + return "RealDataStructObserver"; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/AIGroupPushHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/AIGroupPushHandler.ets new file mode 100644 index 0000000..8b167b4 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/AIGroupPushHandler.ets @@ -0,0 +1,25 @@ +import { ISubDataHandler } from 'base_mobilepush' +import { AIGroupClient, AI_GROUP_BIZ_NAME } from 'service_aigroup' + +/** + * AI分组的数据分组订阅处理器 + * 参考 iOS LJAIGroupManager 中的 MobiPush 注册机制 + * ctrlid = future_aigroupconfig + */ +export class AIGroupPushHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = AI_GROUP_BIZ_NAME + + onReceiveGroupData(data: string): void { + try { + const pushData: AIGroupPushData = JSON.parse(data) as AIGroupPushData + AIGroupClient.getInstance().onAIGroupPushNotification(pushData.ver) + } catch (e) { + // 解析失败时也触发更新,不传版本号 + AIGroupClient.getInstance().onAIGroupPushNotification() + } + } +} + +interface AIGroupPushData { + ver?: number +} diff --git a/future_entry_demo/src/main/ets/groupsub/business/AccountAnalysisLuaPushHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/AccountAnalysisLuaPushHandler.ets new file mode 100644 index 0000000..167ee1f --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/AccountAnalysisLuaPushHandler.ets @@ -0,0 +1,86 @@ +import { ISubDataHandler } from 'base_mobilepush' +import { GlobalContext, HXLog } from 'biz_common' +import { ScriptUpdateService } from '@b2c-f/billparsemodule' + +/** + * 账户分析 Lua 脚本更新推送处理器 + * + * 处理来自服务端的账户分析 Lua 脚本版本更新通知 + */ +export class AccountAnalysisLuaPushHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = "qhtCapitalLuaUpdate" + private static readonly TAG = "AccountAnalysisLuaPush" + + onReceiveGroupData(data: string): void { + HXLog.i(AccountAnalysisLuaPushHandler.TAG, `receive: ${data}`) + + try { + const pushData: AccountAnalysisLuaPushData = JSON.parse(data) as AccountAnalysisLuaPushData + + if (pushData && pushData.AccountAnalysisLua) { + const version = pushData.AccountAnalysisLua.version + if (version) { + HXLog.i(AccountAnalysisLuaPushHandler.TAG, `收到 Lua 脚本版本更新通知: version=${version}`) + this.handleScriptUpdate(version) + } else { + HXLog.w(AccountAnalysisLuaPushHandler.TAG, '版本号为空') + } + } else { + HXLog.w(AccountAnalysisLuaPushHandler.TAG, '推送数据格式错误') + } + } catch (error) { + HXLog.e(AccountAnalysisLuaPushHandler.TAG, `解析推送数据失败: ${error}`) + } + } + + /** + * 处理脚本更新 + */ + private handleScriptUpdate(version: string): void { + try { + const context = GlobalContext.get() + if (!context) { + HXLog.e(AccountAnalysisLuaPushHandler.TAG, 'Context 为空,无法更新脚本') + return + } + + const updateService = new ScriptUpdateService(context) + + // 异步执行更新操作 + updateService.downloadAndInstall(version) + .then((result) => { + if (result.success) { + if (result.updated) { + HXLog.i(AccountAnalysisLuaPushHandler.TAG, + `Lua 脚本更新成功: version=${result.version}, scriptPath=${result.scriptPath}`) + } else { + HXLog.i(AccountAnalysisLuaPushHandler.TAG, + `Lua 脚本已是最新版本: version=${result.version}`) + } + } else { + HXLog.e(AccountAnalysisLuaPushHandler.TAG, + `Lua 脚本更新失败: ${result.errorMessage}`) + } + }) + .catch((error: Error) => { + HXLog.e(AccountAnalysisLuaPushHandler.TAG, `Lua 脚本更新异常: ${error.message}`) + }) + } catch (error) { + HXLog.e(AccountAnalysisLuaPushHandler.TAG, `处理脚本更新失败: ${error}`) + } + } +} + +/** + * 账户分析 Lua 脚本版本信息 + */ +export interface AccountAnalysisLuaVersion { + version: string +} + +/** + * 账户分析 Lua 脚本推送数据格式 + */ +export interface AccountAnalysisLuaPushData { + AccountAnalysisLua: AccountAnalysisLuaVersion +} diff --git a/future_entry_demo/src/main/ets/groupsub/business/BaseStorageHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/BaseStorageHandler.ets new file mode 100644 index 0000000..e6b4369 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/BaseStorageHandler.ets @@ -0,0 +1,78 @@ +import { RandomUtil } from "@pura/harmony-utils"; +import { ISubDataHandler } from "base_mobilepush"; +import { HXLog } from "biz_common"; +import { StringUtil } from "biz_common/src/main/ets/utils/StringUtil"; +import { HexinVersionControl } from "biz_trade/src/main/ets/utils/HexinVersionControl"; +import { CodeTableManager } from "lib_codetablemanager"; + +export abstract class BaseStorageHandler implements ISubDataHandler { + /** + * 获取日志tag + */ + abstract getTag(): string + + /** + * 获取推送中的数据更新时间key + */ + abstract getTimeKey(): string + + /** + * 处理升级 + */ + abstract update() + + /** + * 获取数据库版本信息 + */ + abstract getDbVersion(): Promise + + private readonly RANDOM_REQUEST_TIME = 10 * 1000 + + private mTimeoutId: number | null = null + + async onReceiveGroupData(data: string) { + if (HexinVersionControl.disableCodeTableIncrementUpdate()) { + return + } + HXLog.d(CodeTableManager.TAG, `${this.getTag()} onReveiveGroupData: ${data}`) + const obj = JSON.parse(data) as Record + if (!obj) { + HXLog.e(CodeTableManager.TAG, `${this.getTag()} onReceiveGroupData obj parse invalid`) + return + } + const time: string = obj[this.getTimeKey()] + const dbVersion = await this.getDbVersion() + const remoteVersion = StringUtil.toSafeNumber(time, 0) + HXLog.d(CodeTableManager.TAG, `${this.getTag()} remoteVersion: ${remoteVersion}, dbVersion: ${dbVersion}`) + + if (StringUtil.isNumber(time) && remoteVersion > dbVersion) { + this.randomDelayRequest(remoteVersion) + } + } + + /** + * 随机延时请求 + */ + private randomDelayRequest(remoteVersion: number) { + // 取消之前的延迟任务 + if (this.mTimeoutId !== null) { + clearTimeout(this.mTimeoutId) + this.mTimeoutId = null + } + + const requestDelay: number = RandomUtil.getRandomInt(0, this.RANDOM_REQUEST_TIME) + console.debug(CodeTableManager.TAG, `${this.getTag()} requestDelay ${requestDelay}`) + + // 设置延迟任务 + this.mTimeoutId = setTimeout(() => { + this.mTimeoutId = null + // 延迟的这段时间,数据库可能被更新了,因此执行前再判断一次,减少不必要的请求 + this.getDbVersion().then((dbVersion: number) => { + HXLog.d(CodeTableManager.TAG, `before update, remoteVersion: ${remoteVersion}, dbVersion: ${dbVersion}`) + if (remoteVersion > dbVersion) { + this.update() + } + }) + }, requestDelay) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/FunctionAuthorityGroupHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/FunctionAuthorityGroupHandler.ets new file mode 100644 index 0000000..3f1ef07 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/FunctionAuthorityGroupHandler.ets @@ -0,0 +1,31 @@ +import { ISubDataHandler } from 'base_mobilepush' +import { FunctionAuthorityRequestManager } from '../../functionauthority/FunctionAuthorityRequester' + +interface FunctionAuthorityGroupData { + timestamp?: string | number + timeStamp?: string | number +} + +export class FunctionAuthorityGroupHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = 'futures_custom_authority' + + onReceiveGroupData(data: string, key?: string, isRealPush?: boolean): void { + const timestamp = this.parseTimestamp(data) + if (timestamp <= 0) { + return + } + FunctionAuthorityRequestManager.requestAuthorityDataForPush(timestamp) + } + + private parseTimestamp(data: string | object): number { + try { + const payload = typeof data === 'string' + ? JSON.parse(data) as FunctionAuthorityGroupData + : data as FunctionAuthorityGroupData + return Number(payload.timestamp ?? payload.timeStamp ?? 0) + } catch (error) { + console.error(`[FunctionAuthorityGroupHandler] 解析权限推送失败: ${JSON.stringify(error)}`) + return 0 + } + } +} diff --git a/future_entry_demo/src/main/ets/groupsub/business/GrayTestPushHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/GrayTestPushHandler.ets new file mode 100644 index 0000000..ddf6833 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/GrayTestPushHandler.ets @@ -0,0 +1,19 @@ +import { ISubDataHandler } from 'base_mobilepush' +import { HXLog } from 'biz_common' +import { GrayTestManager } from 'lib_graytestmanager' + +export class GrayTestPushHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = "futures_new_gray_test_config_upgrade_notice" + + onReceiveGroupData(data: string): void { + HXLog.i("GrayTest", `GrayTestPushHandler receive: ${data}`) + let verTimestamp: PushTimesTamp = JSON.parse(data) as PushTimesTamp + if (verTimestamp && verTimestamp.timestamp > 0) { + GrayTestManager.getInstance().dealReceiveMobilePush(verTimestamp.timestamp) + } + } +} + +export interface PushTimesTamp { + timestamp: number +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPagePushHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPagePushHandler.ets new file mode 100644 index 0000000..11ad41e --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPagePushHandler.ets @@ -0,0 +1,28 @@ +import { AdsPopupManager } from "@b2c-f/fuhm-ads" +import { AdsPopupHost } from "@b2c/first_page" +import { ISubDataHandler } from "base_mobilepush" +import { HXLog } from "biz_common" + +/** + * author : liuqingliang@myhexin.com + * time : created on 2026/6/9 + * desc : 全域运营位-只会推送一次,不会实时推送 + */ +export class QhStrategyAdPagePushHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = 'app_page_info' + private static readonly KEY_TIME = "app_6" + + onReceiveGroupData(data: string) { + try { + const pushData = JSON.parse(data) as Record + const timeStamp = pushData[QhStrategyAdPagePushHandler.KEY_TIME] ?? '' + + if (timeStamp === AdsPopupManager.pushTimestamp) { + return + } + AdsPopupHost.getInstance().requestPageInfo(timeStamp) + } catch (error) { + HXLog.e('QhStrategyAdPushHandler', `Failed to process push: ${error.message}`) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPushHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPushHandler.ets new file mode 100644 index 0000000..664b4ac --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/QhStrategyAdPushHandler.ets @@ -0,0 +1,59 @@ +import { AdsManager, ADS_TYPE_INDEX_POPUP, ADS_TYPE_QHTMKTPOP } from "@b2c-f/fuhm-ads" +import { AdsPopupHost, FirstPagePopupAdHelper } from "@b2c/first_page" +import { ISubDataHandler } from "base_mobilepush" +import { HXLog, PreferenceService } from "biz_common" +import { HXUserService } from "biz_hxservice" + + +/** + *
+ * author: ME
+ * desc: 期货策略广告推送处理器,对齐 Android QhStragyAdPushHandler
+ * createTime: 2026/06/01
+ * 
+ */ +export class QhStrategyAdPushHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = 'qh_strategy_ad_push' + private static readonly PUSH_STRATEGY_AD_KEY = 'push_stragy_ad' + private static readonly SP_STRATEGY_AD = "sp_stragy_ad" + private static readonly KEY_AD_TYPE = "adtype" + private static readonly KEY_AD_IDS = "id" + + onReceiveGroupData(data: string) { + try { + const pushData = JSON.parse(data) as Record + const adType = pushData[QhStrategyAdPushHandler.KEY_AD_TYPE] ?? '' + const adId = pushData[QhStrategyAdPushHandler.KEY_AD_IDS] ?? '' + + if (!adType || !adId) { + HXLog.w('QhStrategyAdPushHandler', 'Invalid push data, missing adType or adId') + return + } + + const pushKey = `${adType}_${adId}` + const userId = HXUserService.getInstance().getUserIdFromLocal() + + const preferenceKey = `${QhStrategyAdPushHandler.PUSH_STRATEGY_AD_KEY}_${userId}` + const localPushKey = PreferenceService.getStringValueSync(QhStrategyAdPushHandler.SP_STRATEGY_AD,preferenceKey, '') + if (pushKey === localPushKey) { + HXLog.d('QhStrategyAdPushHandler', `Already pushed: ${pushKey}`) + return + } + PreferenceService.putVal(QhStrategyAdPushHandler.SP_STRATEGY_AD,preferenceKey, pushKey, true) + + if (adType === ADS_TYPE_INDEX_POPUP) { + // 收到策略广告后重置启动弹窗过的标志 + FirstPagePopupAdHelper.getInstance().isShowedPopupAds = false + } + + if (adType === ADS_TYPE_QHTMKTPOP) { + // 期货营销悬浮球-处理策略广告推送 + AdsPopupHost.getInstance().handlePush() + } + + AdsManager.refreshAdsAfterPushByAdType(adType, adId) + } catch (error) { + HXLog.e('QhStrategyAdPushHandler', `Failed to process push: ${error.message}`) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/SelfCodeGroupHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/SelfCodeGroupHandler.ets new file mode 100644 index 0000000..6c0ee64 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/SelfCodeGroupHandler.ets @@ -0,0 +1,19 @@ +import { ISubDataHandler } from 'base_mobilepush'; +import { UgcClient } from '../../register/ugcregister/UgcClient'; + +/** + * 自选分组的数据分组订阅 + * @author menghuiyuan@myhexin.com + */ +export class SelfCodeGroupHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = "blockstock" + onReceiveGroupData(data: string): void { + let version:groupUpdate = JSON.parse(data) as groupUpdate + UgcClient.getInstance().onSelfGroupRemoteVersion(version.ver) + } + +} + +export interface groupUpdate{ + ver:number +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/SelfCodeMainListHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/SelfCodeMainListHandler.ets new file mode 100644 index 0000000..0ef2133 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/SelfCodeMainListHandler.ets @@ -0,0 +1,14 @@ +import { ISubDataHandler } from 'base_mobilepush'; +import { UgcClient } from '../../register/ugcregister/UgcClient'; +import { groupUpdate } from './SelfCodeGroupHandler'; +/** + * 默认自选数据分组订阅 + * @author zhangshan2@myhexin.com + */ +export class SelfCodeMainListHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = "futuresselfcode" + onReceiveGroupData(data: string): void { + let version:groupUpdate = JSON.parse(data) as groupUpdate + UgcClient.getInstance().onSelfCodeRemoteVersion(version.ver) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/groupsub/business/SidGroupHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/SidGroupHandler.ets new file mode 100644 index 0000000..e40c93b --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/SidGroupHandler.ets @@ -0,0 +1,70 @@ +import { ISubDataHandler } from 'base_mobilepush' +import { WxPaySidPermissionMarker } from 'biz_auth' +import { HXLog, PreferenceService } from 'biz_common' +import { SidPermissionRefreshClient } from 'biz_auth/src/main/ets/auth/sid/SidPermissionRefreshClient' +import { HXUserService } from 'biz_hxservice' + +const TAG = 'SidGroupHandler' + +/** + * SID权限更新分组推送数据。 + * 对应分组key: futures_app_sid_update_notice + * timeStamp为服务端下发的更新标记,用于和本地已处理标记比较,变化时主动刷新SID权限。 + */ +interface SidGroupData { + timeStamp?: string | number +} + +export class SidGroupHandler implements ISubDataHandler { + static readonly GROUP_HANDLE_NAME = 'futures_app_sid_update_notice' + private static readonly PREFERENCES_NAME = 'sid_update_notice' + private static readonly KEY_PREFIX = 'sid_update_' + + onReceiveGroupData(data: string): void { + try { + // 主站只用timeStamp表达“SID权限有新版本”,客户端不解析具体权限内容。 + const flag = this.parsePushFlag(data) + if (!flag || flag.length === 0) { + return + } + const userId = HXUserService.getInstance().getUserId() + // 本地仅记录已成功处理的timeStamp,避免同一条分组推送重复触发主动请求。 + const savedFlag = PreferenceService.getStringValueSync( + SidGroupHandler.PREFERENCES_NAME, + SidGroupHandler.KEY_PREFIX + userId, + '' + ) + if (flag === savedFlag) { + return + } + // 主动请求成功且SID发生变化后,SidPermissionRefreshClient会写入本次flag。 + WxPaySidPermissionMarker.clear() + SidPermissionRefreshClient.requestPermission(true, flag) + } catch (error) { + HXLog.e(TAG, `handle sid group push failed: ${JSON.stringify(error)}`) + } + } + + private parsePushFlag(data: string | object): string { + if (!data) { + return '' + } + let payload: SidGroupData | null = null + if (typeof data === 'string') { + try { + payload = JSON.parse(data) as SidGroupData + } catch (error) { + HXLog.e(TAG, `parse sid group push failed: ${JSON.stringify(error)}`) + return '' + } + } else { + // 分组分发链路可能已把payload解析为对象,这里兼容两种输入形态。 + payload = data as SidGroupData + } + const flag = payload?.timeStamp + if (flag === undefined || flag === null) { + return '' + } + return flag.toString() + } +} diff --git a/future_entry_demo/src/main/ets/groupsub/business/StockGroupHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/StockGroupHandler.ets new file mode 100644 index 0000000..2c5a169 --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/StockGroupHandler.ets @@ -0,0 +1,25 @@ +import { AppUtil } from "@pura/harmony-utils"; +import { HXLog } from "biz_common"; +import { CodeTableManager } from "lib_codetablemanager"; +import { BaseStorageHandler } from "./BaseStorageHandler"; + +export class StockGroupHandler extends BaseStorageHandler { + getTag(): string { + return 'StockGroupHandler' + } + + getTimeKey(): string { + return 'time' + } + + update(): void { + HXLog.d(CodeTableManager.TAG, `${this.getTag()} update`) + CodeTableManager.get().requestStock(AppUtil.getContext(), this.getTag()) + } + + async getDbVersion(): Promise { + const version = await CodeTableManager.get().getStockInfoDbVersion() + HXLog.d(CodeTableManager.TAG, `${this.getTag()} getDbVersion ${version}`) + return version + } +} diff --git a/future_entry_demo/src/main/ets/groupsub/business/VarietyGroupHandler.ets b/future_entry_demo/src/main/ets/groupsub/business/VarietyGroupHandler.ets new file mode 100644 index 0000000..bed658b --- /dev/null +++ b/future_entry_demo/src/main/ets/groupsub/business/VarietyGroupHandler.ets @@ -0,0 +1,26 @@ +import { AppUtil } from "@pura/harmony-utils"; +import { HXLog } from "biz_common"; +import { CodeTableManager } from "lib_codetablemanager"; +import { BaseStorageHandler } from "./BaseStorageHandler"; + +export class VarietyGroupHandler extends BaseStorageHandler { + getTag(): string { + return 'VarietyGroupHandler' + } + + getTimeKey(): string { + return 'timestamp' + } + + update(): void { + HXLog.d(CodeTableManager.TAG, `${this.getTag()} update`) + CodeTableManager.get().requestVariety(AppUtil.getContext(), this.getTag()) + } + + async getDbVersion(): Promise { + const version = await CodeTableManager.get().getVarietyInfoDbVersion() + HXLog.d(CodeTableManager.TAG, `${this.getTag()} getDbVersion ${version}`) + return version + } + +} diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_close.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_close.png new file mode 100644 index 0000000..a271cc9 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_close.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left.png new file mode 100644 index 0000000..5955463 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left_disable.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left_disable.png new file mode 100644 index 0000000..70348e4 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_left_disable.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right.png new file mode 100644 index 0000000..3f79ea3 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right_disable.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right_disable.png new file mode 100644 index 0000000..1f5346c Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_move_right_disable.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_open.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_open.png new file mode 100644 index 0000000..49a2591 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_open.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_switch_land.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_switch_land.png new file mode 100644 index 0000000..2614a7b Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_switch_land.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down.png new file mode 100644 index 0000000..b390b99 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down_disable.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down_disable.png new file mode 100644 index 0000000..ff46ec6 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_down_disable.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up.png new file mode 100644 index 0000000..9c3e48d Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up.png differ diff --git a/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up_disable.png b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up_disable.png new file mode 100644 index 0000000..4c4f535 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/curvezoomtoolbar_btn_zoom_up_disable.png differ diff --git a/future_entry_demo/src/main/ets/image/description_expand_down.png b/future_entry_demo/src/main/ets/image/description_expand_down.png new file mode 100644 index 0000000..ca4a430 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/description_expand_down.png differ diff --git a/future_entry_demo/src/main/ets/image/description_expand_down_night.png b/future_entry_demo/src/main/ets/image/description_expand_down_night.png new file mode 100644 index 0000000..22b2998 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/description_expand_down_night.png differ diff --git a/future_entry_demo/src/main/ets/image/description_expand_up.png b/future_entry_demo/src/main/ets/image/description_expand_up.png new file mode 100644 index 0000000..1ce82d6 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/description_expand_up.png differ diff --git a/future_entry_demo/src/main/ets/image/description_expand_up_night.png b/future_entry_demo/src/main/ets/image/description_expand_up_night.png new file mode 100644 index 0000000..0d0e274 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/description_expand_up_night.png differ diff --git a/future_entry_demo/src/main/ets/image/icon_down_arrow.png b/future_entry_demo/src/main/ets/image/icon_down_arrow.png new file mode 100644 index 0000000..1eaebca Binary files /dev/null and b/future_entry_demo/src/main/ets/image/icon_down_arrow.png differ diff --git a/future_entry_demo/src/main/ets/image/icon_up_arrow.png b/future_entry_demo/src/main/ets/image/icon_up_arrow.png new file mode 100644 index 0000000..5ba69da Binary files /dev/null and b/future_entry_demo/src/main/ets/image/icon_up_arrow.png differ diff --git a/future_entry_demo/src/main/ets/image/kline_bs_detail_arrow.png b/future_entry_demo/src/main/ets/image/kline_bs_detail_arrow.png new file mode 100644 index 0000000..609d060 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/kline_bs_detail_arrow.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down.png new file mode 100644 index 0000000..5a83a81 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down_night.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down_night.png new file mode 100644 index 0000000..1de316b Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_down_night.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up.png new file mode 100644 index 0000000..28d1fa2 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up_night.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up_night.png new file mode 100644 index 0000000..a01e0a4 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_up_night.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero.png new file mode 100644 index 0000000..d1ce94d Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero_night.png b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero_night.png new file mode 100644 index 0000000..88f5d8d Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_icon_aianalysis_zero_night.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_screen_conversion.png b/future_entry_demo/src/main/ets/image/theme_screen_conversion.png new file mode 100644 index 0000000..3f711d6 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_screen_conversion.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_screen_conversion_night.png b/future_entry_demo/src/main/ets/image/theme_screen_conversion_night.png new file mode 100644 index 0000000..c1136ea Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_screen_conversion_night.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_side_edge_expand.png b/future_entry_demo/src/main/ets/image/theme_side_edge_expand.png new file mode 100644 index 0000000..1ccc94b Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_side_edge_expand.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_side_edge_expand_night.png b/future_entry_demo/src/main/ets/image/theme_side_edge_expand_night.png new file mode 100644 index 0000000..380e2a5 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_side_edge_expand_night.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_side_edge_shrink.png b/future_entry_demo/src/main/ets/image/theme_side_edge_shrink.png new file mode 100644 index 0000000..73b9401 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_side_edge_shrink.png differ diff --git a/future_entry_demo/src/main/ets/image/theme_side_edge_shrink_night.png b/future_entry_demo/src/main/ets/image/theme_side_edge_shrink_night.png new file mode 100644 index 0000000..fe40371 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/theme_side_edge_shrink_night.png differ diff --git a/future_entry_demo/src/main/ets/image/timescale_horizontal_quick.png b/future_entry_demo/src/main/ets/image/timescale_horizontal_quick.png new file mode 100644 index 0000000..c5aa890 Binary files /dev/null and b/future_entry_demo/src/main/ets/image/timescale_horizontal_quick.png differ diff --git a/future_entry_demo/src/main/ets/initialize/AdsInitializor.ets b/future_entry_demo/src/main/ets/initialize/AdsInitializor.ets new file mode 100644 index 0000000..a469139 --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/AdsInitializor.ets @@ -0,0 +1,103 @@ + +import { + AdsManager, + AdRuntimeConfig +} from '@b2c-f/fuhm-ads' +import { AppConfigManager, HXLog, ScreenUtil } from 'biz_common' +import { CookieManager, HXUserService, KaihuFlagManager, NewUserManager, ServerTimeProvider, + UserSourceManager } from 'biz_hxservice' +import { AppUtil } from '@pura/harmony-utils' + +const TAG = 'AdsInitializer' + +/** + * FuHmAds 运行时配置 + * 提供广告组件所需的上下文、用户信息和业务能力 + */ +class AdsRuntimeConfig implements AdRuntimeConfig { + + + getContext(): Object | null { + return AppUtil.getContext() + } + + getScreenWidth(): string { + return ScreenUtil.get().getScreenWidth().toString() + } + + getScreenHeight(): string { + return ScreenUtil.get().getScreenHeight().toString() + } + + getInnerVersion(): string { + return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionMiddleNumberStr()??'' + } + + getComeFrom(): string { + return KaihuFlagManager.getInstance().getKaihuFlag() + } + + getTrade(): string { + let userSource = UserSourceManager.getInstance().getUserSource() + if (UserSourceManager.NO_SOURCE != userSource) { + return userSource + }else { + return "" + } + } + + getDeviceCode(): string { + return '' + } + + getUserId(): string { + const userId = HXUserService.getInstance().getUserId() + return userId || '' + } + + getUserIdFromPassport(): string { + const userId = HXUserService.getInstance().getUserIdFromLocal() + return userId || '' + } + + isNewUser(): boolean { + return NewUserManager.getInstance().isNewUser() + } + + getChannelId(): string { + return NewUserManager.getChannelId() + } + + getCurrentTimeMillis(): number { + return ServerTimeProvider.getInstance().getServerTimeMillis() + } + + + getUserAgent(): string { + return AppConfigManager.getInstance().getUA() + } + + getCookie(): string { + return CookieManager.getInstance().getCookieSync() + } + + + uploadElk(message: string): void { + HXLog.w(TAG, `Ads elk ${message}`) + } +} + +/** + * 广告模块初始化器 + */ +export class AdsInitializer { + /** + * 初始化广告模块 + */ + static initAds(): void { + HXLog.i(TAG, 'initAds start') + const adsConfig = new AdsRuntimeConfig() + AdsManager.initAdContext(adsConfig) + HXLog.i(TAG, 'initAds done') + } +} diff --git a/future_entry_demo/src/main/ets/initialize/CloudStoreLogInitializor.ets b/future_entry_demo/src/main/ets/initialize/CloudStoreLogInitializor.ets new file mode 100644 index 0000000..b2cb4b3 --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/CloudStoreLogInitializor.ets @@ -0,0 +1,64 @@ +import { HXLog } from "biz_common" +import { CloudStoreLogManager, ICloudStoreLogger } from "lib_cloud_store" +import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from '@b2c-m/ohos_elk_logger_upload' +import { HXUserService } from 'biz_hxservice' + +const CLOUD_STORE = "cloud_store" + +/** + * 云存储错误消息构建类 + */ +class CloudStoreErrMessageBuilder extends ElkUploadMessageBuilder { + constructor(bizLevel: string) { + super(Date.now(), CLOUD_STORE, bizLevel) + } +} + +/** + * 上传云存储错误信息到 ELK + * @param errorMessage 错误信息 + */ +function uploadCloudStoreError(error: string) { + const errorMessageBuilder = new CloudStoreErrMessageBuilder('e') + errorMessageBuilder.userid(HXUserService.getInstance().getUserId()) + errorMessageBuilder.messageKey('CloudStore') + errorMessageBuilder.messageValue(error) + const message = new ElkUploadMessage(errorMessageBuilder) + ElkService.getInstance().pushBusinessLog(message, true) +} + +/** + * HXLog 适配器 + * 将 lib_cloud_store 的日志桥接到 HXLog + */ +class HXLogAdapter implements ICloudStoreLogger { + debug(module: string, message: string): void { + HXLog.d(module, message) + } + + info(module: string, message: string): void { + HXLog.i(module, message) + } + + warn(module: string, message: string): void { + HXLog.w(module, message) + } + + error(module: string, message: string): void { + HXLog.e(module, message) + uploadCloudStoreError(`[${module}] ${message}`) + } +} + +/** + * 云存储日志初始化器 + */ +export class CloudStoreLogInitializor { + /** + * 初始化云存储日志 + */ + static initCloudStoreLog() { + const hxLogAdapter = new HXLogAdapter() + CloudStoreLogManager.getInstance().setLogger(hxLogAdapter) + } +} diff --git a/future_entry_demo/src/main/ets/initialize/ConfigImpl.ets b/future_entry_demo/src/main/ets/initialize/ConfigImpl.ets new file mode 100644 index 0000000..3da1bec --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/ConfigImpl.ets @@ -0,0 +1,81 @@ +import {AppInfoConfig, IAppConfig, IDeviceConfig } from 'biz_common'; +export class AppConfigProvider implements IAppConfig { + + private static readonly BEGIN_INDEX = 5 + getBundleName(): string { + return AppInfoConfig.getBundleName(); + } + + + getInnerVersionFull(): string { + return AppInfoConfig.APPLET_INNER_VERSION_FULL; + } + + getInnerVersionMiddle(): string { + return AppInfoConfig.APPLET_INNER_VERSION.substring(AppConfigProvider.BEGIN_INDEX -1) + } + + getInnerVersionMiddleNumberStr(): string { + return AppInfoConfig.APPLET_INNER_VERSION.substring(AppConfigProvider.BEGIN_INDEX).replaceAll(".","") + } + + getOuterVersion(): string { + + return AppInfoConfig.getVersionName(); + } + + getVersionCode(): string { + return AppInfoConfig.getVersionCode(); + } + + getAppPlatform(): string { + return AppInfoConfig.APP_PLATFORM; + } + + getAppSign(): string { + return AppInfoConfig.APP_FOR; + } + + getProgramId(): string { + return AppInfoConfig.APP_PROGID; + } + + getChannelId(): string { + return AppInfoConfig.APP_CHANNEL_ID; + } + + getSourceId(): string { + return AppInfoConfig.APP_CHANNEL_ID; + } + +} + +export class DeviceConfigProvider implements IDeviceConfig { + getDeviceWidth(): number { + return 720; + } + + getDeviceHeight(): number { + return 1440; + } + + getDensity(): number { + return 3.0; + } + + getDeviceType(): string { + return "android"; + } + + getIMEI(): string { + throw new Error('Method not implemented.'); + } + + getIMSI(): string { + throw new Error('Method not implemented.'); + } + + getMACAddress(): string { + throw new Error('Method not implemented.'); + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/initialize/ConfigInitializor.ets b/future_entry_demo/src/main/ets/initialize/ConfigInitializor.ets new file mode 100644 index 0000000..5036a1b --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/ConfigInitializor.ets @@ -0,0 +1,13 @@ +import { AppConfigManager } from 'biz_common' +import { AppConfigProvider, DeviceConfigProvider } from './ConfigImpl'; + +/** + * 宿主App初始化配置信息 + */ +export class ConfigInitializer { + + static init() { + AppConfigManager.getInstance().registerConfigProvider(new AppConfigProvider()); + AppConfigManager.getInstance().registerDeviceConfigProvider(new DeviceConfigProvider()); + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/initialize/GrayTestInitializor.ets b/future_entry_demo/src/main/ets/initialize/GrayTestInitializor.ets new file mode 100644 index 0000000..84dbbc5 --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/GrayTestInitializor.ets @@ -0,0 +1,33 @@ +import { AppConfigManager, AppInfoConfig, } from "biz_common" +import { DebugToolUtil } from "biz_debug" +import { CookieManager, HXUserService, ServerTimeProvider } from "biz_hxservice" +import { GrayTestManager, GrayTestInjectInterface } from "lib_graytestmanager" + +export class GrayTestInitializor { + /** + * 初始化灰度配置 + */ + static initGrayTest() { + const injectParams = new GrayTestInjectInterface() + injectParams.getUserId = () => { + return HXUserService.getInstance().getUserIdFromLocal() + } + injectParams.getCookie = () => { + return CookieManager.getInstance().getCookieSync() + } + injectParams.getUserAgent = () => { + return AppConfigManager.getInstance().getUA() + } + injectParams.getAppVersion = () => { + return AppInfoConfig.getCurrentInnerVersionNumber().toString() + } + injectParams.getServerTimestamp = () => { + return ServerTimeProvider.getInstance().getServerTimeMillis() + } + injectParams.getDebugMode = () => { + return DebugToolUtil.isDebugMode() + } + + GrayTestManager.getInstance().config(injectParams) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/initialize/TradeInitializor.ets b/future_entry_demo/src/main/ets/initialize/TradeInitializor.ets new file mode 100644 index 0000000..0561e6b --- /dev/null +++ b/future_entry_demo/src/main/ets/initialize/TradeInitializor.ets @@ -0,0 +1,145 @@ +import { ContractChooseManager, ContractDirectionType, ContractInfo, ContractMainType, + FavoriteGroup } from "@b2c-f/contractchoose"; +import { ContractBeanFactory, ContractCode, MarketId, PositionCenter } from "@b2c-f/futures_ohos_trade_sdk"; +import { Context } from "@kit.AbilityKit"; +import { Markets } from "biz_common"; +import { InactiveContractManager } from "biz_market"; +import { AccountAnalysisTradeLoginListener, + AccountProvider, + FuturesAccountAnalysisHttpProvider, FuturesBillParseModuleHttpProvider, + TradeUtils, TradeReportDateManager } from "biz_trade"; +import { TradeDailyDataManager, TradeDailyManager, TradeHighFrequentManager, TradeLoginOnlineManager, + TradeMsgUniqueIdManager, TradeSettlementBillManager } from "biz_trade"; +import { TradeType } from "biz_trade/src/main/ets/trade/manager/TradeCustomTabManager"; +import { CodeTableManager, StockRegularUtils } from "lib_codetablemanager"; +import { loadCustomGroups } from "service_watchlist"; +import { TradeDailyConvertManager } from "../NotificationExtension/TradeDaily/TradeDailyConvertManager"; +import { TradeDailyPushDataManager } from "../NotificationExtension/TradeDaily/TradeDailyPushDataManager"; + +export class TradeInitializor { + /** + * 初始化高频交易管理器 + */ + static async init(context: Context) { + TradeInitializor.initContractChoose() + + TradeHighFrequentManager.getInstance().initManager(context) + TradeLoginOnlineManager.getInstance().initManager(context) + AccountAnalysisTradeLoginListener.getInstance().start() + TradeSettlementBillManager.getInstance() + FuturesAccountAnalysisHttpProvider.initializeHttpProvider() + FuturesBillParseModuleHttpProvider.initializeHttpProvider() + TradeSettlementBillManager.getInstance() + + // 交易日志 + TradeDailyManager.getInstance() + await TradeMsgUniqueIdManager.getInstance().initDB(context) + await TradeDailyDataManager.getInstance().init(context) + await TradeDailyPushDataManager.getInstance().initDB(context) + await TradeDailyConvertManager.getInstance().registerForeground(context) + TradeReportDateManager.getInstance().updateTradingDate() + } + + /** + * 初始化合约选择方法注入 + */ + static initContractChoose() { + ContractChooseManager.getInstance().setIsOptionContractProvider((showCode, marketId) => { + return StockRegularUtils.isOption(showCode) + }) + + ContractChooseManager.getInstance().setIsMainLinkContractProvider(async (showCode, marketId) => { + let contractBean = await ContractBeanFactory.getInstance() + .buildContract(new ContractCode(showCode), new MarketId(marketId)) + return TradeUtils.isCanTradeMainLink(contractBean) + }) + + ContractChooseManager.getInstance().setIsMainContractProvider(async (showCode, marketId) => { + return TradeUtils.isZhuliCode(showCode) + }) + + ContractChooseManager.getInstance().setPositionProvider(async () => { + const accountIdentify = AccountProvider.getInstance().getCurrentAccount()?.accountIdentify() + if (!accountIdentify) { + return [] + } + const positions = PositionCenter.getInstance().getAllPositionSummary(accountIdentify)?.getPositionInfos() + return positions?.flatMap((position) => { + const stockCode = CodeTableManager.get().getStockInfoByShowCodeSync(position.contractCode.code, position.marketId.code)?.stockCode ?? '' + return new ContractInfo( + position.contractCode.code, + position.marketId.code, + stockCode, + position.contractName, + position.isOptionPosition, + ContractMainType.NONE, + position.isLongPosition() ? ContractDirectionType.BUY : ContractDirectionType.SELL + ) + }) ?? [] + }) + + ContractChooseManager.getInstance().setFavoritesProvider(async () => { + const favorites = await TradeUtils.getSelfContractForTrade(TradeType.NONE) + return favorites.flatMap((favorite) => { + if (favorite.contractCode && favorite.marketId) { + const stockCode = CodeTableManager.get().getStockInfoByShowCodeSync(favorite.contractCode.code, favorite.marketId.code)?.stockCode ?? '' + return new ContractInfo( + favorite.contractCode.code, + favorite.marketId.code, + stockCode, + favorite.contractName ?? '', + favorite.isOptionContract(), + TradeUtils.isZhuliCode(stockCode) ? ContractMainType.MAIN : ContractMainType.NOT_MAIN, + ContractDirectionType.NONE, + InactiveContractManager.get().isInactiveContract(stockCode) && !Markets.isArbitrageMarket(favorite.marketId.code) + ) + } + return undefined + }) + }) + + ContractChooseManager.getInstance().setFavoritesGroupProvider(async () => { + const groups = loadCustomGroups(true) + const favoriteGroup = new FavoriteGroup() + for (const group of groups) { + const contracts = group.stocks + if (contracts.length > 0) { + favoriteGroup.addGroup(group.name, contracts.flatMap((contract) => { + const stockInfo = CodeTableManager.get().getStockInfoSync(contract.code, contract.market) + const showCode = stockInfo?.showCode + const stockName = stockInfo?.stockName + return new ContractInfo( + showCode, + contract.market, + contract.code, + contract.name ?? stockName, + StockRegularUtils.isOption(showCode), + TradeUtils.isZhuliCode(contract.code) ? ContractMainType.MAIN : ContractMainType.NOT_MAIN, + ContractDirectionType.NONE, + InactiveContractManager.get().isInactiveContract(contract.code) && !Markets.isArbitrageMarket(contract.market) + ) + })) + } + } + return favoriteGroup + }) + + ContractChooseManager.getInstance().setMainContractProvider(async (showCode, marketId) => { + const stockCode = CodeTableManager.get().getStockInfoByShowCodeSync(showCode, marketId)?.stockCode ?? '' + const contract = await TradeUtils.changeMainLinkToMainContract2(stockCode, marketId) + if (contract) { + return new ContractInfo( + contract.contractCode?.code, + contract.marketId?.code, + stockCode, + contract.contractName ?? '', + StockRegularUtils.isOption(showCode), + ContractMainType.NONE, + ContractDirectionType.NONE + ) + } + return new ContractInfo() + }) + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/monitor/APMHelper.ets b/future_entry_demo/src/main/ets/monitor/APMHelper.ets new file mode 100644 index 0000000..2d50ea1 --- /dev/null +++ b/future_entry_demo/src/main/ets/monitor/APMHelper.ets @@ -0,0 +1,239 @@ +import { HXEventMonitorPlugin } from "@kernel/app_monitor_event"; +import { + HXMonitor, + HXMonitorLogger, + IElkService, + IMonitorAppConfig, + IMonitorOnStartCallback +} from "@kernel/app_monitor_lib"; +import { HXPerformancePlugin } from "@kernel/app_monitor_performance"; +import { HXWebMonitorPlugin } from '@kernel/app_monitor_web'; +import { HXUserService } from "biz_hxservice"; +import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from "service_monitor"; +import { + AuthLogMonitor, + DefaultLogParamConfig, + HXTcpMonitorPlugin, + LogParamsConfig, + TcpMonitorConfig +} from "@kernel/app_monitor_tcp"; +import { HXAuthLogMonitor } from "biz_auth"; +import { HXHttpMonitorPlugin } from "@kernel/app_monitor_http"; +import { HXCrashMonitorPlugin, ICrashMonitorConfig } from "@kernel/app_monitor_crash"; +import { router } from "@kit.ArkUI"; +import { GIT_COMMIT } from "biz_common/src/main/ets/utils/debug/GitBuildConfig"; +import { AppConfigManager, getConnectionManager } from "biz_common"; +import { HxCBASAgentProvider } from "@b2c-spi/hx_cbas_api"; +import { + AppContextLike, + type LeakWatcherConfig, + initializeWatcherFromPreference +} from '@kernel/ohos_leak_watcher'; +import BuildProfile from "BuildProfile"; + +const APP_ID = "mobile-archive-am-qihuo-harmony" +const DEBUG_VERSION = "FUHM037.08.999.10.32" + +export class APMHelper { + static clientIp: string = ""; + + /** + * 判断是否为测试版本(debug 或 forTest) + */ + static isDebugOrTestBuild(): boolean { + const modeName = BuildProfile.BUILD_MODE_NAME.toString() + const targetName = BuildProfile.TARGET_NAME.toString() + return modeName === 'debug' || targetName === 'forTest' + } + + /** + * 初始化APM + * @param context + */ + static init(context: Context) { + //目前先只在debug包下打印日志 + const modeName: string = BuildProfile.BUILD_MODE_NAME.toString() + HXMonitorLogger._logOpen = modeName === 'debug' + + HXMonitor.builder(context) + .setAppId(APP_ID) + .hostParameter(new HXMonitorAppConfig()) + .setElkService(new APMElkService()) + .plugin(new HXPerformancePlugin()) + .plugin(new HXEventMonitorPlugin()) + .plugin(new HXTcpMonitorPlugin(new HXTcpMonitorConfig())) + .plugin(new HXWebMonitorPlugin()) + .plugin(new HXHttpMonitorPlugin()) + .plugin(new HXCrashMonitorPlugin(new HXCrashMonitorConfig())) + .build() + .startAllPlugins(new APMMonitorOnStartCallback()) + APMHelper.initLeakWatcher(context) + } + + /** + * 初始化内存泄漏监控 + * @param context 应用上下文 + */ + private static initLeakWatcher(context: Context) { + let appContext: AppContextLike = { + filesDir: context.getApplicationContext().filesDir || '' + } + let config: LeakWatcherConfig = { + enabled: true, + appContext: appContext, + baseDir: `${appContext.filesDir}/jsleak_watcher`, + leakObjectTypes: [], + resourceWatcher: { + enabled: true, + row: 1, + size: 10240, + timeOut: 1 + }, + retention: { maxArtifacts: 50, maxDays: 7, maxTotalBytes: 536870912 }, + upload: { + enabled: false, + url: '', + method: 'POST', + headers: [] + } + }; + initializeWatcherFromPreference(config); + } +} + +class APMMonitorOnStartCallback implements IMonitorOnStartCallback { + onStarted(code: number): void { + //暂时不做操作 + HXMonitorLogger.d("APMHelper", `APM started:${code}`) + } +} + +/** + * apm异常日志回调 + */ +class APMElkService implements IElkService { + pushBusinessLog(logType: string, bt: string, message: string | null): void; + pushBusinessLog(logType: string, bt: string, message: string | null, extraData: object | null): void; + pushBusinessLog(logType: string, bt: string, message: string | null, extraData?: object | null): void { + try { + const builder = new APMElkBuilder(logType) + builder.userid(HXUserService.getInstance().getUserId()) + builder.messageKey(bt) + builder.messageValue(message || "") + if (extraData) { + builder.messageExt(JSON.stringify(extraData)) + } + const elkMessage = new ElkUploadMessage(builder) + ElkService.getInstance().pushBusinessLog(elkMessage) + } catch (e) { + console.error("APMElkService upload message fail", e) + } + } +} + +class APMElkBuilder extends ElkUploadMessageBuilder { + constructor(biz_level: string) { + super(Date.now(), "apm", biz_level); + } +} + +/** + * 崩溃采集配置 + */ +class HXCrashMonitorConfig implements ICrashMonitorConfig { + provideUserName(): string { + return HXUserService.getInstance().getUserName(); + } + + provideBuildType(): string { + return BuildProfile.BUILD_MODE_NAME.toString() + } + + provideAppName(): string { + return getContext().applicationInfo.name + } + + async provideCBASInfo(): Promise { + const result = await HxCBASAgentProvider.getInstance().getRecentCBASData() + return result || "none" + } + + provideCurrentPageId(): string { + return router.getState().name + } + + provideGitVersion(): string { + return GIT_COMMIT + } + + providePackageName(): string { + return getContext().applicationInfo.name + } +} + +/** + * APM配置 + */ +class HXMonitorAppConfig extends IMonitorAppConfig { + getUserId(): string | null { + return HXUserService.getInstance().getUserIdFromLocal() + } + + getDeviceCode(): string | null { + //deviceInfo.ODID 目前华为获取设备id的api已经不可靠,现改为userid。 + return this.getUserId() + } + + getVersion(): string { + if (APMHelper.isDebugOrTestBuild()) { + return DEBUG_VERSION + } + return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull()??DEBUG_VERSION + } +} + +/** + * tcp监控配置 + */ +class HXTcpMonitorConfig extends TcpMonitorConfig { + getLogParamConfig(): LogParamsConfig { + return new HXTcpLogParamsConfig() + } + + getAuthLogMonitor(): AuthLogMonitor { + return HXAuthLogMonitor.getInstance() + } +} + +class HXTcpLogParamsConfig extends DefaultLogParamConfig { + getAppId(): string | null { + return APP_ID + } + + getAppVersion(): string | null { + if (APMHelper.isDebugOrTestBuild()) { + return DEBUG_VERSION + } + return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionFull()??DEBUG_VERSION; + } + + getPlatform(): string | null { + return AppConfigManager.getInstance().getConfigProvider()?.getAppPlatform()??APP_ID + } + + getServerIp(): string | null { + return getConnectionManager()?.getConnectedIp() || "" + } + + getUid(): string | null { + return HXUserService.getInstance().getUserIdFromLocal() + } + + getCip(): string | null { + return APMHelper.clientIp + } + + isUserVIP(): boolean { + return false + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/monitor/AppEvent.ets b/future_entry_demo/src/main/ets/monitor/AppEvent.ets new file mode 100644 index 0000000..013740e --- /dev/null +++ b/future_entry_demo/src/main/ets/monitor/AppEvent.ets @@ -0,0 +1,96 @@ +import hiAppEvent from '@ohos.hiviewdfx.hiAppEvent'; +import { HXLog } from 'biz_common/src/main/ets/logger/HXLog'; +import { HXUserService } from 'biz_hxservice'; +import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor'; + +const TAG: string = 'eventSubscription'; + +export function eventSubscription() { + hiAppEvent.addWatcher({ + name: "futures", + appEventFilters: [//APP_CRASH, APP_FREEZE事件将在下次启动时获取 + { + domain: hiAppEvent.domain.OS, + names: [hiAppEvent.event.APP_CRASH, hiAppEvent.event.APP_FREEZE, hiAppEvent.event.MAIN_THREAD_JANK, + hiAppEvent.event.CPU_USAGE_HIGH, hiAppEvent.event.RESOURCE_OVERLIMIT, hiAppEvent.event.ADDRESS_SANITIZER] + }, + ], + onReceive: async (domain: string, appEventGroups: Array) => { + HXLog.i(TAG, `HiAppEvent onReceive: domain=${domain}`) + setTimeout(() => { + scheduleHiAppEventUpload(appEventGroups) + }, 1000) + } + }); +} + +export function scheduleHiAppEventUpload(appEventGroups: Array) { + try { + let uploadMessage: Array = getFaultMessage(appEventGroups) + if (uploadMessage.length > 0) { + uploadMessage.forEach((message: HiAppEventUploadInfo, index: number) => { + uploadAppEventElkMessage("e", message, true) + }) + } + } catch (e) { + const error : string = ' Error while scheduleHiAppEventUpload, the appEventGroups is ' + JSON.stringify(appEventGroups) + HXLog.e(TAG, error) + uploadAppEventElkMessage('e',{message_key: 'scheduleHiAppEventUpload', message_value:error},true) + } + +} + +function getFaultMessage(appEventGroups: Array) { + const messageArr: Array = [] + if (appEventGroups && appEventGroups.length > 0) { + // 遍历事件组 + appEventGroups.forEach((eventGroup: hiAppEvent.AppEventGroup) => { + // 遍历事件对象集合 + eventGroup.appEventInfos.forEach((eventInfo: hiAppEvent.AppEventInfo) => { + let message: string = ''; + message += `HiAppEvent eventInfo.domain=${eventInfo.domain}\n` // 事件领域 + + `HiAppEvent eventInfo.name=${eventInfo.name}\n` // 事件名称 + + `HiAppEvent eventInfo.eventType=${eventInfo.eventType}\n` // 事件名称 + + `HiAppEvent eventInfo.params.time=${eventInfo.params['time']}\n` // 事件发生的时间 + + `HiAppEvent eventInfo.params.crash_type=${eventInfo.params['crash_type']}\n` + + `HiAppEvent eventInfo.params.foreground=${eventInfo.params['foreground']}\n` + + `HiAppEvent eventInfo.params.bundle_version=${eventInfo.params['bundle_version']}\n` + + `HiAppEvent eventInfo.params.bundle_name=${eventInfo.params['bundle_name']}\n` + + `HiAppEvent eventInfo.params.exception=${JSON.stringify(eventInfo.params['exception'])}\n` + + `HiAppEvent eventInfo.params.hilog.size=${eventInfo.params['hilog'].length}\n`; + const uploadInfo: HiAppEventUploadInfo = { + message_key: eventInfo.domain, + message_value: eventInfo.name, + message_ext: message + } + messageArr.push(uploadInfo) + }) + }) + } + return messageArr +} + +function uploadAppEventElkMessage(biz_level: string, info: HiAppEventUploadInfo, isInstant: boolean) { + const messageBuilder = new HiAppEventElkBuilder(biz_level) + messageBuilder.userid(HXUserService.getInstance().getUserId()) + if (info) { + messageBuilder.messageKey(info?.message_key ? info.message_key : '') + messageBuilder.messageValue(info?.message_value ? info.message_value : '') + messageBuilder.messageExt(info?.message_ext ? info.message_ext : '') + } + const message = new ElkUploadMessage(messageBuilder) + ElkService.getInstance().pushBusinessLog(message, isInstant) +} + + +interface HiAppEventUploadInfo { + message_key?: string, + message_value?: string, + message_ext?: string +} + +class HiAppEventElkBuilder extends ElkUploadMessageBuilder { + constructor(biz_level: string = 'e') { + super(Date.now(), "HiAppEvent", biz_level); + } +} diff --git a/future_entry_demo/src/main/ets/monitor/EntryMonitor.ets b/future_entry_demo/src/main/ets/monitor/EntryMonitor.ets new file mode 100644 index 0000000..9329e14 --- /dev/null +++ b/future_entry_demo/src/main/ets/monitor/EntryMonitor.ets @@ -0,0 +1,47 @@ +import { HXUserService } from 'biz_hxservice'; +import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from 'service_monitor'; + +export class EntryMonitor { + private static instance: EntryMonitor; + public static getInstance() : EntryMonitor{ + if(!EntryMonitor.instance) { + EntryMonitor.instance = new EntryMonitor() + } + return EntryMonitor.instance + } + public UploadElkMessage(biz_level:string,info: EntryUploadInfo, isInstant:boolean) { + const messageBuilder = new EntryElkBuilder(biz_level) + messageBuilder.userid(HXUserService.getInstance().getUserId()) + if(info) { + messageBuilder.messageKey(info?.message_key ? info.message_key : '' ) + messageBuilder.messageValue(info?.message_value ? info.message_value : '') + messageBuilder.messageExt(info?.message_ext ? info.message_ext : '') + } + const message = new ElkUploadMessage(messageBuilder) + + ElkService.getInstance().pushBusinessLog(message,isInstant) + } + +} + +export function buildEntryUploadBasicInfo(message:string,caller:string) { + let info : EntryUploadInfo = { + message_key: caller, + message_value: `${caller} upload message ${message} `, + } + return info; +} + + +export interface EntryUploadInfo { + message_key?:string, + message_value?:string, + message_ext?:string +} + + +class EntryElkBuilder extends ElkUploadMessageBuilder { + constructor(biz_level: string) { + super(Date.now(), "entry", biz_level); + } +} diff --git a/future_entry_demo/src/main/ets/monitor/SecurityMonitorInit.ets b/future_entry_demo/src/main/ets/monitor/SecurityMonitorInit.ets new file mode 100644 index 0000000..67639c1 --- /dev/null +++ b/future_entry_demo/src/main/ets/monitor/SecurityMonitorInit.ets @@ -0,0 +1,67 @@ +import { PassportManager } from "biz_auth/src/main/ets/auth/PassportManager"; +import { CookieManager, HXUserService } from "biz_hxservice"; +import { SecurityMonitorProxy } from "lib_apm_security"; +import { bundleManager } from "@kit.AbilityKit"; +import { AppConfigManager, AppInfoConfig } from "biz_common"; +import { StringBuilder } from "hxutil"; +import { StrUtil } from "@pura/harmony-utils"; + +/** + * author:zhangshan2 + * created on 2025/1/3 + * desc: + */ +export class SecurityMonitorInit{ + private static instance:SecurityMonitorInit + private constructor() { + } + + static getInstance(){ + if (!SecurityMonitorInit.instance) { + SecurityMonitorInit.instance = new SecurityMonitorInit() + } + return SecurityMonitorInit.instance + } + + /** + * 崩溃保护初始化 + */ + init(){ + if (!AppInfoConfig.Official){ + return; + } + + SecurityMonitorProxy.getInstance().commonInfInfo=()=>{ + let userId = HXUserService.getInstance().getUserIdFromLocal(); + let info = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT); + let sb = new StringBuilder(); + + sb.append("userId:").append(userId).append("\r\n") + .append("bundleName:").append(info.name).append("\r\n") + .append("versionName:").append(info.versionName).append("\r\n") + .append("innerVersion:").append(AppInfoConfig.APPLET_INNER_VERSION) + + return sb.toString(); + } + SecurityMonitorProxy.getInstance().agent=()=>{ + return AppConfigManager.getInstance().getUA(); + } + SecurityMonitorProxy.getInstance().innerVersion = ()=>{ + return AppInfoConfig.APPLET_INNER_VERSION_FULL; + } + + SecurityMonitorProxy.getInstance().isUserTemp = ()=>{return HXUserService.getInstance().isTemporaryUser()} ; + SecurityMonitorProxy.getInstance().cooke = ()=>{ + let cookie = CookieManager.getInstance().getCookieSync(); + if (!cookie) { + return ""; + } + return cookie; + }; + + //初始化启动崩溃保护 + SecurityMonitorProxy.getInstance().initSecurityMonitor(); + } + + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/network/HXNetwork.ets b/future_entry_demo/src/main/ets/network/HXNetwork.ets new file mode 100644 index 0000000..a40ae7a --- /dev/null +++ b/future_entry_demo/src/main/ets/network/HXNetwork.ets @@ -0,0 +1,43 @@ +import { connection } from "@kit.NetworkKit"; +import { Want } from "@kit.AbilityKit"; +import { AppUtil } from "@pura/harmony-utils"; +import { HXLog } from "biz_common"; + +export class HXNetwork { + private static instance: HXNetwork; + private TAG: string = "HXNetwork"; + + public static getInstance(): HXNetwork { + if (!HXNetwork.instance) { + HXNetwork.instance = new HXNetwork() + } + return HXNetwork.instance + } + + /** + * 检测网络是否可用 + */ + detectNet(): boolean { + try { + let handle = connection.getDefaultNetSync(); + if (!handle || handle.netId === 0) { + // 是否获取到有效网络连接 + HXLog.e(this.TAG, "Can\'t get available connection!"); + return false; + } + + let capabilities = connection.getNetCapabilitiesSync(handle); + let cap = capabilities.networkCap || []; + + // 网络是否可用 + if (!cap.includes(connection.NetCap.NET_CAPABILITY_VALIDATED) && !cap.includes(connection.NetCap.NET_CAPABILITY_INTERNET)) { + HXLog.e(this.TAG, "The network is not available."); + return false; + } + return true; + } catch (e) { + HXLog.e(this.TAG, e); + return false; + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/network/NetworkAnomalyDialog.ets b/future_entry_demo/src/main/ets/network/NetworkAnomalyDialog.ets new file mode 100644 index 0000000..2796305 --- /dev/null +++ b/future_entry_demo/src/main/ets/network/NetworkAnomalyDialog.ets @@ -0,0 +1,77 @@ +/** + * 网络异常(不可用)弹窗 + */ +@Preview +@Component +export struct NetworkAnomalyDialog { + exitApp?: () => void; + setup?: () => void; + retry?: () => void; + exitBtnText: string | ResourceStr = "退出"; + setupBtnText: string | ResourceStr = "设置网络"; + retryBtnText: string | ResourceStr = "重试"; + //弹窗标题 + title?: string | ResourceStr = "提示"; + //弹窗正文 + content?: string | ResourceStr = "联网可能异常,请检查您的网络!"; + borderW: number = 0.7; + borderR: number = 10; + marginTop: number = 20; + + build() { + Column() { + Text(this.title).fontSize(18).margin({ top: this.marginTop }).fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + Text(this.content).fontSize(14).margin({ top: this.marginTop }) + .fontColor($r('app.color.elements_text_primary_02')) + + Row() { + Button(this.exitBtnText, { type: ButtonType.Normal, stateEffect: true }) + .onClick(() => { + if (this.exitApp) { + this.exitApp(); + } + }) + .fontColor($r('app.color.elements_text_secondary')) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .layoutWeight(1) + .border({ + width: { top: this.borderW, right: this.borderW }, + color: $r('app.color.elements_others_divider_primary'), + radius: { bottomLeft: this.borderR } + }) + Button(this.setupBtnText, { type: ButtonType.Normal, stateEffect: true }) + .onClick(() => { + if (this.setup) { + this.setup(); + } + }) + .fontColor($r('app.color.elements_text_primary_01')) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .layoutWeight(1) + .border({ + width: { top: this.borderW }, + color: $r('app.color.elements_others_divider_primary'), + }) + Button(this.retryBtnText, { type: ButtonType.Normal, stateEffect: true }) + .onClick(() => { + if (this.retry) { + this.retry(); + } + }) + .fontColor($r('app.color.elements_text_primary_01')) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .layoutWeight(1) + .border({ + width: { top: this.borderW, left: this.borderW }, + color: $r('app.color.elements_others_divider_primary'), + radius: { bottomRight: this.borderR } + }) + } + .margin({ top: this.marginTop }) + } + .backgroundColor($r('app.color.surface_layer1_foreground')) + .width('75%') + .borderRadius(this.borderR) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/network/NetworkAnomalyPage.ets b/future_entry_demo/src/main/ets/network/NetworkAnomalyPage.ets new file mode 100644 index 0000000..8988d40 --- /dev/null +++ b/future_entry_demo/src/main/ets/network/NetworkAnomalyPage.ets @@ -0,0 +1,123 @@ +import { AppConfigManager, HXLog, PreferenceService } from 'biz_common' +import { common, Want } from '@kit.AbilityKit' +import router from '@ohos.router' +import { window } from '@kit.ArkUI' +import { deviceInfo } from '@kit.BasicServicesKit' +import { NetworkAnomalyDialog } from './NetworkAnomalyDialog' +import { HXNetwork } from './HXNetwork' +import { ConnectionSeq } from '@kernel/lib_communication' +import { AuthProviderService } from 'biz_auth' +import { AdvertisingMonitorService } from 'biz_hxservice' + +const SettingWant: Want = { + bundleName: 'com.huawei.hmos.settings', + abilityName: 'com.huawei.hmos.settings.MainAbility', +} + +/** + * 网络异常页面 + * 用于承接空白Logo页面,网络异常弹窗 + */ +@Entry +@Component +struct NetworkAnomalyPage { + private TAG: string = "HXNetwork"; + private systemBarColor = "#00000000"; + private systemBarContentColor = "#FFFFFFFF"; + + async setSystemBar() { + let context = (this.getUIContext().getHostContext() as common.UIAbilityContext); + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(context); + // 前提:设置窗口为全屏窗口 + if ((deviceInfo.deviceType !== '2in1')) { + windowClass.setWindowLayoutFullScreen(true); + } else { + windowClass.setWindowLayoutFullScreen(false); + } + + await windowClass.setWindowSystemBarProperties({ + // 颜色属性为ARGB,将蒙层设置为0%使其透明 + navigationBarColor: this.systemBarColor, + statusBarColor: this.systemBarColor, + navigationBarContentColor: this.systemBarContentColor, + statusBarContentColor: this.systemBarContentColor + }) + } + + aboutToAppear(): void { + this.setSystemBar() + } + + onExit() { + HXLog.i(this.TAG, "EXIT APP!!"); + // 用户退出应用 + (this.getUIContext().getHostContext() as common.UIAbilityContext).terminateSelf(() => { + }); + } + + onJumpToSetup() { + HXLog.i(this.TAG, "JumpSetup!!"); + let context = (this.getUIContext().getHostContext() as common.UIAbilityContext); + context.startAbility(SettingWant, (err, data) => { + HXLog.i(this.TAG, `startAbility : err : ${JSON.stringify(err) ?? ''}`); + HXLog.i(this.TAG, `startAbility : data : ${JSON.stringify(data) ?? ''}`); + }) + } + + onRetry() { + const netAvailable = HXNetwork.getInstance().detectNet(); + if (netAvailable) { + // 用户是否同意隐私政策 + const userAgree = PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false); + const jumpUrl = userAgree ? 'pages/Index' : 'pages/LauncherPage'; + router.replaceUrl({ url: jumpUrl }, router.RouterMode.Single, (err) => { + if (err) { + console.error("Failed to start Index Page: " + err) + return; + } + if (userAgree) { + AdvertisingMonitorService.getInstance().init(this.getUIContext().getHostContext() as common.UIAbilityContext) + AppConfigManager.getInstance().fetchIMEI().then((isSuccess: boolean) => { + if (isSuccess) { + AuthProviderService.getInstance().sendAuthRequest(ConnectionSeq.HQ); + } + }); + } + }) + } + } + + build() { + RelativeContainer() { + NetworkAnomalyDialog({ + exitApp: this.onExit, + setup: this.onJumpToSetup, + retry: this.onRetry, + title: '提示', + content: '联网可能异常,请检查您的网络!' + }) + .zIndex(4) + .align(Alignment.Center) + .alignRules( + { + 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }, + 'bottom': { 'anchor': '__container__', 'align': VerticalAlign.Bottom }, + 'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }, + 'right': { 'anchor': '__container__', 'align': HorizontalAlign.End } + }) + .backgroundColor($r('app.color.dialog_bg_99666666')) + Image($r('app.media.launch_logo')) + .objectFit(ImageFit.Auto) + .width('80%') + .height(205) + .alignRules({ + 'bottom': { + 'anchor': "__container__", + 'align': VerticalAlign.Bottom + }, + 'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center }, + }) + }.width('100%').height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/CommonListPage.ets b/future_entry_demo/src/main/ets/pages/CommonListPage.ets new file mode 100644 index 0000000..8470ffa --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/CommonListPage.ets @@ -0,0 +1,81 @@ +import { SectorList } from 'biz_market'; +import router from '@ohos.router'; +import common from '@ohos.app.ability.common'; +import window from '@ohos.window'; +import { StatusBar } from '../widget/StatusBar'; +import { SubPageBar } from 'biz_selfcode'; +import { TypeUtil } from 'biz_common'; +import { StockListPageConfig } from 'biz_market/src/main/ets/components/stockListPage/Config'; +import { sectorListPageConfig } from 'biz_market/src/main/ets/features/sectorList/Config'; + + +@Entry +@Component +struct SectorListPage { + @State barHeight: number = 0; + private pageConfig: StockListPageConfig = sectorListPageConfig; + private selectedTabIndex: number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + this.barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + async resetSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + /** + * + * 从路由中获取参数并初始化页面数据 + */ + aboutToAppear() { + let routerParam = router.getParams() as Record; + if (routerParam.selectedTabIndex && TypeUtil.isNumber(routerParam.selectedTabIndex)) { + this.selectedTabIndex = routerParam.selectedTabIndex as number; + } + if (routerParam.pageConfig) { + this.pageConfig = routerParam.pageConfig as StockListPageConfig; + } + this.setSystemBar() + } + + aboutToDisappear() { + this.resetSystemBar() + } + + build() { + Column() { + StatusBar(); + SubPageBar({ + title: this.pageConfig.pageTitle, + }); + Flex({ direction: FlexDirection.Column }) { + SectorList({ + defaultTabIndex: this.selectedTabIndex, + pageConfig: this.pageConfig + }) + .flexShrink(1) + .height('100%') + }.layoutWeight(1) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/CommonWebPage.ets b/future_entry_demo/src/main/ets/pages/CommonWebPage.ets new file mode 100644 index 0000000..786afbb --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/CommonWebPage.ets @@ -0,0 +1,92 @@ +import router from '@ohos.router' +import web_webview from '@ohos.web.webview'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; + +/** + * 暂时使用的页面 + */ +@Entry +@Component +export default struct CommonWebPage { + private url: string = ''; + private title: string = '' + private webController: WebviewController = new web_webview.WebviewController; + @State barHeight:number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + this.barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + async resetSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + aboutToAppear() { + this.setSystemBar() + + const routerParam = router.getParams() as Record; + this.url = routerParam.url; + this.title = routerParam.title || ''; + } + + aboutToDisappear(){ + this.resetSystemBar() + try { + this.webController.stop() + } catch (e) { + // WebView may already be detached. + } + } + + build() { + Column() { + Blank().height(px2vp(this.barHeight) +2).color($r('app.color.subpage_title_red')).width('100%') + Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Column() { + Image($r('app.media.toolbar_arrow_left')) + .width(19) + .height(24) + .fillColor('#FFFFFF') + }.margin({ right: 16 }) + .onClick(() => { + router.back(); + }) + .padding(10) + Text(this.title).fontSize(19).fontColor('#FFFFFF') + }.margin({ left: 16 }) + } + .height(44).backgroundColor($r('app.color.navbar_bg_red')).width('100%') + + Web({ + src: this.url, + controller: this.webController + }).domStorageAccess(true) + .mixedMode(MixedMode.All) + .flexGrow(1) + .flexShrink(1) + } + .height('100%') + .width('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/FloatingWindowPage.ets b/future_entry_demo/src/main/ets/pages/FloatingWindowPage.ets new file mode 100644 index 0000000..c91d5d4 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/FloatingWindowPage.ets @@ -0,0 +1,23 @@ +import { DebugService, FloatingDebugWindow} from '@b2c-spi/hx_cbas_api'; + +/** + * 悬浮窗页面 + */ +@Entry +@Component +struct FloatingWindowPage { + build() { + Column() { + FloatingDebugWindow({ + onMinimize: (): void => { DebugService.getInstance().minimizeToIcon(); }, + onClose: (): void => { DebugService.getInstance().hideWindow(); }, + onMove: (deltaX: number, deltaY: number): void => { + DebugService.getInstance().moveWindow(deltaX, deltaY); + } + }) + } + .width('100%') + .height('100%') + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/Index.ets b/future_entry_demo/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..909294e --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/Index.ets @@ -0,0 +1,548 @@ +import { LoginCommonConstants } from 'biz_login'; +import { RSAKeyManager, WxPaySidPermissionMarker } from 'biz_auth'; +import common from '@ohos.app.ability.common'; +import window from '@ohos.window'; +import { BusinessError } from '@ohos.base'; +import { saveSelfcodeSync } from 'service_watchlist'; +import emitter from '@ohos.events.emitter'; +import WTInit from 'biz_trade/src/main/ets/utils/WTInit'; +import { + AdsPageConfig, + EmitterConstants, + GlobalContext, + HeadSettingManager, + HXLog, + HxTags, + IndexTabManager, + OptionPageConstants, + TabManager, + TabManagerConstants +} from 'biz_common'; +import hilog from '@ohos.hilog'; +import { CookieManager, HXUserService, KaihuFlagManager, NewUserManager, UserSourceManager } from 'biz_hxservice'; +import { StatusBar } from '../widget/StatusBar'; +import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; +import { deviceInfo } from '@kit.BasicServicesKit'; +import { AdsPopupHost, FirstPage } from '@b2c/first_page'; +import { router } from '@kit.ArkUI'; +import { HQPage } from './future/HQPage'; +import TradeBizPageContainer from './TradeBizPageContainer'; +import { GrayTestManager } from 'lib_graytestmanager' +import { HexinVersionControl } from 'biz_trade/src/main/ets/utils/HexinVersionControl'; +import { DiscoverPage } from '@b2c/biz_discover'; +import { HQPageConstants } from 'biz_common/src/main/ets/constants/HQPageConstants'; +import { NumberUtil } from '@pura/harmony-utils'; +import { parseInt } from '@wolfx/lodash'; +import { cbasService } from '../register/cbasregister/CbasServiceRegister'; +import BuildProfile from 'biz_common/BuildProfile'; +import { DebugFloatingBall } from './debug/DebugFloatingBall'; +import { GroupSubManager } from 'base_mobilepush'; +import { HXPushService } from 'service_push'; +import { FIRST_PAGE_ONAPPEAR_EVENT } from '../entryability/WantManager'; +import { DialogHub } from '@hadss/dialoghub'; +import { DrawLineInit, QuoteCustomSettingManager, SettingHelper, SettingDataSource, QuoteSettingEvents, CurveBottomToolsDataManager } from 'biz_quote'; +import { TradeSettingManager } from 'biz_trade/src/main/ets/setting'; +import { AutoLoginManager } from 'biz_trade/src/main/ets/trade/manager/AutoLoginManager'; +import { ColourModeStatus } from '@kernel/theme_manager'; +import { enableDarkMode, ThemeManagerEmitterConstants } from '@kernel/theme_manager'; +import { CmeTraceForeignManager } from 'biz_quote/src/main/ets/manager/CmeTraceForeignManager'; +import { HQUGCManager } from 'biz_market'; +import { AdsManager } from '@b2c-f/fuhm-ads'; + + +const TAG: string = 'Struct Index'; +const SWIPER_INDEX_0: number = 0; +const SWIPER_INDEX_1: number = 1; + +@Entry +@Component +struct IndexM { + /**************************************/ + //Tab管理 + tabManager: TabManager = new IndexTabManager(); + //主页TabIndex + @Provide('mainPageIndex') currentIndex: number = 0; + private tabController: TabsController = new TabsController(); + /**************************************/ + //交易用的index page show + @Provide MainPageShow: boolean = false; + @Provide pageShow: boolean = false; + /**************************************/ + //全局userId, userName快速获取 + @Provide userId: string = ''; + @Provide userName: string = ''; + /**************************************/ + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; + /**************************************/ + //一多断点 + @StorageLink('displayBreakPoint') curBp: string = 'sm' + @StorageLink('windowWidthVp') windowWidthVp: number = 0; + /**************************************/ + //自选业务 + @Provide('SIDE_BAR_ENABLE') @Watch('sideBarStatusChanged') sideBarEnable: boolean = false; + @Provide('PRESSED_ROW') pressedRow: Array = new Array(3).fill(''); + /**************************************/ + // 云同步数据更新回调 + private onSettingUpdateCallback = () => { + HXLog.i(TAG, 'QuoteSettingEvents.SETTING_UPDATE received, clearing HeadSettingManager cache') + HeadSettingManager.getInstance().clearCache() + } + @Provide('SELF_CODE_TITLE') selfCodeTitle: string = '同花顺自选'; + /**************************************/ + //侧滑栏 + @State scrollable: boolean = false; + @State swiperIndex: number = SWIPER_INDEX_1; + private swiperController: SwiperController = new SwiperController() + @State swiperOffset: number = 0; + @State @Watch('coeChanged') coefficient: number = 0; + //蒙层可见性 + @State stackVis: boolean = false + //布局总宽度 + private totalWidth = 0 + @State isShowStatusBar: boolean = false + @State isHideYaoYi: boolean = false; + // 行情二级Tab + @Provide hqPageSubTabIndex: number = HQPageConstants.HQ_PAGE_INDEX_SELF_CODE_PAGE + // 期权三级Tab + @Provide optionPageL3TabIndex: number = OptionPageConstants.HQ_PAGE_INDEX_T_QUOTE_PAGE + // 主题监听 + @StorageProp(enableDarkMode) @Watch('onThemeModeChanged') enableDarkMode: boolean = ColourModeStatus.enableDarkMode() + + // 主题变化回调 + onThemeModeChanged() { + HXLog.d(TAG, 'onThemeModeChanged triggered, enableDarkMode=' + this.enableDarkMode) + this.setSystemBar(this.stackVis) + } + + parseOffset(curOffset: number): number { + if (this.totalWidth > 0) { + let coe = this.swiperIndex === 1 ? (Math.abs(curOffset) / (this.totalWidth * 0.85)) : + (1 - Math.abs(curOffset) / (this.totalWidth * 0.85)) + console.log(`lym: coe: ${coe}`) + return coe * 0.4 + } + + return 0 + } + + coeChanged() { + } + + sideBarStatusChanged() { + } + + + //沉浸式设置、状态栏设置 + async setSystemBar(stackVis: boolean) { + // 获取主窗口 + let windowStageInstance: window.WindowStage = AppStorage.get('EntryAbilityWindowStage') as window.WindowStage + if (!windowStageInstance) { + return + } + let windowClass: window.Window = windowStageInstance.getMainWindowSync() + + // 前提:设置窗口为全屏窗口 + if ((deviceInfo.deviceType !== '2in1')) { + windowClass.setWindowLayoutFullScreen(true); + } else { + console.log("deviceType" + deviceInfo.deviceType) + windowClass.setWindowLayoutFullScreen(false); + } + // 根据主题设置状态栏内容颜色 + let isDarkMode = ColourModeStatus.enableDarkMode() + let contentColor = isDarkMode ? "#FFFFFFFF" : "#D6000000" + await windowClass.setWindowSystemBarProperties({ + // 颜色属性为ARGB,将蒙尘设置为0%使其透明 + navigationBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + statusBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + navigationBarContentColor: stackVis + ? getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_with_mask_color') + : contentColor, + statusBarContentColor: stackVis + ? getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_with_mask_color') + : contentColor + }) + } + + @Builder + TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) { + Column() { + Image(this.currentIndex === targetIndex ? selectedImg : normalImg) + .size({ width: 24, height: 24 }) + .fillColor(this.currentIndex !== targetIndex ? $r('app.color.elements_text_primary_02') : $r('app.color.elements_text_primary_01')) + Text(title)//'#2E58FF' + .fontColor(this.currentIndex === targetIndex ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_primary_02')).fontSize('11fp').margin({ top: 4 }) + } + .hoverEffect(HoverEffect.Highlight) + //.width('100%') + .height(50) + .justifyContent(FlexAlign.Center) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + updateUserInfo() { + this.userId = HXUserService.getInstance().getUserId(); + this.userName = HXUserService.getInstance().getUserName(); + cbasService.setUserId(this.userId) + } + + subscribeLoginEvent() { + emitter.on({ + eventId: LoginCommonConstants.USER_LOGIN + }, async () => { + HXLog.d(TAG, HxTags.entryTags + 'subscribe login index triggered at ' + Date.now()) + this.updateUserInfo(); + // 登录后刷新 drawline 用户上下文,确保后续同步按当前账号隔离。 + DrawLineInit.init() + WxPaySidPermissionMarker.requestPermissionIfMarked() + WTInit.initAfterAppLogin() + GroupSubManager.getInstance().requestGroupData(); + RSAKeyManager.getInstance().requestRSAInfo(); + GrayTestManager.getInstance().requestGrayTestWhenUserIdChanged(this.userId) + HXPushService.getInstance().shouldUploadNotificationByUseridChanged(); + // 发起 CME 内外盘联动配置请求(只请求一次) + CmeTraceForeignManager.getInstance().request() + NewUserManager.getInstance().onAuthSucceed(() => { + AdsManager.reset() + if (NewUserManager.getInstance().needRequestAdsNow()) { + AdsManager.refreshNotifyFirstPageAds(true) + } + }) + KaihuFlagManager.getInstance().onConnectToServer() + UserSourceManager.getInstance().onConnectToServer() + // 更新行情设置管理器的用户ID(登录成功强制重新下载云端数据) + QuoteCustomSettingManager.getInstance().updateUserId( + HXUserService.getInstance().getUserIdFromLocal(), + true // 强制重新下载云端数据 + ) + + // 更新底部操作栏排序管理器的用户ID(登录成功强制重新下载云端数据) + CurveBottomToolsDataManager.getInstance().updateUserId( + HXUserService.getInstance().getUserIdFromLocal(), + true + ) + + // 更新交易设置管理器的用户ID(登录成功强制重新下载云端数据) + TradeSettingManager.getInstance().updateUserId( + HXUserService.getInstance().getUserIdFromLocal(), + false + ) + }); + emitter.on({ + eventId: LoginCommonConstants.USER_LOGOUT + }, () => { + HXLog.d(TAG, 'LoginEvent LoginCommonConstants.USER_LOGOUT') + this.updateUserInfo(); + // 登出后重新写入临时用户上下文,避免沿用上一个账号的画线身份。 + DrawLineInit.init() + CurveBottomToolsDataManager.getInstance().updateUserId( + HXUserService.getInstance().getUserIdFromLocal(), + false + ) + + // 退出账号,重置所有自动登录信息 + AutoLoginManager.getInstance().forceClearAutoLoginAccountToken() + + // 退出登录时清除表头设置缓存,并发送事件通知页面刷新为默认配置 + HeadSettingManager.getInstance().clearCache() + emitter.emit({ eventId: QuoteSettingEvents.SETTING_UPDATE }) + }); + /*emitter.on({ + eventId: EventConstants.ID_JUMP_TO_WT_FIRST_PAGE + }, (data) => { + // 用于其它页面 跳到主页面,并显示指定tab + let index = data.data!!["tabIndex"] as number; + if (this.currentIndex != index) { + router.back({ + url: "pages/Index", + params: { + tabIndex: index, + } + }) + } + });*/ + } + + subscribeTabChangeIndex() { + emitter.on({ eventId: EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX }, (eventData) => { + this.tabController.changeIndex(2) + if (eventData && eventData.data) { + let tabUIManagerIndex: string | undefined = eventData.data[EmitterConstants.EVENT_DATA_KEY_TAB_INDEX] + if (tabUIManagerIndex && NumberUtil.isNumber(tabUIManagerIndex)) { + this.tabController.changeIndex(parseInt(tabUIManagerIndex)) + } + let qhPageSubTabIndex: string | undefined = + eventData.data[EmitterConstants.EVENT_DATA_KEY_HQ_PAGE_SUB_TAB_INDEX] + if (qhPageSubTabIndex && NumberUtil.isNumber(qhPageSubTabIndex)) { + this.hqPageSubTabIndex = parseInt(qhPageSubTabIndex) + } + let optionPageL3TabIndex: string | undefined = + eventData.data[EmitterConstants.EVENT_DATA_KEY_OPTION_PAGE_SUB_TAB_INDEX] + if (optionPageL3TabIndex && NumberUtil.isNumber(optionPageL3TabIndex)) { + this.optionPageL3TabIndex = parseInt(optionPageL3TabIndex) + } + } + }) + } + + subscribeSwiperChangeIndex() { + emitter.on({ eventId: EmitterConstants.SWIPER_CHANGETOINDEX_0 }, () => { + this.swiperController.changeIndex(0, true) + this.coefficient = 0.4 + this.stackVis = true + this.setSystemBar(this.stackVis) + }) + emitter.on({ eventId: EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP }, () => { + this.swiperController.changeIndex(1, true) + this.stackVis = false + this.setSystemBar(this.stackVis) + }) + } + + unSubscribeLoginEvent() { + HXLog.d(TAG, HxTags.entryTags + + 'Unsubscribe LoginEvent LoginCommonConstants.USER_LOGOUT ,LoginCommonConstants.USER_LOGOUT triggered at ' + + Date.now()) + emitter.off(LoginCommonConstants.USER_LOGIN); + emitter.off(LoginCommonConstants.USER_LOGOUT); + } + + aboutToDisappear() { + HXLog.d(TAG, 'Index aboutToDisappear triggered at ' + Date.now()) + emitter.off(EmitterConstants.SWIPER_CHANGETOINDEX_1_TEMP) + emitter.off(EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX) + emitter.off(QuoteSettingEvents.SETTING_UPDATE, this.onSettingUpdateCallback) + this.unSubscribeLoginEvent(); + // this.unSubscribeThemeChange(); + saveSelfcodeSync() + } + + aboutToAppear() { + hilog.info(0x0000, 'Entry IndexPage', '%{public}s', 'page aboutToAppear'); + this.subscribeLoginEvent(); + this.subscribeSwiperChangeIndex() + this.subscribeTabChangeIndex() + this.setSystemBar(this.stackVis) + this.isHideYaoYi = HexinVersionControl.hideTrade(); + DialogHub.init(this.getUIContext()) + + // 初始化行情设置管理器(使用当前用户ID,可能是临时用户) + QuoteCustomSettingManager.getInstance().initialize( + this.context, + HXUserService.getInstance().getUserIdFromLocal() + ).catch((err: Error) => { + HXLog.e(TAG, `QuoteCustomSettingManager early init failed: ${err.message}`) + }) + + CurveBottomToolsDataManager.getInstance().initialize( + this.context, + HXUserService.getInstance().getUserIdFromLocal() + ).catch((err: Error) => { + HXLog.e(TAG, `CurveBottomToolsDataManager early init failed: ${err.message}`) + }) + + HQUGCManager.get().queryData(CookieManager.getInstance().getCookieSync()) + + // 配置表头设置管理器的云同步访问器 + HeadSettingManager.getInstance().setSettingAccessor( + (key: string, defaultValue: ESObject): ESObject => SettingHelper.getSetting(key, defaultValue, SettingDataSource.QUOTE_CLOUD), + (key: string, value: ESObject): void => { SettingHelper.updateSetting(key, value, SettingDataSource.QUOTE_CLOUD) } + ) + + // 监听云同步数据更新事件,清除表头设置缓存以便重新从云端读取 + emitter.on({ eventId: QuoteSettingEvents.SETTING_UPDATE }, this.onSettingUpdateCallback) + + // 初始化交易设置管理器(使用当前用户ID,可能是临时用户) + TradeSettingManager.getInstance().initialize( + this.context + ).catch((err: Error) => { + HXLog.e(TAG, `TradeSettingManager early init failed: ${err.message}`) + }) + + // 冷启动先初始化一次 drawline 基础能力,保证首次进入行情页即可绘制同步线。 + DrawLineInit.init() + } + + onPageShow() { + this.updateUserInfo(); + this.MainPageShow = true; + this.pageShow = true; + this.tabManager.notifyVisibilityChangeWithVisibility(true) + this.setSystemBar(this.stackVis) + //fixme 这里直接通过生命周期去请求是比较取巧的解决方案,应该在认证成功后就发起,后续修改 + //index页面在前台且展示自选tab页 + if (router.getParams()) { + let params = router.getParams() as Record; + let indexStr = params.tabIndex; + if (indexStr) { + let index = Number.parseInt(indexStr.toString()); + if (index && index != this.currentIndex) { + this.tabController.changeIndex(index); + } + } else if (params.loginSuccessBackParam && params.loginSuccessBackParam == 'selectjiaoyi') { + this.tabController.changeIndex(TabManagerConstants.INDEX_TAB_INDEX_TRADE_PAGE); + } + } + + // 创建调试悬浮球(仅非release包) + if (BuildProfile.BUILD_MODE_NAME.toString() != "release" && DebugFloatingBall.canShow) { + let windowStage = AppStorage.get('EntryAbilityWindowStage') as window.WindowStage + DebugFloatingBall.createDebugFloatingBall(windowStage) + } + } + + /** + * 根据是否隐藏交易获取发现tab的索引 + * @returns + */ + private getDiscoverIndex():number{ + return this.isHideYaoYi?TabManagerConstants.INDEX_TAB_INDEX_DISCOVER_PAGE-1:TabManagerConstants.INDEX_TAB_INDEX_DISCOVER_PAGE + } + + /** + * 根据 Tab 索引获取广告悬浮球对应的 pageId + */ + private getFloatBallPageId(tabIndex: number): AdsPageConfig { + if (tabIndex === TabManagerConstants.INDEX_TAB_INDEX_FIRST_PAGE) { + return AdsPageConfig.FIRST_PAGE_ID + } + return AdsPageConfig.INVALID + } + + onPageHide() { + this.MainPageShow = false; + this.pageShow = false; + this.tabManager.notifyVisibilityChangeWithVisibility(false) + } + + /** + * 当首页节点挂载时触发 + */ + private onFirstPageNodeAppear() { + // 发送首页挂载的通知 + GlobalContext.get().eventHub.emit(FIRST_PAGE_ONAPPEAR_EVENT); + // 首页挂载后进行push服务的初始化检查和上报 + HXPushService.getInstance().onFirstPageAppear(() => { + }); + // 通知广告悬浮球页面切换(首次进入首页时需主动调用) + AdsPopupHost.getInstance().onPageJump(AdsPageConfig.FIRST_PAGE_ID); + } + + build() { + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { + StatusBar() + .visibility(this.isShowStatusBar ? Visibility.Visible : Visibility.None) + Tabs({ controller: this.tabController }) { + if (this.tabManager.getShowingTabSet().has(TabManagerConstants.FirstPage)) { + TabContent() { + FirstPage({ tabManager: this.tabManager.getManager(TabManagerConstants.FirstPage) }) + } + .tabBar(this.TabBuilder("首页", TabManagerConstants.INDEX_TAB_INDEX_FIRST_PAGE, $r("app.media.icon_home_fill_24"), + $r("app.media.icon_home_24"))) + .onAppear(()=> { + this.onFirstPageNodeAppear(); + }) + } + + if (this.tabManager.getShowingTabSet().has(TabManagerConstants.SelfcodePage)) { + TabContent() { + HQPage({ tabManager: this.tabManager.getManager(TabManagerConstants.SelfcodePage) }) + } + .tabBar(this.TabBuilder("行情", TabManagerConstants.INDEX_TAB_INDEX_QUOTE_PAGE, $r("app.media.icon_line_chart_fill_24"), + $r("app.media.icon_line_chart_24"))) + } + + if (!this.isHideYaoYi && this.tabManager.getShowingTabSet().has(TabManagerConstants.TradePage)) { + TabContent() { + TradeBizPageContainer({ + tabManager: this.tabManager.getManager(TabManagerConstants.TradePage) + }); + } + .tabBar(this.TabBuilder("交易", TabManagerConstants.INDEX_TAB_INDEX_TRADE_PAGE, $r("app.media.icon_trade_fill_24") + , $r("app.media.icon_trade_24"))) + } + + if (this.tabManager.getShowingTabSet().has(TabManagerConstants.NewsPage)) { + TabContent() { + DiscoverPage({ + tabManager: this.tabManager.getManager(TabManagerConstants.NewsPage) + }) + } + .tabBar(this.TabBuilder("发现", this.getDiscoverIndex(), $r("app.media.icon_compass_fill_24"), + $r("app.media.icon_compass_24"))) + } + } + .vertical(this.curBp === 'lg' ? true : false) + .barPosition(this.curBp === 'lg' ? BarPosition.Start : BarPosition.End) + .width('100%') + .height('100%') + .scrollable(false) + .flexGrow(1) + .flexBasis('0%') + .animationDuration(0) + .onChange((index: number) => { + if (!this.isHideYaoYi && this.currentIndex != 2 && index == 2 && + HXUserService.getInstance().isTemporaryUser()) { + this.tabController.changeIndex(this.currentIndex) + router.pushUrl({ + url: 'pages/login/LoginPage', + params: { + flexSwitchFlag: true, + sourcePage: 'Index', + backParam: 'selectjiaoyi' + }, + }, router.RouterMode.Single) + } else { + if (index == TabManagerConstants.INDEX_TAB_INDEX_FIRST_PAGE) { + this.isShowStatusBar = false + } else { + this.isShowStatusBar = true + } + this.currentIndex = index; + this.scrollable = (!this.isHideYaoYi && + this.currentIndex === TabManagerConstants.INDEX_TAB_INDEX_TRADE_PAGE) ? true : false; + this.tabManager.notifyIndexChange(index); + } + }) + .onAreaChange((oldArea: Area, newArea: Area) => { + if (newArea.height) { + let height = newArea.height as number; + GlobalContext.initPageHeight(height); + } + }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .barHeight(this.curBp === 'lg' ? '100%' : 50) + .barWidth(this.curBp == 'lg' ? 50 : '100%') + .animationDuration(0) + .divider({ + strokeWidth: 1, + color: $r('app.color.elements_others_divider_primary') + }) + if (this.curBp !== 'lg') { + Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor($r('app.color.surface_layer1_foreground')) + } + } + .height('100%') + .width('100%') + } +} + +function reqPermissionsFromUser(permissions: Array, context: common.UIAbilityContext): void { + let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); + // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗 + atManager.requestPermissionsFromUser(context, permissions).then((data) => { + let grantStatus: Array = data.authResults; + let length: number = grantStatus.length; + for (let i = 0; i < length; i++) { + if (grantStatus[i] === 0) { + // 用户授权,可以继续访问目标操作 + } else { + // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限 + return; + } + } + // 授权成功 + }).catch((err: BusinessError) => { + console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`); + }) +} diff --git a/future_entry_demo/src/main/ets/pages/IndicatorDraftManagePage.ets b/future_entry_demo/src/main/ets/pages/IndicatorDraftManagePage.ets new file mode 100644 index 0000000..324102c --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/IndicatorDraftManagePage.ets @@ -0,0 +1,19 @@ +/** + * 自编指标草稿管理页面入口 + * + * 此文件是路由入口,实际页面实现在 biz_indicator 模块中 + */ + +import { IndicatorDraftManagePage as IndicatorDraftManagePageImpl } from 'biz_indicator' + +@Entry +@Component +export struct IndicatorDraftManagePage { + build() { + Column() { + IndicatorDraftManagePageImpl() + } + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/IndicatorParamPage.ets b/future_entry_demo/src/main/ets/pages/IndicatorParamPage.ets new file mode 100644 index 0000000..d1ccf80 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/IndicatorParamPage.ets @@ -0,0 +1,19 @@ +/** + * 指标参数设置页面入口 + * + * 此文件是路由入口,实际页面实现在 biz_indicator 模块中 + */ + +import { IndicatorParamPage as IndicatorParamPageImpl } from 'biz_indicator' + +@Entry +@Component +export struct IndicatorParamPage { + build() { + Column() { + IndicatorParamPageImpl() + } + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/IndicatorParserTestPage.ets b/future_entry_demo/src/main/ets/pages/IndicatorParserTestPage.ets new file mode 100644 index 0000000..0d520f7 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/IndicatorParserTestPage.ets @@ -0,0 +1,100 @@ +/** + * indicator_parser HAR 验证测试页面 + * + * Author: LY + * Date: 2026-01-07 + */ + +import { IndicatorParserTask } from 'biz_indicator' + +@Entry +@Component +struct IndicatorParserTestPage { + @State testResult: string = '点击按钮开始测试' + @State isLoading: boolean = false + + build() { + Column() { + Text('Indicator Parser HAR 测试') + .fontSize(24) + .fontWeight(FontWeight.Bold) + .margin({ top: 50, bottom: 30 }) + + Button(this.isLoading ? '测试中...' : '开始测试') + .width('80%') + .height(50) + .fontSize(18) + .enabled(!this.isLoading) + .onClick(() => { + this.runTest() + }) + + Scroll() { + Text(this.testResult) + .fontSize(14) + .fontColor('#333333') + .padding(20) + } + .width('90%') + .height(400) + .margin({ top: 30 }) + .backgroundColor('#F5F5F5') + .borderRadius(10) + } + .width('100%') + .height('100%') + .backgroundColor('#FFFFFF') + } + + async runTest() { + this.isLoading = true + this.testResult = '开始测试...\n' + + try { + // 1. 创建实例 + this.testResult += '\n[1] 创建 IndicatorParserTask 实例...' + const parser = new IndicatorParserTask() + this.testResult += ' ✅ 成功\n' + + // 2. 测试预解析 - 使用简单的 MA 均线公式(异步版本) + this.testResult += '\n[2] 测试预解析 (MA均线公式, 异步版本)...' + const script = 'MA5:MA(CLOSE,5);' + const preParseResult = await parser.getPreParseResAsync(script, '', '') + if (preParseResult) { + this.testResult += ' ✅ 成功\n' + this.testResult += ` 解析结果: ${preParseResult.substring(0, 200)}...\n` + } else { + this.testResult += ' ⚠️ 返回空 (可能需要数据)\n' + } + + // 3. 测试获取列数 + this.testResult += '\n[3] 测试 getColumnNum()...' + const colNum = parser.getColumnNum() + this.testResult += ` ✅ 列数: ${colNum}\n` + + // 4. 测试获取行数 + this.testResult += '\n[4] 测试 getRowNum()...' + const rowNum = parser.getRowNum() + this.testResult += ` ✅ 行数: ${rowNum}\n` + + // 5. 测试清空数据 + this.testResult += '\n[5] 测试 clearData()...' + parser.clearData() + this.testResult += ' ✅ 成功\n' + + // 6. 销毁实例 + this.testResult += '\n[6] 销毁实例...' + parser.destroy() + this.testResult += ' ✅ 成功\n' + + this.testResult += '\n=============================\n' + this.testResult += '🎉 HAR 验证通过!所有接口可正常调用\n' + this.testResult += '=============================\n' + + } catch (error) { + this.testResult += `\n\n❌ 测试失败: ${error}\n` + } finally { + this.isLoading = false + } + } +} diff --git a/future_entry_demo/src/main/ets/pages/LandscapeWebViewPage.ets b/future_entry_demo/src/main/ets/pages/LandscapeWebViewPage.ets new file mode 100644 index 0000000..ca38148 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/LandscapeWebViewPage.ets @@ -0,0 +1,143 @@ +import router from '@ohos.router' +import { WebViewComponent, CustomWebviewController, NavigationBarViewModel } from 'biz_webview'; +import display from '@ohos.display'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; +import { WebViewUrlUtil } from 'biz_webview/src/main/ets/Util/WebViewUrlUtil'; +import { handleBackEvent } from 'biz_common/src/main/ets/pc/PCAdapter'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { emitter } from '@kit.BasicServicesKit'; +import { EmitterConstants } from 'biz_common'; + +const DOMAIN = 0x0000; +const TAG = 'LandscapeWebViewPage'; + +@Entry +@Component +export default struct LandscapeWebViewPage { + private webViewController: CustomWebviewController = new CustomWebviewController() + private url: string = '' + private refreshOnWidthChange: boolean = false; + @State onShow: boolean = false + navigationBarHandler: NavigationBarViewModel = new NavigationBarViewModel(); + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + private windowClass: window.Window | null = null + @StorageLink('displayBreakPoint') @Watch('displayChanged') curBp: string = 'sm' + useCookie: boolean = false + //登录状态改变后刷新web + isLoginRefreshWeb: boolean = false + + registerJSEvent() { + display.getDefaultDisplaySync() + } + + aboutToAppear() { + let routerParam = router.getParams() as Record; + this.url = routerParam.url; + routerParam = WebViewUrlUtil.assign(routerParam, this.url) + try { + this.refreshOnWidthChange = routerParam.refreshOnWidthChange === "true" ? true : false; + } catch (error) { + console.error(`getStringSync failed, error code: ${error.code}, message: ${error.message}.`) + } + this.isLoginRefreshWeb = "" + routerParam['isLoginRefreshWeb'] === "true" ? true : false; + if ("" + routerParam['useCookie'] === "true") { + this.useCookie = true + } + this.registerJSEvent() + // 提前切换到横屏,减少对其他页面的影响 + this.switchToLandscape() + } + + aboutToDisappear() { + } + + // 拦截系统返回上一级 + onBackPress() { + if (this.webViewController && this.webViewController.isAttached && this.webViewController.accessBackward()) { + this.webViewController.backward() + return true + } + return !this.navigationBarHandler.canGoBack() + } + + onPageShow() { + this.onShow = true + // 从后台恢复时重新切换到横屏 + this.switchToLandscape() + } + + onPageHide() { + if (this.onShow == true) { + this.onShow = false + } + this.switchToPortrait() + } + + displayChanged() { + } + + /** + * 切换为横屏显示 + */ + private async switchToLandscape(): Promise { + try { + const mainWindow = await this.context.windowStage.getMainWindow() + this.windowClass = mainWindow + await this.windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE) + } catch (err) { + hilog.warn(DOMAIN, TAG, '切换横屏失败: %{public}s', JSON.stringify(err)); + } + } + + /** + * 切换为竖屏显示 + */ + private async switchToPortrait(): Promise { + try { + const mainWindow = await this.context.windowStage.getMainWindow() + this.windowClass = mainWindow + await this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT) + } catch (err) { + hilog.warn(DOMAIN, TAG, '切换竖屏失败: %{public}s', JSON.stringify(err)); + } + } + + + build() { + Stack({ alignContent: Alignment.TopStart }) { + Column() { + WebViewComponent({ + urlString: this.url, + onShow: this.onShow, + webViewController: this.webViewController, + navigationBarHandler: this.navigationBarHandler, + refreshOnWidthChange: this.refreshOnWidthChange, + useCookie: this.useCookie, + isLoginRefreshWeb: this.isLoginRefreshWeb, + overScrollMode: OverScrollMode.NEVER + }) + .layoutWeight(1) + } + .onKeyEvent((event: KeyEvent) => { + handleBackEvent(event) + }) + .height('100%') + .width('100%') + .padding({ left: 44, right: 44 }) + + // 返回按钮 + Image($r("app.media.navigate_back_icon")) + .width(24) + .height(24) + .margin({ left: 60, top: 16 }) + .onClick(() => { + emitter.emit({ eventId: EmitterConstants.LANDSCAPE_WEBVIEW_BACK }) + router.back(); + }) + } + .height('100%') + .width('100%') + .id('biz_webview/landscape_container') + } +} diff --git a/future_entry_demo/src/main/ets/pages/LauncherPage.ets b/future_entry_demo/src/main/ets/pages/LauncherPage.ets new file mode 100644 index 0000000..e3611bf --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/LauncherPage.ets @@ -0,0 +1,107 @@ +import { AppConfigManager, PreferenceService } from 'biz_common' +import { common } from '@kit.AbilityKit' +import { hilog } from '@kit.PerformanceAnalysisKit' +import router from '@ohos.router' +import { AuthProviderService, HXAuthManager, LoginParamHelper } from 'biz_auth' +import { PrivacyUserDialog } from '../widget/PrivacyUserDialog' +import { StatusBar } from '../widget/StatusBar' +import { window } from '@kit.ArkUI' +import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants' +import { deviceInfo } from '@kit.BasicServicesKit' +import { AdvertisingMonitorService } from 'biz_hxservice' + +/** + * 启动页面 + * 用于承接空白Logo页面,隐私弹窗确认页面以及广告 + * @author menghuiyuan@myhexin.com + */ +@Entry +@Component +struct LauncherPage { + + @State userPrivacyDialogVisible : boolean = false; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context); + // 前提:设置窗口为全屏窗口 + // PC + + if( (deviceInfo.deviceType !== '2in1') ) { + windowClass.setWindowLayoutFullScreen(true); + } else { + windowClass.setWindowLayoutFullScreen(false); + } + + await windowClass.setWindowSystemBarProperties({ + // 颜色属性为ARGB,将蒙尘设置为0%使其透明 + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + aboutToAppear(): void { + this.setSystemBar() + this.userPrivacyDialogVisible = !PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false); + } + + onCancel() { + hilog.info(0xfff1, "LauncherPage.AgreePolicy onCancel", "clicked",""); + //如果用户拒绝了授权,关闭应用 + (getContext(this) as common.UIAbilityContext).terminateSelf(() => {}); + } + + onAccept() { + hilog.info(0xfff1, "LauncherPage.AgreePolicyOnAccept", "clicked",""); + PreferenceService.putVal('Privacy', 'isUserAgree', true, true); + router.replaceUrl( { url : 'pages/Index'}, router.RouterMode.Single, (err) => { + if (err) { + console.error("Failed to start Index Page: " + err) + return; + } + AdvertisingMonitorService.getInstance().init(getContext(this) as common.UIAbilityContext) + AppConfigManager.getInstance().fetchIMEI().then((isSuccess : boolean) => { + if (isSuccess) { + AuthProviderService.getInstance().sendAuthRequest(ConnectionSeq.HQ); + } + }); + }) + } + + build() { + RelativeContainer() { + if (this.userPrivacyDialogVisible) { + PrivacyUserDialog({ + cancel: this.onCancel, + confirm: this.onAccept, + title: $r("app.string.privacy_policy_title"), + content: $r("app.string.privacy_policy_description"), + leftBtnText: $r("app.string.text_disagreed"), + rightBtnText: $r("app.string.text_agreed") + }).zIndex(4) + .align(Alignment.Center) + .alignRules( + { + 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }, + 'bottom': { 'anchor': '__container__', 'align': VerticalAlign.Bottom }, + 'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }, + 'right': { 'anchor': '__container__', 'align': HorizontalAlign.End } + }) + .id('dialog_privacy_policy') + .backgroundColor($r('app.color.dialog_bg_99666666')) + } + Image($r('app.media.launch_logo')) + .objectFit(ImageFit.Auto) + .width('80%').height(205).alignRules({ + 'bottom': { + 'anchor': "__container__", + 'align': VerticalAlign.Bottom + }, + 'middle': { 'anchor': '__container__', 'align': HorizontalAlign.Center }, + }).id("image") + }.width('100%').height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/MinimizedIconPage.ets b/future_entry_demo/src/main/ets/pages/MinimizedIconPage.ets new file mode 100644 index 0000000..a170ba4 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/MinimizedIconPage.ets @@ -0,0 +1,20 @@ +import { DebugService, FloatingMinimizedIcon } from '@b2c-spi/hx_cbas_api'; +import { common } from '@kit.AbilityKit'; + +/** + * 最小化图标页面 + */ +@Entry +@Component +struct MinimizedIconPage { + build() { + Column() { + FloatingMinimizedIcon({ + onExpand: (): void => { DebugService.getInstance().showWindow(getContext(this) as common.UIAbilityContext); } + }) + } + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/QuoteDetailPage.ets b/future_entry_demo/src/main/ets/pages/QuoteDetailPage.ets new file mode 100644 index 0000000..eb5bada --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/QuoteDetailPage.ets @@ -0,0 +1,146 @@ +import { QuotePage, AIAnalysisWebDialogController } from 'biz_quote'; +import common from '@ohos.app.ability.common'; +import { StatusBar } from '../widget/StatusBar'; +import { AbilityConstant } from '@kit.AbilityKit'; +import emitter from '@ohos.events.emitter'; +import { AdsPageConfig, emitAdsPage, EmitterConstants, ScreenUtil } from 'biz_common'; +import { HXLog, HxTags } from 'biz_common'; +import { PeriodValue } from 'biz_futures_chart'; +import { CurveRuntimeData } from 'biz_quote/src/main/ets/util/CurveRuntimeData'; +import { SpotGoodsVarietyManager } from 'biz_chart_data_processor'; +import { router, window } from '@kit.ArkUI'; +import { TradeLineAskToChangeState } from 'biz_quote/src/main/ets/draw/overlay/hx_line/tradeline_manager/TradeLineAskToChangeState'; + +/** + * 分时页承载Page + * @author menghuiyuan@myhexin.com + */ +@Entry +@Component +struct QuoteDetailPage { + private readonly TAG = "QuoteDetailPage" + + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + /** + * 这个pageShow 用于闪电下单,别删除 + */ + @Provide pageShow: boolean = false; + @Provide quoteDetailPageShow: boolean = false; + @Provide showFlashTrade: boolean = false; + @Provide showDrawLineStopLoss: boolean = false; + @Provide showDrawOrder: boolean = false; + @Provide bottomToolsPopupVisible: boolean = false; + @StorageLink('isFullScreen') @Watch('onScreenChange') isLandscape: boolean = false; // 是否横屏 + @Provide isReLandscape: boolean = false; // 是否反向横屏 + // 横屏合约选择列表是否展开 + @Provide landStockListExpand: boolean = false; + @Provide currentPeriod: PeriodValue = CurveRuntimeData.get().timeLinePeriod; + + private resetPageStatusWhenLogin = () => { + this.resetPageStatus(); + } + + aboutToAppear(): void { + emitter.on({ + eventId: EmitterConstants.USER_LOGIN + }, this.resetPageStatusWhenLogin); + // 进入分时/K 线页面时预加载现货类别列表(成功后内存级缓存,失败下次重试) + SpotGoodsVarietyManager.getInstance().requestSpotCategoryIfNeeded() + } + + aboutToDisappear(): void { + HXLog.d("QuoteDetailPage",HxTags.quoteTags + "QuoteDetailPage aboutToDisappear , then unsubscribe (EmitterConstants.USER_LOGIN) ") + emitter.off(EmitterConstants.USER_LOGIN,this.resetPageStatusWhenLogin) + AIAnalysisWebDialogController.close() + } + + // 拦截返回操作 + onBackPress(): boolean | void { + if (ScreenUtil.get().isSmallLand()) { + // 类直板机横屏 + ScreenUtil.get().setPortrait() + } + + if (ScreenUtil.get().isFullScreen) { + ScreenUtil.get().isFullScreen = false + ScreenUtil.get().clickFull = false + return true + } + if (this.showDrawLineStopLoss) { + TradeLineAskToChangeState.getInstance().askToDiscard(() => { + router.back() + }) + return true + } + if (this.showDrawOrder) { + TradeLineAskToChangeState.getInstance().askToDiscard(() => { + router.back() + }) + return true + } + } + + onPageShow(): void { + //分时页前台时启用应用接续 + this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => { + HXLog.i(this.TAG, "setMissionContinueState Active") + }); + this.quoteDetailPageShow = true; + this.pageShow = true; + HXLog.d('QuoteDetailPage', HxTags.entryTags + 'QuoteDetailPage page show triggered') + emitAdsPage(AdsPageConfig.QUOTE_DETAIL_PAGE_ID) + } + + onPageHide(): void { + this.bottomToolsPopupVisible = false; + this.quoteDetailPageShow = false; + this.pageShow = false; + //当分时页在后台时,关闭应用接续功能 + this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE, (result) => { + HXLog.i(this.TAG, "setMissionContinueState InActive") + }); + emitAdsPage(AdsPageConfig.INVALID) + if (AppStorage.get('isAbilityForeGround') == true) { + this.onSwitchToOtherPage() + } + } + + private resetPageStatus() { + if (this.quoteDetailPageShow) { + this.quoteDetailPageShow = false + setTimeout(() => { + this.quoteDetailPageShow = true + }) + } + } + + private onSwitchToOtherPage() { + AIAnalysisWebDialogController.close() + } + + private onScreenChange() { + if (this.isLandscape) { + // 横屏隐藏状态栏 + ScreenUtil.get().setSystemBarEnabled(false); + } else { + ScreenUtil.get().setSystemBarEnabled(true); + } + AIAnalysisWebDialogController.close() + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar({ + bkColor: $r('app.color.surface_layer1_foreground'), + maskVisible: this.bottomToolsPopupVisible, + maskColor: $r('app.color.wt_40_000000') + }) + .visibility(this.isLandscape ? Visibility.None : Visibility.Visible) + QuotePage() + .flexShrink(1) + .height('100%') + } + .backgroundColor($r('app.color.surface_layer1_background')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/SearchPage.ets b/future_entry_demo/src/main/ets/pages/SearchPage.ets new file mode 100644 index 0000000..a318331 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/SearchPage.ets @@ -0,0 +1,79 @@ +import { SearchView} from 'biz_search' +import router from '@ohos.router' +import common from '@ohos.app.ability.common'; +import window from '@ohos.window'; +import { GlobalContext } from 'biz_common/src/main/ets/utils/GlobalContext'; +import { SearchScope, SearchScene } from 'biz_search/src/main/ets/model/SearchParam'; +import { TradeSearchScope } from 'biz_search/src/main/ets/model/TradeSearchScope'; + +@Entry +@Component +struct SearchPage { + @State barHeight:number = 0; + @Provide searchPageShown:boolean = false; + @Provide isFromTrade: boolean = false + + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + // 行情页搜索范围 + searchScope: SearchScope = SearchScope.COMPREHENSIVE_SCOPE + searchScene: string = SearchScene.NORMAL + // 交易下单页搜索范围 + @State tradeSearchScope: TradeSearchScope | null = null + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + let barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + let storeBarHeight = GlobalContext.statusBarHeight + + if (barHeight !== storeBarHeight && barHeight !== 0) { + this.barHeight = barHeight + } else { + this.barHeight = storeBarHeight + } + } + + onBackPress(){ + + } + + onPageShow(): void { + this.searchPageShown = true + } + + onPageHide() { + this.searchPageShown = false + } + + aboutToAppear() { + this.setSystemBar() + let routerParam = router.getParams() as Record + if (routerParam) { + this.isFromTrade = routerParam['isFromTrade']?.toString() === '1' + if (this.isFromTrade) { + // 交易下单页 - 解析 TradeSearchScope + let scopeType = routerParam['scopeType'] as number + this.tradeSearchScope = TradeSearchScope.getSearchScope(scopeType) ?? TradeSearchScope.FUTURES_ALL + } else { + // 行情页 - 使用原有 SearchScope 字符串 + this.searchScope = routerParam['searchScope'] as string || SearchScope.COMPREHENSIVE_SCOPE + this.searchScene = routerParam['searchScene']?.toString() ?? SearchScene.NORMAL + } + } + } + + build() { + Column() { + Blank().height(px2vp(this.barHeight) +2).color($r('app.color.surface_layer1_foreground')).width('100%') + SearchView({ + searchScope: this.searchScope, + searchScene: this.searchScene, + tradeSearchScope: this.tradeSearchScope, + isFromTrade: this.isFromTrade + }) + } + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/SelfCodeSecondPage.ets b/future_entry_demo/src/main/ets/pages/SelfCodeSecondPage.ets new file mode 100644 index 0000000..570be03 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/SelfCodeSecondPage.ets @@ -0,0 +1,79 @@ +import router from '@ohos.router' +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; +import { StatusBar } from '../widget/StatusBar'; +import { SelfCodeSecondPage } from 'biz_selfcode'; + +@Entry + +@Component +struct SecondPage { + @Provide newsPageShow:number = 0; //0 not show 1 shown + @State barHeight:number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + @State titlePrefix:string = ''; + @State codeListStr:string = ''; + @State marketListStr:string = ''; + @State stockListStr:string = '' + @State initIndex:number = 0; + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + this.barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + async resetSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + /** + * 从路由中获取参数并初始化页面数据 + */ + aboutToAppear() { + let routerParam = router.getParams() as Record; + this.codeListStr = routerParam.codeList; + this.marketListStr = routerParam.marketList; + this.stockListStr = routerParam.stockList + this.titlePrefix = routerParam.title; + + } + aboutToDisappear(){ + } + + build() { + Column() { + StatusBar() + this.buildSecondPage(this.stockListStr,this.titlePrefix,this.initIndex) + //this.buildTestPage(this.codeListStr, this.marketListStr, this.titlePrefix) + } + .width('100%') + .height('100%') + } + + @Builder + buildSecondPage(stockListStr:string, titlePrefix:string, initIndex:number ) { + SelfCodeSecondPage({selfCodeList:stockListStr,titleWord:titlePrefix,initIndex:initIndex}) + } + @Builder + buildTestPage(code:string, market:string,title:string) { + Text('入参'+code+market+title) + .fontSize(100) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/StockListPage.ets b/future_entry_demo/src/main/ets/pages/StockListPage.ets new file mode 100644 index 0000000..59b2696 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/StockListPage.ets @@ -0,0 +1,137 @@ +import { + buildHeaderRowData, + CellData, + HeaderItem, + OriginalData, + parseHeaderConfig, + RowData, + SortStatus, + TableData +} from '@b2b/hq_table' +import router from '@ohos.router' +import util from '@ohos.util' +import { HQTableComponent, StockListPageParam } from 'biz_market' +import { SubPageBar } from 'biz_selfcode' +import common from '@ohos.app.ability.common' +import { StatusBar } from '../widget/StatusBar' +import { emitter } from '@kit.BasicServicesKit' +import { EmitterConstants } from 'biz_common/Index' + +@Entry +@Component +struct StockListPage { + private frameId: number = 0 + private pageId: number = 0 + private headerConfig: Array = [] + //首次进入页面的默认排序字段 + private defaultSortId?: number + //默认排序类型 + private defaultSortStatus: SortStatus = SortStatus.DESC + //是否需要根据默认排序表头重新设置表头位置 + private isNeedReRankHeader: boolean = false + //页面标题 + private title: string = "" + @State hasInit: boolean = false + @Provide tableData: TableData = TableData.createEmptyTableData() + @State header: RowData = new RowData([], [], new OriginalData()) + @State requestNotifier: number = 0; + @State barHeight: number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + private relinkCb = () => { + this.requestNotifier++; + } + aboutToAppear() { + let param = router.getParams() as StockListPageParam + this.title = param.pageTitle + this.frameId = param.pageId + this.pageId = param.pageId + this.defaultSortId = param.defaultSortId + this.defaultSortStatus = param.defaultSortStatus + this.isNeedReRankHeader = param.isNeedReRankHeader + + if (param.headerConfig) { + this.headerConfig = param.headerConfig + this.initHeaderRow() + } else if (param.headerConfigFile) { + getContext(this).resourceManager.getRawFileContent(param.headerConfigFile, (error, value) => { + if (error) { + console.log(error.message) + } else { + let textDecoder = util.TextDecoder.create('utf-8') + this.headerConfig.push(...parseHeaderConfig(textDecoder.decodeWithStream(value))) + this.initHeaderRow() + } + }) + } + + this.subscribeNetRelink() + } + + subscribeNetRelink() { + emitter.on({ + eventId:EmitterConstants.NETWORK_TCP_RECONNECT, + },this.relinkCb) + } + + aboutToDisappear() { + emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT,this.relinkCb) + } + + private initHeaderRow() { + if (this.headerConfig.length > 0) { + let headerRow = buildHeaderRowData(this.headerConfig) + this.initDefaultSort(headerRow) + this.header = headerRow + this.hasInit = true + } + } + + private initDefaultSort(header: RowData) { + if (typeof this.defaultSortId !== 'undefined') { + header.forEachCell((cellData: CellData) => { + if (cellData.dataId === this.defaultSortId) { + cellData.sortStatus = this.defaultSortStatus + header.currentSortCell = cellData + } + }) + + if (this.isNeedReRankHeader && header.currentSortCell) { + let index = header.scrollableCols.indexOf(header.currentSortCell) + if (index >= 0) { + header.scrollableCols.splice(index, 1) + header.scrollableCols.splice(1, 0, header.currentSortCell) + + let configIndex = index + header.fixCols.length + let configItem = this.headerConfig.splice(configIndex, 1)[0] + this.headerConfig.splice(1 + header.fixCols.length, 0, configItem) + } + } + } + } + + onPageShow() { + this.requestNotifier++ + } + + build() { + Column() { + StatusBar() + //Blank().height(px2vp(this.barHeight) +2).color($r('app.color.subpage_title_red')).width('100%') + SubPageBar({ + title: this.title + }) + Column() { + if (this.hasInit) { + HQTableComponent({ + frameId: this.frameId, + pageId: this.pageId, + header: this.header, + headerConfig: this.headerConfig, + requestNotifier: this.requestNotifier + }) + } + }.layoutWeight(1) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/SystemIndicatorsTestPage.ets b/future_entry_demo/src/main/ets/pages/SystemIndicatorsTestPage.ets new file mode 100644 index 0000000..2ee1fea --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/SystemIndicatorsTestPage.ets @@ -0,0 +1,162 @@ +/** + * SystemIndicatorsManage 验证测试页面 + * + * Author: LY + * Date: 2026-01-12 + */ + +import { SystemIndicatorsManage, CategoryInfo } from 'biz_indicator' +import { IndicatorInfoModel } from 'biz_indicator' + +@Entry +@Component +struct SystemIndicatorsTestPage { + @State testResult: string = '点击按钮开始测试' + @State isLoading: boolean = false + private context: Context = getContext(this) + + build() { + Column() { + Text('SystemIndicatorsManage 测试') + .fontSize(22) + .fontWeight(FontWeight.Bold) + .margin({ top: 50, bottom: 30 }) + + Button(this.isLoading ? '测试中...' : '开始测试') + .width('80%') + .height(50) + .fontSize(18) + .enabled(!this.isLoading) + .onClick(() => { + this.runTest() + }) + + Scroll() { + Text(this.testResult) + .fontSize(13) + .fontColor('#333333') + .padding(16) + } + .width('90%') + .layoutWeight(1) + .margin({ top: 20 }) + .backgroundColor('#F5F5F5') + .borderRadius(10) + } + .width('100%') + .height('100%') + .padding({ bottom: 30 }) + .backgroundColor('#FFFFFF') + } + + async runTest() { + this.isLoading = true + this.testResult = '========== 开始测试 ==========\n' + + try { + const manage = SystemIndicatorsManage.getInstance() + + // [1] 从 biz_indicator HSP 模块的 rawfile 初始化数据 + this.testResult += '\n[1] 从 biz_indicator HSP 模块加载数据...\n' + await manage.initDataFromModule(this.context) + if (manage.isInitialized()) { + this.testResult += ' ✅ 初始化成功\n' + this.testResult += ` 版本: ${manage.getDataVersion()}\n` + } else { + this.testResult += ' ❌ 初始化失败\n' + return + } + + // [2] 验证指标数量 + this.testResult += '\n[2] 验证指标数量...\n' + const count = manage.getIndicatorCount() + this.testResult += ` 指标总数: ${count}\n` + if (count === 55) { + this.testResult += ' ✅ 数量正确 (55个)\n' + } else { + this.testResult += ` ⚠️ 数量: ${count}\n` + } + + // [3] 获取全部指标 + this.testResult += '\n[3] 获取全部指标 getAllIndicators()...\n' + const allIndicators = manage.getAllIndicators() + this.testResult += ` 返回 ${allIndicators.length} 个指标\n` + if (allIndicators.length > 0) { + const first = allIndicators[0] + this.testResult += ` 第一个: ${first.indicatorId} - ${first.name}\n` + this.testResult += ' ✅ 成功\n' + } + + // [4] 获取主图指标 + this.testResult += '\n[4] 获取主图指标 getMainIndicators()...\n' + const mainIndicators = manage.getMainIndicators() + this.testResult += ` 主图指标数: ${mainIndicators.length}\n` + if (mainIndicators.length > 0) { + const names: string[] = [] + for (let i = 0; i < Math.min(5, mainIndicators.length); i++) { + names.push(mainIndicators[i].indicatorId) + } + this.testResult += ` 前5个: ${names.join(', ')}\n` + this.testResult += ' ✅ 成功\n' + } + + // [5] 获取副图指标 + this.testResult += '\n[5] 获取副图指标 getViceIndicators()...\n' + const viceIndicators = manage.getViceIndicators() + this.testResult += ` 副图指标数: ${viceIndicators.length}\n` + if (viceIndicators.length > 0) { + const names: string[] = [] + for (let i = 0; i < Math.min(5, viceIndicators.length); i++) { + names.push(viceIndicators[i].indicatorId) + } + this.testResult += ` 前5个: ${names.join(', ')}\n` + this.testResult += ' ✅ 成功\n' + } + + // [6] 查询单个指标 + this.testResult += '\n[6] 查询单个指标 getIndicatorInfo("MA")...\n' + const maIndicator = manage.getIndicatorInfo('MA') + if (maIndicator) { + this.testResult += ` 名称: ${maIndicator.name}\n` + this.testResult += ` 源码长度: ${maIndicator.sourceCode.length} 字符\n` + this.testResult += ` 参数数量: ${maIndicator.paramsArray.length}\n` + this.testResult += ` 线条数量: ${maIndicator.lineStyleArray.length}\n` + this.testResult += ' ✅ 成功\n' + } else { + this.testResult += ' ❌ 未找到 MA 指标\n' + } + + // [7] 搜索功能 + this.testResult += '\n[7] 搜索指标 searchIndicators("MACD")...\n' + const searchResults = manage.searchIndicators('MACD') + this.testResult += ` 搜索结果数: ${searchResults.length}\n` + if (searchResults.length > 0) { + for (let i = 0; i < searchResults.length; i++) { + const item = searchResults[i] + this.testResult += ` - ${item.indicatorId}: ${item.name}\n` + } + this.testResult += ' ✅ 成功\n' + } + + // [8] 获取分类列表 + this.testResult += '\n[8] 获取分类列表 getAllCategories()...\n' + const categories = manage.getAllCategories() + this.testResult += ` 分类数量: ${categories.length}\n` + for (let i = 0; i < categories.length; i++) { + const cat = categories[i] + this.testResult += ` - ${cat.code}: ${cat.name}\n` + } + this.testResult += ' ✅ 成功\n' + + // 测试完成 + this.testResult += '\n==============================\n' + this.testResult += '🎉 所有测试通过!\n' + this.testResult += '==============================\n' + + } catch (error) { + this.testResult += `\n\n❌ 测试异常: ${error}\n` + } finally { + this.isLoading = false + } + } +} diff --git a/future_entry_demo/src/main/ets/pages/TemporaryBlockWebPage.ets b/future_entry_demo/src/main/ets/pages/TemporaryBlockWebPage.ets new file mode 100644 index 0000000..4899b20 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/TemporaryBlockWebPage.ets @@ -0,0 +1,119 @@ +import router from '@ohos.router' +import web_webview from '@ohos.web.webview'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; + +/** + * 暂时使用的页面 + */ +@Entry +@Component +export default struct TemporaryBlockWebPage { + private url: string = ''; + private title: string = '' + private webController: WebviewController = new web_webview.WebviewController; + + @State barHeight:number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + this.barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + async resetSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + aboutToAppear() { + this.setSystemBar() + const routerParam = router.getParams() as Record; + this.url = routerParam.url; + this.title = routerParam.title || ''; + } + + aboutToDisappear(){ + this.resetSystemBar() + try { + this.webController.stop() + } catch (e) { + // WebView may already be detached. + } + } + + build() { + Column() { + Blank().height(px2vp(this.barHeight) +2).color($r('app.color.subpage_title_red')).width('100%') + Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Start }) { + Column() { + Image($r('app.media.toolbar_arrow_left')) + .width(19) + .height(24) + .fillColor('#FFFFFF') + .onClick(() => { + router.back(); + }); + }.margin({ right: 16 }) + Text(this.title).fontSize(19).fontColor('#FFFFFF') + }.margin({ left: 16 }) + } + .height(44).backgroundColor($r('app.color.navbar_bg_red')).width('100%') + + Web({ + src: this.url, + controller: this.webController + }).domStorageAccess(true) + .mixedMode(MixedMode.All) + .flexGrow(1) + .flexShrink(1) + .javaScriptProxy({ + object: {}, + name: 'Falcon', + methodList: [], + controller: this.webController, + }) + .onPageEnd(() => { + // 暂时处理办法, 不支持用户关闭回顾按钮和历史分时,手动去掉 + this.webController.runJavaScript( + `var elementToRemove = document.querySelector('.fixed.flex.items-center.bg-foreground'); + if (elementToRemove) { + elementToRemove.parentNode.removeChild(elementToRemove); + } + var leftArrow = document.querySelector("#bottom-popup-window > div.ml-auto.mr-16 > div.date-picker-container.flex.absolute.top-59.right-26 > div.date-picker-arrow-container.mr-4.text-sec.text-16.z-99.pl-4"); + if (leftArrow) { + leftArrow.style.color = "rgba(0,0,0,.2)"; + } + var parentElement = document.querySelector("#bottom-popup-window > div.ml-auto.mr-16 > div.date-picker-container.flex.absolute.top-59.right-26") + parentElement.addEventListener('click', function(event) { + var targetElement = event.target.closest("#bottom-popup-window > div.ml-auto.mr-16 > div.date-picker-container.flex.absolute.top-59.right-26 > div.date-picker-arrow-container.mr-4.text-sec.text-16.z-99.pl-4"); + var dateEl = event.target.closest("#bottom-popup-window > div.ml-auto.mr-16 > div.date-picker-container.flex.absolute.top-59.right-26 > div.date-picker-content.flex.items-center.justify-center.ml-auto.mb-5.z-99.intraDay.expanded"); + if (targetElement || dateEl) { + event.stopPropagation(); + targetElement.style.color = "rgba(0,0,0,.2)"; + }; + }, true); + `) + }) + } + .height('100%') + .width('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/TradeBizPageContainer.ets b/future_entry_demo/src/main/ets/pages/TradeBizPageContainer.ets new file mode 100644 index 0000000..00c1f69 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/TradeBizPageContainer.ets @@ -0,0 +1,345 @@ +import { EmitterConstants, TradePageTabManager } from 'biz_common'; +import { + MoniFutureAccountManager, + MoniPage, TradeConstant, TradeHomePage, TradePage, WeituoLoginPage } from 'biz_trade'; +import { AccountProvider } from 'biz_trade/src/main/ets/features/account/models/AccountProvider'; +import { emitter } from '@kit.BasicServicesKit'; +import { TradeConstants } from 'biz_trade/src/main/ets/constants/TradeConstants'; +import { TradeApiInit } from 'biz_trade/src/main/ets/trade/api/TradeApiInit'; +import { TradeCustomTabManager } from 'biz_trade/src/main/ets/trade/manager/TradeCustomTabManager'; +import { HXUserService } from 'biz_hxservice'; +import { router } from '@kit.ArkUI'; +import { AutoLoginManager } from 'biz_trade/src/main/ets/trade/manager/AutoLoginManager'; +import { Account } from 'biz_trade/src/main/ets/constants/TypeConstants'; +import { TradeApiManager } from 'biz_trade/src/main/ets/trade/api/TradeApiManager'; +import { TradeOrderPageListType, fromValue } from 'biz_trade/src/main/ets/pages/homepage/TradeHomeConstants'; +import { TradeSettingUtils } from 'biz_trade/src/main/ets/utils/TradeSettingUtils'; +import { TradeParamManager } from 'biz_trade/src/main/ets/utils/TradeParamManager'; + +const TAB_INDEX_REAL = 0 +const TAB_INDEX_MONI = 1 + +/** + * 交易tab的通用父页面,主要用来实现顶部tab切换(A股、港美股、基金、期货) + */ +@Preview +@Component +export default struct TradeBizPageContainer { + @ObjectLink tabManager: TradePageTabManager; + @State @Watch('onTabIndexChange') currentIndex: number = 0; + /** + * 是否有登录的实盘账号 + */ + @State hasRealLoggedAccount: boolean = false + /** + * 是否有登录的模拟账号 + */ + @State hasSimulateLoggedAccount: boolean = false + @State isVisible: boolean = false + /** + * 是否显示交易下单页面 + */ + @State showTradeOrderPage: boolean = false + /** + * 交易下单tab index + */ + @State currentTabIndex: number = 0 + + /** + * 当前账号 + */ + @State currentAccount: Account = undefined + private swiperController = new SwiperController() + + /** + * 登录订阅回调 + */ + private hasLoginAccountCallBack = () => { + this.hasRealLoggedAccount = AccountProvider.getInstance().hasRealLoggedAccount() + this.hasSimulateLoggedAccount = AccountProvider.getInstance().hasSimulateLoggedAccount() + this.currentAccount = AccountProvider.getInstance().getCurrentAccount() + } + + /** + * 登出订阅回调 + */ + private loginOutAccountCallBack = () => { + this.hasRealLoggedAccount = AccountProvider.getInstance().hasRealLoggedAccount() + this.hasSimulateLoggedAccount = AccountProvider.getInstance().hasSimulateLoggedAccount() + this.showTradeOrderPage = false + this.currentAccount = undefined + } + + /** + * 交易回报点击回调 + */ + private CJHBPushCallBack = (eventData: emitter.EventData) => { + if (this.showTradeOrderPage) { + // 交易下单页面已经有监听处理 + return + } + // 处理在交易首页需要切换到交易下单再切列表 + if (eventData && eventData.data) { + let tradeType: number | undefined = eventData.data[TradeConstant.KEY_TRADE_TYPE] + if (tradeType) { + TradeCustomTabManager.get().setTradeType(tradeType); + } + let tableIndex: number | undefined = eventData.data[TradeConstant.KEY_TABLE_INDEX] + this.gotoOrderPage(fromValue(tableIndex ?? 0)) + } + } + + onTabIndexChange() { + this.swiperController.changeIndex(this.currentIndex) + } + + aboutToAppear(): void { + TradeApiInit.getInstance().initTradeCacheData(); + MoniFutureAccountManager.getInstance().addMoniAccount() + this.tabManager.visibilityChangeListener = (isVisible) => { + this.isVisible = isVisible + if (isVisible) { + this.onPageShow() + }else { + this.onPageHide() + } + } + emitter.on({ eventId: TradeConstants.TRADE_ACCOUNT_LOGOUT }, this.loginOutAccountCallBack) + emitter.on(EmitterConstants.CJHB_PUSH_CLICK, this.CJHBPushCallBack) + } + + aboutToDisappear(): void { + emitter.off(TradeConstants.TRADE_ACCOUNT_LOGOUT, this.loginOutAccountCallBack) + emitter.off(EmitterConstants.CJHB_PUSH_CLICK, this.CJHBPushCallBack) + } + + onPageShow(): void { + const needEnterOrderPage = TradeParamManager.get().getNeedEnterOrderPage() + this.hasRealLoggedAccount = AccountProvider.getInstance().hasRealLoggedAccount() + this.hasSimulateLoggedAccount = AccountProvider.getInstance().hasSimulateLoggedAccount() + this.currentAccount = AccountProvider.getInstance().getCurrentAccount() + if (needEnterOrderPage) { + this.showTradeOrderPage = TradeParamManager.get().getNeedEnterOrderPage() + this.currentTabIndex = TradeOrderPageListType.POSITION + TradeParamManager.get().setNeedEnterOrderPage(false) + this.currentIndex = this.currentAccount?.isMoni() ? TAB_INDEX_MONI : TAB_INDEX_REAL + } + this.changeTabAccordingAccount() + emitter.on({eventId: TradeConstants.TRADE_ACCOUNT_LOGIN}, this.hasLoginAccountCallBack) + } + + onPageHide(): void { + emitter.off(TradeConstants.TRADE_ACCOUNT_LOGIN, this.hasLoginAccountCallBack) + } + + build() { + Flex({ direction: FlexDirection.Column }) { + if (this.currentAccount && ((this.hasRealLoggedAccount && this.currentIndex == TAB_INDEX_REAL) || (this.hasSimulateLoggedAccount && this.currentIndex == TAB_INDEX_MONI))) { + if (this.showTradeOrderPage) { + TradePage({ + tradeTabShow: this.tabManager.tabVisibility, + isShowStatusBar: false, + currentTabIndex: this.currentTabIndex, + attachPage: "TradePageFromIndex", + clickBack: () => { + this.onOrderPageBack() + } + }) + } else { + this.TitleBuilder() + TradeHomePage({ + tradeTabShow: this.tabManager.tabVisibility, + tradeAccount: this.currentAccount, + jumpOrderPage: (listType) => { + this.gotoOrderPage(listType) + }, + loginCallBack: { + onSwitchSuccess: () => { + this.loginSuccess(false) + }, + onSwitchFailed: () => {} + } + }) + } + } else { + this.TitleBuilder() + Divider().vertical(false).height(0.5).color($r('app.color.elements_others_divider_primary')) + Swiper() { + if (this.currentIndex == TAB_INDEX_MONI) { + MoniPage({ + onForeground: this.isVisible, + loginSuccess: () => { + this.loginSuccess() + } + }) + } else { + WeituoLoginPage({ + tradeTabShow: this.tabManager.tabVisibility, + loginSucc: () => { + this.loginSuccess() + } + }) + } + } + .index(this.currentIndex) + .indicator(false) + .disableSwipe(true) + } + } + .width('100%') + .height('100%') + } + + @Builder + TitleBuilder() { + Row() { + Blank() + .width(40) + .height('100%') + + Row() { + Row() { + Text("实盘") + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(this.currentIndex === TAB_INDEX_REAL ? 22 : 16) + .fontWeight(this.currentIndex === TAB_INDEX_REAL ? FontWeight.Medium : 400) + .textAlign(TextAlign.Center) + .margin({ top: this.currentIndex === TAB_INDEX_REAL ? 0 : 6, right: 12 }) + .onClick(() => { + if (this.currentIndex == TAB_INDEX_REAL) { + return + } + this.currentIndex = TAB_INDEX_REAL; + this.selectTabReLogin() + }) + + Text("模拟") + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(this.currentIndex === TAB_INDEX_REAL ? 16 : 22) + .fontWeight(this.currentIndex === TAB_INDEX_REAL ? 400 : FontWeight.Medium) + .textAlign(TextAlign.Center) + .margin({ top: this.currentIndex === TAB_INDEX_REAL ? 6 : 0 }) + .onClick(() => { + if (this.currentIndex == TAB_INDEX_MONI) { + return + } + AutoLoginManager.getInstance().cancelAutoLoginRequest('click moni btn') + this.currentIndex = TAB_INDEX_MONI; + this.selectTabReLogin() + }) + } + .height(22) + } + .height('100%') + .justifyContent(FlexAlign.Center) + .layoutWeight(1) + + Row() { + Image($r('app.media.icon_bot_24')) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(24) + .height(24) + .objectFit(ImageFit.Contain) + .onClick(() => { + this.onAgentClick() + }) + } + .margin({ right: 16 }) + } + .backgroundColor($r('app.color.surface_layer1_foreground')) + .height(44) + .width('100%') + .justifyContent(FlexAlign.Center) + } + + /** + * 切换tab重新登录 + */ + private selectTabReLogin() { + let tradeAccount: Account = undefined + if (this.currentIndex == TAB_INDEX_REAL) { + tradeAccount = AccountProvider.getInstance().getLastRealLoginAccount() + } else { + tradeAccount = AccountProvider.getInstance().getLastSimulateLoginAccount() + } + TradeApiManager.getInstance().reLoginAccount('trade tab', tradeAccount, () => { + this.loginSuccess(false) + }) + } + + /** + * 登录成功 + */ + private loginSuccess(canEnterOrderPage: boolean = true) { + this.hasRealLoggedAccount = AccountProvider.getInstance().hasRealLoggedAccount() + this.hasSimulateLoggedAccount = AccountProvider.getInstance().hasSimulateLoggedAccount() + this.currentAccount = AccountProvider.getInstance().getCurrentAccount() + // 登录后根据设置自动进入下单页(切换tab触发的登录除外) + if (this.currentAccount && canEnterOrderPage && TradeSettingUtils.getInstance().isDirectEnterOrderPage()) { + this.currentTabIndex = TradeOrderPageListType.POSITION + this.showTradeOrderPage = true + } + } + + /** + * 从交易下单页返回 + */ + private onOrderPageBack() { + this.showTradeOrderPage = false + this.changeTabAccordingAccount() + } + + /** + * 根据账号切换tab + */ + private changeTabAccordingAccount() { + if (this.hasRealLoggedAccount && this.hasSimulateLoggedAccount) { + if (AccountProvider.getInstance().getCurrentAccount()?.isMoni()) { + this.currentIndex = TAB_INDEX_MONI + } else { + this.currentIndex = TAB_INDEX_REAL + } + } + } + + /** + * 跳转到交易下单页 + */ + private gotoOrderPage(listType: TradeOrderPageListType) { + this.currentTabIndex = listType + this.showTradeOrderPage = true + } + + private onAgentClick() { + if (this.autoLogin()) { + return + } + router.pushUrl({ + url: 'pages/WebViewComponentPage', params: { + url: 'https://eq.10jqka.com.cn/ai/webapp/?source=ths_liejin_qihuo&llm=1&showHeader=1&navLeftShow=0&comefrom=%1s&isSupportInputUpload=1&fullscreen=1', + title: '', + updateH5Title: '0' + } + }, + router.RouterMode.Single, (err) => { + if (err) { + console.error(`go to message page failed , cause : ${err.message}`) + } + }) + } + + private autoLogin(): boolean { + if (!HXUserService.getInstance().isLoggedIn()) { + const topRoutePath = router.getState().path + router.pushUrl({ + url: 'pages/login/LoginPage', + params: { + flexSwitchFlag: true, + sourcePage: `${topRoutePath}${router.getState().name}` + } + }) + return true; + } + return false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/TrendForecastPage.ets b/future_entry_demo/src/main/ets/pages/TrendForecastPage.ets new file mode 100644 index 0000000..1ddd3cf --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/TrendForecastPage.ets @@ -0,0 +1,23 @@ +import router from '@ohos.router' +import { TrendForecastPageParams, TrendForecastView } from 'biz_forecast' + +@Entry +@Component +struct TrendForecastPage { + @State params: TrendForecastPageParams = new TrendForecastPageParams() + + aboutToAppear(): void { + const routerParams = router.getParams() as Record + this.params = TrendForecastPageParams.fromRecord(routerParams) + } + + build() { + Column() { + TrendForecastView({ + params: this.params + }) + } + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/WebViewComponentPage.ets b/future_entry_demo/src/main/ets/pages/WebViewComponentPage.ets new file mode 100644 index 0000000..7bebd30 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/WebViewComponentPage.ets @@ -0,0 +1,266 @@ +import router from '@ohos.router' +import { WebViewComponent, CustomWebviewController, WebViewRightBarButton, NavigationBarViewModel, WebViewCallbackRegistry } from 'biz_webview'; +import display from '@ohos.display'; +import window from '@ohos.window'; +import common from '@ohos.app.ability.common'; +import { StatusBar } from '../widget/StatusBar'; +import { WebViewUrlUtil } from 'biz_webview/src/main/ets/Util/WebViewUrlUtil'; +import { KeyCode } from '@kit.InputKit'; +import { handleBackEvent } from 'biz_common/src/main/ets/pc/PCAdapter'; +import { VideoComponent } from 'lib_media'; +import { GlobalContext, WebJumpLinkWhiteManager } from 'biz_common'; +import { getResourceString } from '@b2c/first_page/src/main/ets/util/ResourceUtil'; + +// 返回回调注册 key +const BACK_PRESS_CALLBACK_KEY = 'backPressCallbackKey' + +@Entry +@Component +export default struct WebViewComponentPage { + private webViewController: CustomWebviewController = new CustomWebviewController() + private url: string = '' + @Prop title: string | Resource = '' + @Prop fullscreen: boolean = false + @Prop showStatusBar: boolean = true + private refreshOnWidthChange: boolean = false; + @State onShow: boolean = false + @State barHeight: number = 0; + navigationBarHandler: NavigationBarViewModel = new NavigationBarViewModel(); + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + @StorageLink('displayBreakPoint') @Watch('displayChanged') curBp: string = 'sm' + useCookie: boolean = false + @Provide videoPlayerUrl: string | null = '' + @State videoComponentHeight: number = 225 + @State updateH5Title: boolean = true + // 是否显示三方资讯标题 + @State isShowOtherTitle: boolean = false; + @State otherUrl: string | null = null; + //登录状态改变后刷新web + isLoginRefreshWeb:boolean = false + // 自定义返回事件回调 + private onBackPressCallback?: () => void + private backPressCallbackKey?: string + + registerJSEvent() { + display.getDefaultDisplaySync() + } + + // 网页标题变更 + onTitleReceive = (title: string) => { + this.title = title + } + + aboutToAppear() { + let routerParam = router.getParams() as Record; + this.url = routerParam.url; + routerParam = WebViewUrlUtil.assign(routerParam, this.url) + let defaultTitle = '' + try { + defaultTitle = this.context.resourceManager.getStringSync($r('app.string.webview_default_title').id); + this.refreshOnWidthChange = routerParam.refreshOnWidthChange === "true" ? true : false; + } catch (error) { + console.error(`getStringSync failed, error code: ${error.code}, message: ${error.message}.`) + } + this.title = routerParam.title || defaultTitle; + const webId = routerParam['webid'] + this.fullscreen = + routerParam['fullscreen'] == '1' || routerParam['notitlebar'] == '1' || routerParam['ishiddenbar'] == '1' || + webId == '2305' || webId == '4224' || + routerParam['express'] == 'fullscreen_web' + if (this.fullscreen) { + this.showStatusBar = false + } + this.isLoginRefreshWeb = ""+routerParam['isLoginRefreshWeb'] === "true" ? true : false; + if (routerParam['showStatusBar'] == '1' && routerParam['StayHighForStatusBar'] != '0' || webId == '4224') { + this.showStatusBar = true + } + if (""+routerParam['useCookie'] === "true") { + this.useCookie = true + } + this.updateH5Title = routerParam['updateH5Title'] != '-1' + this.isShowOtherTitle = routerParam['isShowOtherTitle'] == 'true' + + const isOtherUrl = WebJumpLinkWhiteManager.getInstance().isNotInWhiteUrls(this.url); + if (this.isShowOtherTitle && isOtherUrl) { + // 更改标题为三方风险提示 + this.title = getResourceString(GlobalContext.get(), $r('app.string.bridge_risk_statement')) + this.otherUrl = this.url + } + + // 获取返回事件回调 + const callbackKey = routerParam[BACK_PRESS_CALLBACK_KEY] + if (callbackKey) { + this.backPressCallbackKey = callbackKey + this.onBackPressCallback = WebViewCallbackRegistry.getBackPressCallback(callbackKey) + } + this.registerJSEvent() + this.initThirdArticleWhiteList() + } + + aboutToDisappear() { + if (this.backPressCallbackKey) { + WebViewCallbackRegistry.unregisterBackPressCallback(this.backPressCallbackKey) + this.backPressCallbackKey = undefined + } + + // 停止 WebView 加载,释放 Chromium 渲染资源 + try { + if (this.webViewController && this.webViewController.isAttached) { + this.webViewController.stop() + } + } catch (e) { + // WebView 可能已销毁 + } + } + + // 拦截系统返回上一级 + onBackPress() { + // 通知业务方用户点击了返回 + if (this.onBackPressCallback) { + this.onBackPressCallback() + } + + if (this.webViewController && this.webViewController.isAttached && this.webViewController.accessBackward()) { + this.webViewController.backward() + return true + } + return !this.navigationBarHandler.canGoBack() + } + + onPageShow() { + this.onShow = true + } + + onPageHide() { + if (this.onShow == true) { + this.onShow = false + } + } + + playerVideoVisibility(): Visibility { + const url = this.videoPlayerUrl + if (!url || url == null || url == '') { + return Visibility.None + } + return Visibility.Visible + } + + displayChanged() { + this.videoPlayerUrl = null + } + + /** + * 初始化第三方风险文章白名单列表 + * 这里做一个兜底,防止客户端启动后,第一次请求白名单接口失败 + * 导致功能不正常 + */ + private initThirdArticleWhiteList() { + if (WebJumpLinkWhiteManager.getInstance().isWhiteListEmpty()) { + WebJumpLinkWhiteManager.getInstance().loadWhiteJumpLink(); + } + } + + build() { + Column() { + if (this.showStatusBar) { + StatusBar() + } + if (!this.fullscreen) { + RelativeContainer() { + Image($r("app.media.icon_arrow_left_24")) + .fillColor($r('app.color.elements_icon_tertiary')) + .width(24) + .height(24) + .alignRules({ + center: { anchor: "__container__", align: VerticalAlign.Center }, + left: { anchor: "__container__", align: HorizontalAlign.Start } + }) + .margin({ left: 16 }) + .hoverEffect(HoverEffect.Highlight) + .onClick(() => { + if (!this.webViewController.isAttached) { + return + } + + if (this.onBackPressCallback) { + this.onBackPressCallback() + } + + if (this.webViewController.accessBackward()) { + this.webViewController.backward() + } else if (this.navigationBarHandler.canGoBack()) { + router.back(); + } + }) + .id('left') + Text(this.title) + .maxFontSize(19) + .minFontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '70%' }) + .alignRules({ + center: { anchor: "__container__", align: VerticalAlign.Center }, + middle: { anchor: "__container__", align: HorizontalAlign.Center } + }) + WebViewRightBarButton({ navigationBarHandler: this.navigationBarHandler }) + .alignRules({ + center: { anchor: "__container__", align: VerticalAlign.Center }, + right: { anchor: "__container__", align: HorizontalAlign.End } + }) + .margin({ right: 8 }) + } + .height(44).backgroundColor($r("app.color.surface_layer1_foreground")).width('100%') + } + + VideoComponent({ onShow: this.onShow }) + .width('100%') + .height(this.videoComponentHeight) + .visibility(this.playerVideoVisibility()) + .onAreaChange((oldValue, newValue) => { + const ratio = 9 / 16 + let newWidth = newValue.width as number + if (newValue.width > 0 && this.videoComponentHeight !== newWidth * ratio) { + this.videoComponentHeight = newWidth * ratio + } + + }) + + WebViewComponent({ + urlString: this.url, + onShow: this.onShow, + webViewController: this.webViewController, + onTitleReceive: this.onTitleReceive, + navigationBarHandler: this.navigationBarHandler, + refreshOnWidthChange: this.refreshOnWidthChange, + useCookie: this.useCookie, + isLoginRefreshWeb:this.isLoginRefreshWeb, + overScrollMode: OverScrollMode.NEVER, + updateH5Title: this.updateH5Title, + isShowOtherTitle: this.isShowOtherTitle, + otherUrl: this.otherUrl, + receiveFullScreenEvent: (isFullScreen: boolean) => { + this.fullscreen = isFullScreen + if (isFullScreen) { + this.showStatusBar = false + } + }, + onMediaUrlChanged: (url: string | null) => { + this.videoPlayerUrl = url + }, + }) + .margin({ top: 0 }) + .layoutWeight(1) + + } + + .onKeyEvent((event: KeyEvent) => { + handleBackEvent(event) + }) + .height('100%') + .width('100%') + .id('biz_webview/container') + } +} diff --git a/future_entry_demo/src/main/ets/pages/aboutus/AboutUs.ets b/future_entry_demo/src/main/ets/pages/aboutus/AboutUs.ets new file mode 100644 index 0000000..964ff92 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/aboutus/AboutUs.ets @@ -0,0 +1,276 @@ +import { promptAction, PromptAction, router } from '@kit.ArkUI'; +import { AppInfoConfig, GlobalContext, HXLog } from 'biz_common'; +import { WebTitleView } from 'biz_login/src/main/ets/view/WebTitleView'; +import pasteboard from '@ohos.pasteboard'; +import { AppUtil, ToastUtil } from '@pura/harmony-utils'; +import { WTClientInfoUtil } from 'biz_trade/src/main/ets/utils/WTClientInfoUtil'; +import { url } from '@kit.ArkTS'; +import { resourceManager } from '@kit.LocalizationKit'; +import { common, UIAbility } from '@kit.AbilityKit'; + +interface IAboutItem { + title: Resource; + showArrow: boolean; + showDivider: boolean; + routeName?: string; + isJumpWeb:boolean + jumpWebUrl?:Resource; + value?: string; +} + +@Entry +@Component +struct AboutUs { + @State appVersion: string|Resource = "版本 "+AppInfoConfig.getVersionName(); + @State deviceId: string = "C1864EBD-9F88-496D-A424-253F7163521D"; + + private aboutItems: IAboutItem[] = [ + { + title: $r('app.string.about_us_product_intro'), + showArrow: true, + showDivider: true, + isJumpWeb:false, + routeName: "pages/aboutus/ProductIntroduction" + }, + { + title: $r('app.string.about_us_contact'), + showArrow: true, + showDivider: true, + isJumpWeb:false, + routeName: "pages/aboutus/ContactUs" + }, + { + title: $r('app.string.about_us_privacy_policy'), + showArrow: true, + showDivider: true, + isJumpWeb:true, + jumpWebUrl:$r('app.string.revoke_privacy_protocol_url') + }, + { + title: $r('app.string.about_us_personal_info_download'), + showArrow: true, + showDivider: true, + isJumpWeb:true, + jumpWebUrl:$r('app.string.user_info_download_url') + }, + { + title: $r('app.string.user_agreement_text'), + showArrow: true, + showDivider: true, + isJumpWeb:true, + jumpWebUrl:$r('app.string.about_user_agreement_url') + }, + { + title: $r('app.string.about_us_recommendation_settings'), + showArrow: true, + showDivider: true, + isJumpWeb:true, + jumpWebUrl: $r('app.string.about_direct_promotion') + } + ] + + aboutToAppear(): void { + this.deviceId = WTClientInfoUtil.getDeviceId() + } + + private copyDataToPasteBoard(dataText:string){ + try { + let sysPasteBoard = pasteboard.getSystemPasteboard() + let data = pasteboard.createPlainTextData(dataText) + sysPasteBoard.setDataSync(data) + promptAction.showToast({message:$r('app.string.copy_success'),duration:3000,alignment:Alignment.Center}) + }catch (e){ + promptAction.showToast({message:$r('app.string.copy_fail'),duration:3000,alignment:Alignment.Center}) + } + } + + private onItemClick(item:IAboutItem){ + if (item.isJumpWeb && item.jumpWebUrl) { + let jumpUrl = AppUtil.getContext().resourceManager.getStringSync(item.jumpWebUrl) + let webTitle = AppUtil.getContext().resourceManager.getStringSync(item.title) + if (jumpUrl && webTitle) { + router.pushUrl({ + url: 'pages/WebViewComponentPage', params: { + url: jumpUrl, + title: webTitle, + updateH5Title: '0', + useCookie:true + } + }) + } + }else { + if (item.routeName) { + router.pushUrl({ url: item.routeName }); + } + } + } + + private openInSystemBrowser(url:string){ + try { + + let context = getContext(this) as common.UIAbilityContext; + context.startAbility({action:'ohos.want.action.viewData',uri:url}) + }catch (e) { + promptAction.showToast({message:$r('app.string.web_link_fail'),duration:3000,alignment:Alignment.Center}) + } + } + + build() { + Column() { + Row().width('100%').height(px2vp(GlobalContext.statusBarHeight)).backgroundColor($r('app.color.surface_layer1_foreground')).flexShrink(0) + WebTitleView({ title: '' }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + + Column() { + Image($r('app.media.theme_icon_system_config_app')) + .width(100) + .height(120) + .margin({ top: 20 }) + + Text(this.appVersion) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ top: 8, bottom: 40 }) + } + .width('100%') + .alignItems(HorizontalAlign.Center) + + Column() { + ForEach(this.aboutItems, (item: IAboutItem, index: number) => { + Row() { + Text(item.title) + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + .layoutWeight(1) + + if (item.value) { + Text(item.value) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ right: 4 }) + + Image($r('app.media.icon_arrow_right_line')) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(16) + .height(16) + + } + + if (item.showArrow) { + Image($r('app.media.icon_arrow_right_line')) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(16) + .height(16) + } + } + .width('100%') + .height(56) + .padding({ left: 16, right: 16 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .onClick(() => { + this.onItemClick(item) + }) + + if (item.showDivider) { + Divider() + .strokeWidth(0.5) + .color($r('app.color.elements_others_divider_primary')) + .margin({ left: 16, right: 16 }) + } + }) + } + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .margin({ left: 12, right: 12 }) + + Column() { + Row() { + Column(){ + Text($r('app.string.about_us_device_id')) + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + .align(Alignment.Start) + Text(this.deviceId) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .align(Alignment.Start) + .margin({top:6}) + }.layoutWeight(1) + .alignItems(HorizontalAlign.Start) + .justifyContent(FlexAlign.Start) + Image($r('app.media.theme_icon_system_config_copy')) + .width(16) + .height(16) + } + .width('100%') + .height(56) + .padding({ left: 16, right: 16 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .onClick(() => { + this.copyDataToPasteBoard(this.deviceId) + }) + Divider() + .strokeWidth(0.5) + .color($r('app.color.elements_others_divider_primary')) + .margin({ left: 16, right: 16 }) + } + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .margin({ left: 12, right: 12 }) + + Column() { + Text($r('app.string.about_us_disclaimer')) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_01')) + .margin({ top: 40 }) + .onClick(() => { + let item = { + title: $r('app.string.about_us_disclaimer'), + showArrow: true, + showDivider: true, + isJumpWeb:true, + jumpWebUrl: $r('app.string.about_statement_url') + } as IAboutItem + this.onItemClick(item) + }) + Column(){ + Text($r('app.string.about_us_icp')) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ top: 16 }) + + Row() { + Image($r('app.media.icon_system_config_app_2')) + .width(16) + .height(16) + .margin({ right: 4 }) + + Text($r('app.string.about_us_platform_desc')) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + } + .margin({ top: 8 }) + }.width('100%') + .onClick(()=>{ + // let item = { + // title: $r('app.string.about_beianhao_query_text'), + // showArrow: true, + // showDivider: true, + // isJumpWeb:true, + // jumpWebUrl: $r('app.string.about_beianhao_query_url') + // } as IAboutItem + // this.onItemClick(item) + this.openInSystemBrowser(AppUtil.getContext().resourceManager.getStringSync($r('app.string.about_beianhao_query_url'))) + }) + + } + .width('100%') + .alignItems(HorizontalAlign.Center) + + Blank() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/aboutus/ContactUs.ets b/future_entry_demo/src/main/ets/pages/aboutus/ContactUs.ets new file mode 100644 index 0000000..de7f578 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/aboutus/ContactUs.ets @@ -0,0 +1,69 @@ +import { GlobalContext } from 'biz_common'; +import { WebTitleView } from 'biz_login/src/main/ets/view/WebTitleView'; + +@Entry +@Component +struct ContactUs { + build() { + Column() { + Row() + .width('100%') + .height(px2vp(GlobalContext.statusBarHeight)) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .flexShrink(0) + WebTitleView({ title: $r('app.string.contact_us_title') }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + + Column() { + Image($r('app.media.contact_us')) + .width('100%') + .height(105) + + + Column() { + Column() { + Text($r('app.string.contact_us_hotline')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .lineHeight(24) + .margin({ top: 32 }) + + Text($r('app.string.contact_us_service_time')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .lineHeight(24) + .margin({ top: 2 }) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + .padding({ left: 16, right: 16 }) + + Column() { + Text($r('app.string.contact_us_address_title')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .alignSelf(ItemAlign.Start) + .margin({ top: 40, bottom: 2 }) + + Text($r('app.string.contact_us_address')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .lineHeight(24) + .alignSelf(ItemAlign.Start) + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + .width('100%') + .backgroundColor(Color.White) + .layoutWeight(1) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_ffffff')) + + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/aboutus/ProductIntroduction.ets b/future_entry_demo/src/main/ets/pages/aboutus/ProductIntroduction.ets new file mode 100644 index 0000000..b571b19 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/aboutus/ProductIntroduction.ets @@ -0,0 +1,101 @@ +import { router } from '@kit.ArkUI'; +import { GlobalContext } from 'biz_common'; +import { WebTitleView } from 'biz_login/src/main/ets/view/WebTitleView'; + +@Entry +@Component +struct ProductIntroduction { + build() { + Column() { + Row() + .width('100%') + .height(px2vp(GlobalContext.statusBarHeight)) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .flexShrink(0) + WebTitleView({ title: $r('app.string.product_intro_title') }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + + + Column() { + + Image($r('app.media.about_product')) + .width('100%') + .height(105) + + Column() { + Text($r('app.string.product_intro_title')) + .fontSize(32) + .fontColor(Color.White) + .fontWeight(FontWeight.Bold) + .margin({ top: 20, left: 24 }) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + + Column() { + Text($r('app.string.product_intro_desc')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .lineHeight(24) + .margin({ left: 16, right: 16 }) + + Column() { + Text($r('app.string.product_intro_market_title')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .fontWeight(FontWeight.Medium) + .alignSelf(ItemAlign.Start) + .margin({ top: 24, bottom: 8 }) + + Text($r('app.string.product_intro_market_desc')) + .fontSize(16) + .fontColor($r('app.color.about_text_light_gray')) + .lineHeight(24) + } + .width('100%') + .padding({ left: 16, right: 16 }) + + Column() { + Text($r('app.string.product_intro_data_title')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .fontWeight(FontWeight.Medium) + .alignSelf(ItemAlign.Start) + .margin({ top: 24, bottom: 8 }) + + Text($r('app.string.product_intro_data_desc')) + .fontSize(16) + .fontColor($r('app.color.about_text_light_gray')) + .lineHeight(24) + } + .width('100%') + .padding({ left: 16, right: 16 }) + + Column() { + Text($r('app.string.product_intro_service_title')) + .fontSize(16) + .fontColor($r('app.color.about_text_dark')) + .fontWeight(FontWeight.Medium) + .alignSelf(ItemAlign.Start) + .margin({ top: 24, bottom: 8 }) + + Text($r('app.string.product_intro_service_desc')) + .fontSize(16) + .fontColor($r('app.color.about_text_light_gray')) + .lineHeight(24) + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + .width('100%') + .backgroundColor(Color.White) + } + .width('100%') + .height('100%') + + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_ffffff')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/AccountAnalysisDebugPage.ets b/future_entry_demo/src/main/ets/pages/debug/AccountAnalysisDebugPage.ets new file mode 100644 index 0000000..e249e17 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/AccountAnalysisDebugPage.ets @@ -0,0 +1,22 @@ +import { AccountAnalysisDebugTestPage } from '@b2c-f/accountanalysis' +import router from '@ohos.router' + +/** + * 账户分析 Debug 页面 + */ +@Entry({ routeName: "AccountAnalysisDebugPage" }) +@Component +export struct AccountAnalysisDebugPage { + build() { + Column() { + AccountAnalysisDebugTestPage({ + onExit: () => { + router.back() + } + }) + } + .width('100%') + .height('100%') + .backgroundColor(Color.White) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/ChangeThemeColor.ets b/future_entry_demo/src/main/ets/pages/debug/ChangeThemeColor.ets new file mode 100644 index 0000000..ae4c439 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/ChangeThemeColor.ets @@ -0,0 +1,174 @@ +/** + * author : xiejialin@myhexin.com + * time : 2026/3/3 16:43 + * desc : 黑白版切换的组件 + */ + +import { common } from '@kit.AbilityKit'; +import { GlobalContext } from 'biz_common'; +import { ColourModeStatus, setAutoColourMode, setDarkColourMode, setLightColourMode } from '@kernel/theme_manager'; +import { window } from '@kit.ArkUI'; + +const followSystemOptionIndex = 0 +const alwaysWhiteOptionIndex = 1 +const alwaysBlackOptionIndex = 2 + +@Builder +export function ChangeThemeColorBuilder() { + ChangeThemeColor() +} + +@Component +export struct ChangeThemeColor { + @State barHeight: number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + @State selectedTheme: number = followSystemOptionIndex; + + aboutToAppear(): void { + this.setSystemBar(); + if (ColourModeStatus.followSystemThemeSetting) { + this.selectedTheme = followSystemOptionIndex + } else { + this.selectedTheme = ColourModeStatus.enableDarkMode() ? alwaysBlackOptionIndex : alwaysWhiteOptionIndex + } + } + + aboutToDisappear(): void { + this.resetSystemBar(); + this.updateMainWindowStatusBarContentColor(); + } + + // 处理主题切换 + private selectTheme(id: number) { + this.selectedTheme = id; + if (id === followSystemOptionIndex) { + setAutoColourMode(GlobalContext.get()) + } else if (id === alwaysWhiteOptionIndex) { + setLightColourMode(GlobalContext.get()) + } else { + setDarkColourMode(GlobalContext.get()) + } + this.setSystemBar() + this.updateMainWindowStatusBarContentColor(); + } + + // 更新主窗口的状态栏内容颜色 + private async updateMainWindowStatusBarContentColor() { + try { + let windowStageInstance: window.WindowStage = AppStorage.get('EntryAbilityWindowStage') as window.WindowStage + if (!windowStageInstance) { + return + } + let mainWindow: window.Window = windowStageInstance.getMainWindowSync() + // 根据当前主题设置状态栏内容颜色 + // 暗色模式下使用白色,浅色模式下使用黑色 + let isDarkMode = ColourModeStatus.enableDarkMode() + let contentColor = isDarkMode ? "#FFFFFFFF" : "#D6000000" + await mainWindow.setWindowSystemBarProperties({ + navigationBarContentColor: contentColor, + statusBarContentColor: contentColor + }) + } catch (e) { + console.error('updateMainWindowStatusBarContentColor error:', JSON.stringify(e)) + } + } + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + let barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + let storeBarHeight = GlobalContext.statusBarHeight + + if (barHeight !== storeBarHeight && barHeight !== 0) { + this.barHeight = barHeight + } else { + this.barHeight = storeBarHeight + } + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + //设置底部系统栏背景色,颜色属性为ARGB,将蒙尘设置为0%使其透明 + navigationBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + //设置顶部系统栏(状态栏)背景色 + statusBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + //导航栏内容色:返回/主页 + navigationBarContentColor: getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_without_mask_color'), + //状态栏内容色:时间/电量 + statusBarContentColor: getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_without_mask_color') + }) + } + + async resetSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + statusBarColor: getContext().resourceManager.getStringByNameSync('entry_transparent_color'), + navigationBarContentColor: getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_without_mask_color'), + statusBarContentColor: getContext().resourceManager.getStringByNameSync('entry_navigation_bar_content_without_mask_color') + }) + } + + build() { + Column() { + // 跟随系统 + Row() { + Text('跟随系统') + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + Blank() + Toggle({ type: ToggleType.Switch, isOn: this.selectedTheme === followSystemOptionIndex }) + .onChange((on: boolean) => { + if (on) { + this.selectTheme(followSystemOptionIndex) + } + }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + .height(56) + + Divider().strokeWidth(1).color($r('app.color.elements_others_divider_primary')) + + // 始终白色 + Row() { + Text('始终白色') + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(16) + Blank() + Toggle({ type: ToggleType.Switch, isOn: this.selectedTheme === alwaysWhiteOptionIndex }) + .onChange((on: boolean) => { + if (on) { + this.selectTheme(alwaysWhiteOptionIndex) + } + }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + .height(56) + + Divider().strokeWidth(1).color($r('app.color.elements_others_divider_primary')) + + // 始终黑色 + Row() { + Text('始终黑色') + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + Blank() + Toggle({ type: ToggleType.Switch, isOn: this.selectedTheme === alwaysBlackOptionIndex }) + .onChange((on: boolean) => { + if (on) { + this.selectTheme(alwaysBlackOptionIndex) + } + }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + .height(56) + } + .width('100%') + .height(300) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugBSPointMockDataPage.ets b/future_entry_demo/src/main/ets/pages/debug/DebugBSPointMockDataPage.ets new file mode 100644 index 0000000..71d2a24 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugBSPointMockDataPage.ets @@ -0,0 +1,745 @@ +/** + * 分时成交点模拟数据管理页面 + * 对应 iOS HXTradeBSPointMockDataViewController + */ + +import { router } from '@kit.ArkUI'; +import { DebugBSPointMockDataManager, DebugBSPointMockData, BSPointMockDataJson, KaiPingType, MaiMaiType } from 'biz_debug'; +import { GlobalContext } from 'biz_common'; +import { ToastUtil } from '@pura/harmony-utils'; +import { StatusBar } from '../../widget/StatusBar'; + +type MockDataItem = BSPointMockDataJson; + +@Entry +@Component +struct DebugBSPointMockDataPage { + @State mockDataList: MockDataItem[] = []; + @State isLoading: boolean = false; + @State showAddDialog: boolean = false; + @State editingItem: MockDataItem | null = null; + + // 确认弹窗 + @State showConfirmDialog: boolean = false; + @State confirmDialogTitle: string = ''; + @State confirmDialogMessage: string = ''; + @State confirmActionType: string = ''; // 'delete' or 'clear' + @State confirmActionId: string = ''; // 用于删除时存储ID + + // 表单数据 + @State formCode: string = ''; + @State formKaiPing: KaiPingType = '开'; + @State formMaiMai: MaiMaiType = '买'; + @State formCount: string = '1'; + @State formPrice: string = ''; + @State formTime: string = ''; + @State formOrderNo: string = ''; + @State enableMockData: boolean = false; + @State enableSimulateTrade: boolean = false; + + private dataManager: DebugBSPointMockDataManager = DebugBSPointMockDataManager.getInstance(); + + aboutToAppear() { + this.initData(); + } + + async initData() { + this.isLoading = true; + try { + // 获取 application context + await this.dataManager.init(GlobalContext.get()); + await this.loadData(); + } catch (error) { + ToastUtil.showToast('初始化失败'); + } finally { + this.isLoading = false; + } + } + + async loadData() { + const list = this.dataManager.getMockDataList(); + this.mockDataList = list.map(item => item.toJson()); + // 加载开关状态 + this.enableMockData = this.dataManager.isEnableMockData(); + this.enableSimulateTrade = this.dataManager.isEnableSimulateTrade(); + } + + // 生成不重复的随机委托编号 + generateUniqueOrderNo(): string { + const existingOrderNos = new Set(this.mockDataList.map(item => item.orderNo)); + let orderNo: string; + let attempts = 0; + do { + // 生成8位随机数字 + orderNo = Math.floor(10000000 + Math.random() * 90000000).toString(); + attempts++; + // 防止无限循环 + if (attempts > 100) { + break; + } + } while (existingOrderNos.has(orderNo)); + return orderNo; + } + + // 打开新增/编辑弹窗 + openDialog(item?: MockDataItem) { + if (item) { + // 编辑模式 + this.editingItem = item; + this.formCode = item.code; + this.formKaiPing = item.kaiPing; + this.formMaiMai = item.maiMai; + this.formCount = item.count.toString(); + this.formPrice = item.price; + this.formTime = item.time; + this.formOrderNo = item.orderNo; + } else { + // 新增模式 + this.editingItem = null; + this.formCode = 'ec2604'; + this.formKaiPing = '开'; + this.formMaiMai = '买'; + this.formCount = '1'; + this.formOrderNo = this.generateUniqueOrderNo(); + + // 根据最后一个数据设置默认时间和价格 + if (this.mockDataList.length > 0) { + const lastItem = this.mockDataList[this.mockDataList.length - 1]; + this.formCode = lastItem.code + this.formPrice = lastItem.price; + + // 时间延后10分钟 + const timeParts = lastItem.time.split(':'); + let hours = parseInt(timeParts[0]); + let minutes = parseInt(timeParts[1]); + let seconds = parseInt(timeParts[2]); + + minutes += 10; + if (minutes >= 60) { + hours += Math.floor(minutes / 60); + minutes = minutes % 60; + } + if (hours >= 24) { + hours = hours % 24; + } + + this.formTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } else { + this.formPrice = ''; + this.formTime = ''; + } + } + this.showAddDialog = true; + } + + // 关闭弹窗 + closeDialog() { + this.showAddDialog = false; + this.editingItem = null; + } + + // 保存数据 + async saveData() { + // 验证 + if (!this.formCode.trim()) { + ToastUtil.showToast('请输入合约代码'); + return; + } + if (!this.formPrice.trim()) { + ToastUtil.showToast('请输入价格'); + return; + } + if (!this.formTime.trim()) { + ToastUtil.showToast('请输入时间'); + return; + } + + // 时间格式校验 HH:mm:ss + const timeRegex = /^\d{2}:\d{2}:\d{2}$/; + if (!timeRegex.test(this.formTime.trim())) { + ToastUtil.showToast('时间格式应为 HH:mm:ss'); + return; + } + + const count = parseInt(this.formCount); + if (isNaN(count) || count <= 0) { + ToastUtil.showToast('请输入有效的手数'); + return; + } + + const mockData = new DebugBSPointMockData( + this.formCode.trim(), + this.formKaiPing, + this.formMaiMai, + count, + this.formPrice.trim(), + this.formTime.trim(), + this.formOrderNo.trim() + ); + + let success = false; + if (this.editingItem) { + // 更新 + mockData.id = this.editingItem.id; + success = await this.dataManager.updateMockData(mockData); + if (success) { + ToastUtil.showToast('更新成功'); + } + } else { + // 新增 + success = await this.dataManager.addMockData(mockData); + if (success) { + ToastUtil.showToast('添加成功'); + } + } + + if (success) { + await this.loadData(); + this.closeDialog(); + } else { + ToastUtil.showToast('保存失败'); + } + } + + // 删除数据 + async deleteData(id: string) { + this.confirmDialogTitle = '确认删除'; + this.confirmDialogMessage = '确定删除该数据吗?'; + this.confirmActionType = 'delete'; + this.confirmActionId = id; + this.showConfirmDialog = true; + } + + // 清空所有数据 + async clearAll() { + this.confirmDialogTitle = '确认清空'; + this.confirmDialogMessage = '确定清空所有模拟数据吗?此操作不可恢复'; + this.confirmActionType = 'clear'; + this.confirmActionId = ''; + this.showConfirmDialog = true; + } + + // 处理删除确认 + async handleDelete(id: string) { + const success = await this.dataManager.deleteMockData(id); + if (success) { + ToastUtil.showToast('删除成功'); + await this.loadData(); + } else { + ToastUtil.showToast('删除失败'); + } + } + + // 处理清空确认 + async handleClear() { + const success = await this.dataManager.clearAllMockData(); + if (success) { + ToastUtil.showToast('已清空所有数据'); + await this.loadData(); + } else { + ToastUtil.showToast('清空失败'); + } + } + + // 格式化显示文本 + getDisplayText(item: MockDataItem): string { + const kaiPingText = item.kaiPing === '开' ? '开' : '平'; + const maiMaiText = item.maiMai === '买' ? '买' : '卖'; + return `${item.code} ${item.price} ${kaiPingText}${maiMaiText} ${item.count}手 ${item.time}`; + } + + // 获取操作类型描述 + getOperateDesc(item: MockDataItem): string { + if (item.maiMai === '买') { + return item.kaiPing === '开' ? '买入开仓' : '买入平仓'; + } else { + return item.kaiPing === '开' ? '卖出开仓' : '卖出平仓'; + } + } + + build() { + Column() { + StatusBar() + // 顶部导航 + this.buildHeader() + // 开关控制面板 + this.buildSwitchPanel() + // 内容区域 + this.buildContent() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.ffffff_2b2b2b')) + } + + @Builder + buildContent(): void { + Stack({ alignContent: Alignment.TopStart }) { + if (this.isLoading) { + Progress({ value: 0, type: ProgressType.Ring }) + .width(48) + .height(48) + .alignSelf(ItemAlign.Center) + } else if (this.mockDataList.length === 0) { + this.buildEmptyView() + } else { + this.buildDataList() + } + + // 新增/编辑弹窗 + if (this.showAddDialog) { + this.buildDialog() + } + + // 确认弹窗 + if (this.showConfirmDialog) { + this.buildConfirmDialog() + } + } + .width('100%') + .layoutWeight(1) + } + + @Builder + buildConfirmDialog() { + // 半透明遮罩 + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.showConfirmDialog = false + }) + + // 弹窗内容 + Column() { + Text(this.confirmDialogTitle) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 12 }) + + Text(this.confirmDialogMessage) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 20 }) + + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor('#FFFFFF') + .onClick(() => { + this.showConfirmDialog = false + }) + + Button('确定') + .layoutWeight(1) + .height(40) + .backgroundColor('#FF6B6B') + .fontColor('#FFFFFF') + .onClick(() => { + const actionType = this.confirmActionType; + const actionId = this.confirmActionId; + this.showConfirmDialog = false; + // 延迟执行,确保弹窗先关闭 + setTimeout(() => { + if (actionType === 'delete') { + this.handleDelete(actionId); + } else if (actionType === 'clear') { + this.handleClear(); + } + }, 100); + }) + } + } + .width('80%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '10%', y: '40%' }) + } + + @Builder + buildHeader() { + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back() + }) + + Text(`成交点模拟数据 (${this.mockDataList.length})`) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + + if (this.mockDataList.length > 0) { + Text('清空') + .fontSize(14) + .fontColor('#FFFFFF') + .onClick(() => { + this.clearAll() + }) + } + } + .width('100%') + .height(56) + .padding({ left: 12, right: 12 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + } + + @Builder + buildSwitchPanel(): void { + Row() { + // 启用模拟数据开关 + Row({ space: 8 }) { + Text('启用') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + Toggle({ type: ToggleType.Switch, isOn: this.enableMockData }) + .selectedColor('#4A90E2') + .onChange((isOn: boolean) => { + this.enableMockData = isOn; + this.dataManager.setEnableMockData(isOn); + }) + } + + Blank() + + // 模拟交易开关 + Row({ space: 8 }) { + Text('模拟交易') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + Toggle({ type: ToggleType.Switch, isOn: this.enableSimulateTrade }) + .selectedColor('#FF9500') + .onChange((isOn: boolean) => { + this.enableSimulateTrade = isOn; + this.dataManager.setEnableSimulateTrade(isOn); + }) + } + } + .width('100%') + .padding({ left: 16, right: 16, top: 12, bottom: 12 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + buildEmptyView() { + Column() { + Text('暂无模拟数据') + .fontSize(16) + .fontColor($r('app.color.elements_text_secondary')) + Text('点击下方按钮添加模拟成交数据') + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .margin({ top: 8 }) + + Column() { + Text('数据格式说明') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 8 }) + Text('• 合约代码:如 ec2604') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 时间格式:HH:mm:ss(如 09:30:00)') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 价格:交易价格') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 手数:交易数量') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 开平:开仓/平仓') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 买卖:买入/卖出') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + } + .alignItems(HorizontalAlign.Start) + .margin({ top: 24, left: 20, right: 20 }) + .padding(16) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + + // 底部添加按钮(当有数据时) + Button('+ 添加模拟数据') + .width('90%') + .height(44) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .margin({ bottom: 20, top: 24 }) + .onClick(() => { + this.openDialog() + }) + } + .justifyContent(FlexAlign.Center) + .width('100%') + } + + @Builder + buildDataList() { + Column() { + List({ space: 8 }) { + ListItem() { + this.buildAddButton() + } + .padding({ left: 16, right: 16, top: 8 }) + + ForEach(this.mockDataList, (item: MockDataItem) => { + ListItem() { + this.buildDataItem(item) + } + .padding({ left: 16, right: 16 }) + }) + } + .width('100%') + .align(Alignment.Top) + .constraintSize({ maxHeight: '70%' }) + + // 底部添加按钮(当有数据时) + Button('+ 添加模拟数据') + .width('90%') + .height(44) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .margin({ bottom: 20 }) + .onClick(() => { + this.openDialog() + }) + } + .width('100%') + } + + @Builder + buildAddButton() { + // 头部按钮 + } + + @Builder + buildDataItem(item: MockDataItem) { + Row() { + Column() { + Text(this.getDisplayText(item)) + .fontSize(15) + .fontColor($r('app.color.elements_text_primary_02')) + Text(this.getOperateDesc(item)) + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + .margin({ top: 4 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Row({ space: 12 }) { + Text('编辑') + .fontSize(14) + .fontColor('#4A90E2') + .onClick(() => { + this.openDialog(item) + }) + + Text('删除') + .fontSize(14) + .fontColor('#FF6B6B') + .onClick(() => { + this.deleteData(item.id) + }) + } + } + .width('100%') + .padding(12) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + } + + @Builder + buildDialog() { + // 半透明遮罩 + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.closeDialog() + }) + + // 弹窗内容 + Column() { + Text(this.editingItem ? '编辑模拟数据' : '新增模拟数据') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 20 }) + + // 合约代码 + this.buildFormItemTextInput('合约代码') + + // 时间 + this.buildFormItemTextInput('时间 (HH:mm:ss)') + + // 价格 + this.buildFormItemTextInput('价格', InputType.Number) + + // 手数 + this.buildFormItemTextInput('手数', InputType.Number) + + // 开平选择 + this.buildFormItemRadio() + + // 买卖选择 + this.buildFormItemRadioMaiMai() + + // 委托编号(可选) + this.buildFormItemTextInput('委托编号(可选)') + + // 按钮 + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor($r('app.color.elements_text_primary_02')) + .onClick(() => { + this.closeDialog() + }) + + Button('保存') + .layoutWeight(1) + .height(40) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .onClick(() => { + this.saveData() + }) + } + .margin({ top: 20 }) + } + .width('90%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + } + + @Builder + buildFormItemTextInput(label: string, inputType: InputType = InputType.Normal) { + Row() { + Text(label) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(100) + + TextInput({ + text: label === '合约代码' ? this.formCode : label === '时间 (HH:mm:ss)' ? this.formTime : label === '价格' ? this.formPrice : label === '手数' ? this.formCount : this.formOrderNo, + placeholder: label === '时间 (HH:mm:ss)' ? '例如: 09:30:00' : label === '价格' ? '例如: 1700' : label === '手数' ? '' : label === '委托编号(可选)' ? '可选' : '' + }) + .type(inputType) + .onChange((val: string) => { + if (label === '合约代码') { + this.formCode = val + } else if (label === '时间 (HH:mm:ss)') { + this.formTime = val + } else if (label === '价格') { + this.formPrice = val + } else if (label === '手数') { + this.formCount = val + } else if (label === '委托编号(可选)') { + this.formOrderNo = val + } + }) + .width(200) + } + .width('100%') + .margin({ bottom: 12 }) + } + + @Builder + buildFormItemRadio() { + Row() { + Text('开平') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(100) + + Row({ space: 12 }) { + Radio({ value: '开', group: 'kaiping' }) + .checked(this.formKaiPing === '开') + .onChange((checked: boolean) => { + if (checked) { + this.formKaiPing = '开' + } + }) + Text('开') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + + Radio({ value: '平', group: 'kaiping' }) + .checked(this.formKaiPing === '平') + .onChange((checked: boolean) => { + if (checked) { + this.formKaiPing = '平' + } + }) + Text('平') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + } + } + .width('100%') + .margin({ bottom: 12 }) + } + + @Builder + buildFormItemRadioMaiMai() { + Row() { + Text('买卖') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(100) + + Row({ space: 12 }) { + Radio({ value: '买', group: 'maimai' }) + .checked(this.formMaiMai === '买') + .onChange((checked: boolean) => { + if (checked) { + this.formMaiMai = '买' + } + }) + Text('买') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + + Radio({ value: '卖', group: 'maimai' }) + .checked(this.formMaiMai === '卖') + .onChange((checked: boolean) => { + if (checked) { + this.formMaiMai = '卖' + } + }) + Text('卖') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + } + } + .width('100%') + .margin({ bottom: 12 }) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugCustomDialog.ets b/future_entry_demo/src/main/ets/pages/debug/DebugCustomDialog.ets new file mode 100644 index 0000000..0bbbcad --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugCustomDialog.ets @@ -0,0 +1,154 @@ +/** + * 通用自定义弹窗参数 + */ +export interface DebugCustomDialogParams { + title?: string; + contentBuilder: () => void; + confirmText?: string; + cancelText?: string; + onConfirm?: () => boolean; + onCancel?: () => void; +} + +/** + * 通用自定义弹窗封装类 + * 使用示例: + * const dialog = new DialogBuilder({ + * title: '标题', + * contentBuilder: () => { 你的 @Builder 内容 }, + * confirmText: '确定', + * onConfirm: () => { console.log('确认') } + * }) + * dialog.show() + */ +export class DebugDialogBuilder { + private dialogController?: CustomDialogController + private params: DebugCustomDialogParams + + constructor(params: DebugCustomDialogParams) { + console.log('[DebugDialogBuilder] constructor called, title:', params.title) + this.params = params + } + + /** + * 显示弹窗 + */ + show() { + console.log('[DebugDialogBuilder] show() start') + if (this.dialogController) { + console.log('[DebugDialogBuilder] closing existing dialog') + this.dialogController.close() + } + + console.log('[DebugDialogBuilder] creating CustomDialogController') + try { + this.dialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: this.params.title || '提示', + contentBuilder: this.params.contentBuilder, + confirmText: this.params.confirmText || '确认', + cancelText: this.params.cancelText, + onConfirm: this.params.onConfirm, + onCancel: this.params.onCancel + }), + autoCancel: true, + alignment: DialogAlignment.Center, + customStyle: true + }) + console.log('[DebugDialogBuilder] CustomDialogController created successfully') + + console.log('[DebugDialogBuilder] calling dialogController.open()') + this.dialogController.open() + console.log('[DebugDialogBuilder] dialogController.open() called') + } catch (e) { + console.error('[DebugDialogBuilder] Error:', e) + } + } + + /** + * 关闭弹窗 + */ + close() { + this.dialogController?.close() + } +} + +/** + * 通用自定义弹窗组件 + */ +@CustomDialog +export struct DebugCustomViewDialog { + controller: CustomDialogController + title: string = '提示' + @BuilderParam contentBuilder: () => void + confirmText: string = '确认' + cancelText?: string = '' + onConfirm?: () => boolean + onCancel?: () => void + + aboutToAppear() { + console.log('[DebugCustomViewDialog] aboutToAppear, title:', this.title) + } + + build() { + Column() { + // 标题 + Text(this.title) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#333333') + .width('100%') + .textAlign(TextAlign.Center) + .padding({ top: 20, bottom: 16 }) + + // 自定义内容区域 + Column() { + this.contentBuilder() + } + .width('100%') + + // 按钮区域 + Row() { + if (this.cancelText) { + Button(this.cancelText) + .fontSize(16) + .fontColor('#666666') + .backgroundColor(Color.Transparent) + .layoutWeight(1) + .onClick(() => { + if (this.onCancel) { + this.onCancel() + } + this.controller.close() + }) + + Divider() + .vertical(true) + .height(40) + .color('#E5E5E5') + } + + Button(this.confirmText) + .fontSize(16) + .fontColor('#2E58FF') + .backgroundColor(Color.Transparent) + .layoutWeight(1) + .onClick(() => { + let isSuccess = true + if (this.onConfirm) { + isSuccess = this.onConfirm() + } + if (isSuccess) { + this.controller.close() + } + }) + } + .width('100%') + .height(52) + .margin({ top: 8 }) + } + .width('80%') + .backgroundColor(Color.White) + .borderRadius(16) + } +} diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBall.ets b/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBall.ets new file mode 100644 index 0000000..2a5d6a8 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBall.ets @@ -0,0 +1,90 @@ +import display from '@ohos.display' +import { window } from '@kit.ArkUI' +import { GlobalContext } from 'biz_common' + +/** + * Debug悬浮球工具类 + * 提供创建和销毁SubWindow悬浮球的静态方法 + */ +export class DebugFloatingBall { + static subWindowName = 'DebugFloatingBall' + + /** + * 是否可以显示 + */ + static canShow: boolean = true + + /** + * 创建Debug悬浮球子窗口 + * @param windowStage 窗口舞台 + */ + static createDebugFloatingBall(windowStage: window.WindowStage) { + // 检查是否已存在,避免重复创建 + try { + const existingWindow = window.findWindow(DebugFloatingBall.subWindowName) + if (existingWindow) { + console.info('[DebugFloatingBall] SubWindow already exists, skipping creation') + return + } + } catch (e) { + // 窗口不存在,继续创建 + } + + windowStage.createSubWindow(DebugFloatingBall.subWindowName, (error, subWindow) => { + if (error.code > 0) { + console.error(`[DebugFloatingBall] Failed to create subWindow: ${error.message}`) + return + } + + try { + // 设置子窗口UI内容 + subWindow.setUIContent('pages/debug/DebugFloatingBallWindow', () => { + // 设置透明背景 + subWindow.setWindowBackgroundColor('#00000000') + }) + + // 计算初始位置(屏幕顶部中间,状态栏下方) + const screenWidth = display.getDefaultDisplaySync().width + const ballSize = vp2px(60) + const initialX = (screenWidth - ballSize) / 2 + const initialY = GlobalContext.statusBarHeight // 使用状态栏高度(px单位) + + // 移动到初始位置 + subWindow.moveWindowTo(initialX, initialY) + + // 设置窗口大小为悬浮球大小(60x60 vp) + subWindow.resize(ballSize, ballSize) + + // 设置窗口可触摸 + subWindow.setWindowTouchable(true) + + subWindow.setWindowFocusable(false) + + // 设置全屏布局(不受状态栏影响) + subWindow.setWindowLayoutFullScreen(true) + + // 显示窗口 + subWindow.showWindow() + + console.info('[DebugFloatingBall] SubWindow created successfully') + } catch (e) { + console.error(`[DebugFloatingBall] Failed to setup subWindow: ${e}`) + } + }) + } + + /** + * 销毁Debug悬浮球窗口 + */ + static destroyDebugFloatingBall() { + try { + const subWindow = window.findWindow(DebugFloatingBall.subWindowName) + if (subWindow && subWindow.isWindowShowing()) { + subWindow.destroyWindow() + console.info('[DebugFloatingBall] SubWindow destroyed') + } + } catch (e) { + console.error(`[DebugFloatingBall] Failed to destroy subWindow: ${e}`) + } + } +} diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBallWindow.ets b/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBallWindow.ets new file mode 100644 index 0000000..78d5299 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugFloatingBallWindow.ets @@ -0,0 +1,268 @@ +import { DebugPanel } from './DebugPanel' +import { display, window } from '@kit.ArkUI' +import { GlobalContext } from 'biz_common' +import { DebugFloatingBall } from './DebugFloatingBall'; + +interface DebugFloatingBallWindowPosition { + x: number; + y: number; +} + +/** + * Debug悬浮球子窗口页面 + * 实现拖拽、点击展开/收起功能 + */ +@Entry +@Component +struct DebugFloatingBallWindow { + @State windowStage: window.WindowStage = AppStorage.get('EntryAbilityWindowStage') as window.WindowStage + @State subWindow: window.Window = window.findWindow('DebugFloatingBall') + // 窗口位置(使用px单位,因为moveWindowTo使用px) + @State windowPosition: DebugFloatingBallWindowPosition = { x: 0, y: 0 } + // 保存展开前的位置(用于收起时恢复) + private savedPosition: DebugFloatingBallWindowPosition = { x: 0, y: 0 } + // 展开状态 + @Provide @Watch('isExpandedChanged') isExpanded: boolean = false + // 常量 + private readonly BALL_SIZE = 60 // vp + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }) + + aboutToAppear() { + try { + // 获取当前窗口位置并保存 + const props = this.subWindow.getWindowProperties() + this.windowPosition.x = props.windowRect.left + this.windowPosition.y = props.windowRect.top + console.info(`[DebugFloatingBallWindow] Initial position: (${this.windowPosition.x}, ${this.windowPosition.y})`) + } catch (error) { + console.error(`[DebugFloatingBallWindow] Error in aboutToAppear: ${error}`) + } + } + + onPageShow(): void { + // 关键:将焦点转移到主窗口,让主窗口可以接收触摸事件 + try { + const subWindowId = this.subWindow.getWindowProperties().id + const mainWindowId = this.windowStage.getMainWindowSync().getWindowProperties().id + window.shiftAppWindowFocus(subWindowId, mainWindowId) + console.info('[DebugFloatingBallWindow] Focus shifted to main window') + } catch (error) { + console.error(`[DebugFloatingBallWindow] Failed to shift focus: ${error}`) + } + } + + onBackPress(): boolean | void { + if (this.isExpanded) { + this.isExpanded = false + } + return true + } + + isExpandedChanged() { + if (!this.isExpanded) { + this.windowPosition.x = this.savedPosition.x + this.windowPosition.y = this.savedPosition.y + this.subWindow.resize(vp2px(this.BALL_SIZE), vp2px(this.BALL_SIZE)) + this.subWindow.moveWindowTo(this.savedPosition.x, this.savedPosition.y) + } + } + + /** + * 处理点击事件 - 展开/收起 + */ + handleClick() { + try { + if (!this.isExpanded) { + // 展开:将SubWindow变为全屏(带动画) + console.info('[DebugFloatingBallWindow] Expanding to fullscreen...') + + // 保存展开前的位置 + this.savedPosition.x = this.windowPosition.x + this.savedPosition.y = this.windowPosition.y + + const screenWidth = display.getDefaultDisplaySync().width + const screenHeight = display.getDefaultDisplaySync().height + + this.isExpanded = true + this.windowPosition.x = 0 + this.windowPosition.y = 0 + this.subWindow.moveWindowTo(0, 0) + this.subWindow.resize(screenWidth, screenHeight) + + } else { + // 收起:恢复为小球 + console.info('[DebugFloatingBallWindow] Collapsing to ball...') + + this.isExpanded = false + } + } catch (error) { + console.error(`[DebugFloatingBallWindow] Failed to resize: ${error}`) + } + } + + /** + * 处理长按事件 - 隐藏悬浮球 + */ + handleLongPress() { + try { + console.info('[DebugFloatingBallWindow] Long press - destroying window') + this.subWindow.destroyWindow() + DebugFloatingBall.canShow = false + } catch (error) { + console.error(`[DebugFloatingBallWindow] Failed to destroy: ${error}`) + } + } + + build() { + if (!this.isExpanded) { + // 收起状态:显示小球(支持拖拽) + Stack() { + this.buildFloatingBall() + } + .backgroundColor(Color.Transparent) + .gesture( + // 拖拽手势(仅在收起状态响应) + PanGesture(this.panOption) + .onActionStart(() => { + console.info('[DebugFloatingBallWindow] Pan start') + }) + .onActionUpdate((event: GestureEvent) => { + // 关键:累加偏移量到窗口位置(参考ResizeSubWindow实现) + this.windowPosition.x += event.offsetX + this.windowPosition.y += event.offsetY + + // 添加边界限制,确保悬浮球不会被屏幕边缘遮挡 + const screenWidth = display.getDefaultDisplaySync().width + const screenHeight = display.getDefaultDisplaySync().height + const windowRect = this.subWindow.getWindowProperties().windowRect + + // 限制在屏幕范围内 + this.windowPosition.x = Math.max(0, Math.min(screenWidth - windowRect.width, this.windowPosition.x)) + this.windowPosition.y = Math.max(0, Math.min(screenHeight - windowRect.height, this.windowPosition.y)) + + // 移动窗口 + this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y) + }) + .onActionEnd(() => { + console.info('[DebugFloatingBallWindow] Pan end') + }) + ) + } else { + // 展开状态:全屏显示(半透明遮罩 + 居中面板,避开状态栏) + Stack() { + // 半透明遮罩背景 + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.3)') + .onClick(() => { + // 点击遮罩区域收起面板 + this.handleClick() + }) + + // 居中显示的面板(顶部留出状态栏空间) + this.buildExpandedPanel() + } + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .padding({ top: px2vp(GlobalContext.statusBarHeight) }) // 留出状态栏高度 + } + } + + /** + * 构建悬浮球UI(收起状态) + */ + @Builder + buildFloatingBall() { + Column() { + Text('后门') + .fontSize(14) + .fontColor(Color.White) + .fontWeight(FontWeight.Bold) + } + .width(this.BALL_SIZE) + .height(this.BALL_SIZE) + .justifyContent(FlexAlign.Center) + .backgroundColor('#FF4444') + .borderRadius(this.BALL_SIZE / 2) + .shadow({ + radius: 10, + color: 'rgba(0, 0, 0, 0.3)', + offsetY: 2 + }) + .onClick(() => { + this.handleClick() + }) + .gesture( + LongPressGesture({ duration: 500 }) + .onAction(() => { + this.handleLongPress() + }) + ) + } + + /** + * 构建展开面板UI + */ + @Builder + buildExpandedPanel() { + Column() { + // 标题栏 + Row() { + Text('Debug工具') + .fontSize(16) + .fontColor(Color.White) + .fontWeight(FontWeight.Bold) + .layoutWeight(1) + + // 关闭按钮 + Text('×') + .fontSize(28) + .fontColor(Color.White) + .fontWeight(FontWeight.Bold) + .width(40) + .height(40) + .textAlign(TextAlign.Center) + .onClick(() => { + this.handleClick() // 收起 + }) + } + .width('100%') + .height(50) + .padding({ left: 16, right: 8 }) + .backgroundColor('#FF4444') + .justifyContent(FlexAlign.SpaceBetween) + + // 内容区域 + Scroll() { + Column() { + DebugPanel() + } + .width('100%') + .alignItems(HorizontalAlign.Start) // 内容左对齐 + .justifyContent(FlexAlign.Start) // 内容从顶部开始 + .padding({ + left: 16, + right: 16, + top: 12, + bottom: 12 + }) + } + .layoutWeight(1) + .width('100%') + .backgroundColor(Color.White) + .scrollBar(BarState.Auto) + .align(Alignment.Top) // Scroll容器顶部对齐 + } + .width('100%') + .height('100%') + .borderRadius(12) + .shadow({ + radius: 20, + color: 'rgba(0, 0, 0, 0.3)', + offsetY: 4 + }) + .clip(true) + } +} diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugModifyUrlPage.ets b/future_entry_demo/src/main/ets/pages/debug/DebugModifyUrlPage.ets new file mode 100644 index 0000000..2845641 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugModifyUrlPage.ets @@ -0,0 +1,14 @@ +import { UrlConfigComponent } from '@b2c-f/url-modify-config' +import { router } from '@kit.ArkUI' + +@Entry +@Component +struct DebugModifyUrlPage { + build() { + Column(){ + UrlConfigComponent({onCloseClick : () => { + router.back() + }}) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugPanel.ets b/future_entry_demo/src/main/ets/pages/debug/DebugPanel.ets new file mode 100644 index 0000000..d701aa9 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugPanel.ets @@ -0,0 +1,875 @@ +import { DialogHolderContext, PromptActionClass } from "@b2c-m/ohos_hx_dialog" +import { ComponentContent, prompt, router } from "@kit.ArkUI" +import { DebugToolUtil } from "biz_debug" +import { buildDebugPopup } from 'biz_debug/src/main/ets/debug//PopupTool'; +import { WebViewUrlUtil } from "biz_webview/src/main/ets/Util/WebViewUrlUtil" +import { DebugCustomViewDialog } from "./DebugCustomDialog" +import { ChangeThemeColor } from "./ChangeThemeColor" +import { preferences } from '@kit.ArkData' +import { AppUtil } from "@pura/harmony-utils" +import { ColourModeStatus, setDarkColourMode, setLightColourMode } from '@kernel/theme_manager' +import { GlobalContext } from 'biz_common' +import { DebugService } from "@b2c-spi/hx_cbas_api"; +import { common } from '@kit.AbilityKit'; +import { KaihuFlagManager, NewUserManager, UserSourceManager } from 'biz_hxservice' + +export { AccountAnalysisDebugPage } from "./AccountAnalysisDebugPage" + +/** + * Debug后门入口数据结构 + */ +interface DebugEntry { + id: string + name: string + description: string + action: () => void +} + +// 主题颜色切换的内联UI +@Builder +function buildChangeThemeColorUI() { + Column() { + ChangeThemeColor() + } + .width('80%') + .padding(24) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) +} + +/** + * Debug调试面板内容组件 + * 表格布局 + 搜索功能 + */ +@Component +export struct DebugPanel { + /** + * 搜索关键字 + */ + @State searchKeyword: string = '' + + /** + * 过滤后的入口列表(使用@State让UI能响应变化) + */ + @State private filteredEntriesList: DebugEntry[] = [] + + @Consume isExpanded: boolean + + /** + * 所有后门入口列表 + */ + private allEntries: DebugEntry[] = [ + { + id: 'app_info', + name: '查看应用基本信息', + description: '查看应用版本、设备信息等', + action: () => { + this.appInfoDialogController.open() + } + }, + { + id: 'gray_config', + name: '控制灰度配置', + description: '查看&配置灰度测试开关', + action: () => { + this.navigateToDebugControl('pages/debug/GrayTestDebugPage') + } + }, + { + id: 'trade_business', + name: '交易业务', + description: '交易业务相关后门工具', + action: () => { + this.navigateToDebugControl('pages/debug/TradeDebugPage') + } + }, + { + id: 'modify_url', + name: '修改Url', + description: '切换正式环境地址or测试环境地址', + action: () => { + this.navigateToDebugControl('pages/debug/DebugModifyUrlPage') + } + }, + { + id: 'config_server', + name: '站点&push&码表配置', + description: '切换服务器', + action: () => { + this.showServerConfig() + } + }, + { + id: 'switch_config', + name: '开关配置', + description: '通过后门控制开关', + action: () => { + this.navigateToDebugControl('pages/debug/DebugSwitchConfigPage') + } + }, + { + id: 'jump_url', + name: '跳转Url', + description: '跳转自定义Url进行测试', + action: () => { + this.jumpUrl = '' + this.jumpUrlDialogController.open() + } + }, + { + id: 'modify_preference', + name: '修改preference', + description: '修改preference对应的key值', + action: () => { + this.preferenceFileName = '' + this.preferenceKey = '' + this.preferenceValue = '' + this.modifyPreferenceDialogController.open() + } + }, + { + id: 'indicator_parser_test', + name: '指标解析器测试', + description: '测试 indicator_parser HAR 是否可用', + action: () => { + this.navigateToDebugControl('pages/IndicatorParserTestPage') + } + }, + { + id: 'system_indicators_test', + name: '系统指标管理器测试', + description: '测试 SystemIndicatorsManage 数据加载和查询', + action: () => { + this.navigateToDebugControl('pages/SystemIndicatorsTestPage') + } + }, + { + id: 'account_analysis_debug', + name: '账户分析Debug', + description: '进入账户分析调试页面', + action: () => { + router.pushNamedRoute({ + name: 'AccountAnalysisDebugPage' + }) + } + }, + { + id: 'change_theme_color', + name: '切换主题颜色', + description: '切换深色/浅色模式', + action: () => { + this.showChangeThemeColor() + } + }, + { + id: 'bspoint_mock_data', + name: '分时成交点模拟数据', + description: '管理分时成交点模拟数据', + action: () => { + this.navigateToDebugControl('pages/debug/DebugBSPointMockDataPage') + } + }, + { + id: 'kline_bspoint_mock_data', + name: 'K线成交点模拟数据', + description: '管理K线成交点模拟数据', + action: () => { + this.navigateToDebugControl('pages/debug/KLineBSPointMockDataPage') + } + }, + { + id: 'kline_bspoint_mock_detail', + name: 'K线成交点详情数据模拟', + description: '管理K线成交点详情模拟数据', + action: () => { + this.navigateToDebugControl('pages/debug/IBSPointDetailDebugPage') + } + }, + { + id: 'leakWatch', + name: '内存泄漏检测', + description: '内存泄漏检测', + action: () => { + this.navigateToDebugControl('pages/debug/LeakMonitorPage') + } + }, + { + id: 'new_user_debug', + name: '新用户调试', + description: '查看/修改新用户判断状态', + action: () => { + this.refreshNewUserDebugInfo() + this.newUserDebugDialogController.open() + } + }, + { + id: 'kaihu_flag_debug', + name: '来源归属值工具', + description: '查看/修改归属qsid和用户来源comeFrom', + action: () => { + this.refreshKaihuDebugInfo() + this.kaihuDebugDialogController.open() + } + }, + { + id: 'cbas_debug', + name: '埋点调试', + description: 'CBAS埋点调试工具', + action: () => { + this.showCBASDebugWindow() + } + } + ] + + /** + * 应用信息弹窗控制器 + */ + private appInfoDialogController: CustomDialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: '应用基本信息', + contentBuilder: () => { + this.buildAppInfoContent(DebugToolUtil.showAppInfo()) + }, + confirmText: '确定' + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + /** + * 跳转Url弹窗控制器 + */ + private jumpUrlDialogController: CustomDialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: '跳转Url', + contentBuilder: () => { + this.buildJumpUrlContent() + }, + confirmText: '跳转', + onConfirm: () => { + return this.clickJumpUrl(this.jumpUrl) + }, + cancelText: '关闭' + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + /** + * 修改preference弹窗控制器 + */ + private modifyPreferenceDialogController: CustomDialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: '修改preference', + contentBuilder: () => { + this.buildModifyPreferenceContent() + }, + confirmText: '确认修改', + onConfirm: () => { + return this.clickConfirmModifyPreference() + }, + cancelText: '关闭' + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + private newUserDebugDialogController: CustomDialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: '新用户调试', + contentBuilder: () => { + this.buildNewUserDebugContent() + }, + confirmText: '关闭' + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + private kaihuDebugDialogController: CustomDialogController = new CustomDialogController({ + builder: DebugCustomViewDialog({ + title: '来源归属值工具', + contentBuilder: () => { + this.buildKaihuDebugContent() + }, + confirmText: '关闭' + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + /** + * 跳转用url + */ + private jumpUrl: string = '' + + /** + * preference文件名 + */ + private preferenceFileName: string = '' + /** + * preference文件名 + */ + private preferenceKey: string = '' + /** + * preference文件名 + */ + private preferenceValue: string = '' + + @State private newUserDebugInfo: string = '' + @State private forceNewUser: boolean = false + @State private kaihuDebugInfo: string = '' + @State private kaihuInputQsid: string = '' + @State private userSourceDebugInfo: string = '' + @State private userSourceInput: string = '' + + /** + * 组件初始化 + */ + aboutToAppear() { + // 初始化时显示所有入口 + this.updateFilteredEntries() + } + + /** + * 更新过滤后的入口列表 + */ + private updateFilteredEntries() { + if (!this.searchKeyword || this.searchKeyword.trim() === '') { + this.filteredEntriesList = this.allEntries + return + } + + const keyword = this.searchKeyword.toLowerCase() + this.filteredEntriesList = this.allEntries.filter(entry => + entry.name.toLowerCase().includes(keyword) || + entry.description.toLowerCase().includes(keyword) + ) + } + + // -------------弹窗内容构建---------------- + + /** + * 构建应用信息内容 + */ + @Builder + buildAppInfoContent(content: string) { + Column() { + Text(content) + .fontSize(14) + .fontColor($r('app.color.text_dark_color')) + .textAlign(TextAlign.Start) + .width('100%') + } + .padding({left: 20, right: 20, top: 4, bottom: 0}) + .width('100%') + } + + /** + * 构建跳转Url内容 + */ + @Builder + buildJumpUrlContent() { + Column() { + TextArea({ + placeholder: '请输入跳转url(暂不支持客户端页面)', + }) + .width('100%') + .fontSize(14) + .height(100) + .maxLines(0) + .onChange((value: string) => { + this.jumpUrl = value + }) + } + .padding({left: 20, right: 20, top: 4, bottom: 0}) + .width('100%') + } + + /** + * 构建修改Preference内容 + */ + @Builder + buildModifyPreferenceContent() { + Column() { + TextArea({ + placeholder: '请输入Preference文件名', + }) + .width('100%') + .fontSize(12) + .height(35) + .maxLines(1) + .margin({ bottom:10 }) + .onChange((value: string) => { + this.preferenceFileName = value + }) + TextArea({ + placeholder: '请输入Key', + }) + .width('100%') + .fontSize(12) + .height(35) + .maxLines(1) + .margin({ bottom:10 }) + .onChange((value: string) => { + this.preferenceKey = value + }) + TextArea({ + placeholder: '请输入Value,当前仅支持string类型', + }) + .width('100%') + .fontSize(12) + .height(35) + .maxLines(1) + .onChange((value: string) => { + this.preferenceValue = value + }) + } + .padding({left: 20, right: 20, top: 4, bottom: 0}) + .width('100%') + } + + @Builder + buildNewUserDebugContent() { + Column() { + Text(this.newUserDebugInfo) + .fontSize(12) + .fontColor('#333333') + .textAlign(TextAlign.Start) + .width('100%') + .margin({ bottom: 12 }) + + Row() { + Button(this.forceNewUser ? '关闭强制新用户' : '开启强制新用户') + .fontSize(12) + .height(32) + .layoutWeight(1) + .onClick(() => { + NewUserManager.getInstance().setForceNewUser(!this.forceNewUser) + this.refreshNewUserDebugInfo() + }) + Button('手动校验') + .fontSize(12) + .height(32) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => { + NewUserManager.getInstance().onAuthSucceed(() => { + this.refreshNewUserDebugInfo() + prompt.showToast({ message: '新用户校验完成' }) + }) + }) + } + .width('100%') + .margin({ bottom: 8 }) + + Row() { + Button('清空缓存') + .fontSize(12) + .height(32) + .layoutWeight(1) + .onClick(() => { + NewUserManager.getInstance().clearUserMapping() + this.refreshNewUserDebugInfo() + }) + Button('重置请求状态') + .fontSize(12) + .height(32) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => { + NewUserManager.getInstance().resetHasPosted() + this.refreshNewUserDebugInfo() + }) + } + .width('100%') + .margin({ bottom: 8 }) + + Button('刷新') + .fontSize(12) + .height(32) + .width('100%') + .onClick(() => { + this.refreshNewUserDebugInfo() + }) + } + .padding({left: 20, right: 20, top: 4, bottom: 0}) + .width('100%') + } + + @Builder + buildKaihuDebugContent() { + Column() { + Text(this.kaihuDebugInfo) + .fontSize(12) + .fontColor('#333333') + .textAlign(TextAlign.Start) + .width('100%') + .margin({ bottom: 12 }) + + TextInput({ + placeholder: '输入归属qsid', + text: this.kaihuInputQsid + }) + .width('100%') + .height(36) + .fontSize(12) + .margin({ bottom: 8 }) + .onChange((value: string) => { + this.kaihuInputQsid = value + }) + + Row() { + Button('手动请求归属') + .fontSize(12) + .height(32) + .layoutWeight(1) + .onClick(() => { + KaihuFlagManager.getInstance().requestKaihuFlag().then(() => { + this.refreshKaihuDebugInfo() + prompt.showToast({ message: '开户来源请求完成' }) + }) + }) + Button('保存归属qsid') + .fontSize(12) + .height(32) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => { + KaihuFlagManager.getInstance().saveKaihuFlag(this.kaihuInputQsid.trim()) + this.refreshKaihuDebugInfo() + }) + } + .width('100%') + .margin({ bottom: 8 }) + + Row() { + Button('清空归属qsid') + .fontSize(12) + .height(32) + .layoutWeight(1) + .onClick(() => { + this.kaihuInputQsid = '' + KaihuFlagManager.getInstance().saveKaihuFlag('') + this.refreshKaihuDebugInfo() + }) + Button('重置flagChanged') + .fontSize(12) + .height(32) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => { + KaihuFlagManager.getInstance().resetFlagChanged() + this.refreshKaihuDebugInfo() + }) + } + .width('100%') + .margin({ bottom: 8 }) + + Text(this.userSourceDebugInfo) + .fontSize(12) + .fontColor('#333333') + .textAlign(TextAlign.Start) + .width('100%') + .margin({ bottom: 8 }) + + TextInput({ + placeholder: '输入来源comeFrom,留空保存为noSource', + text: this.userSourceInput + }) + .width('100%') + .height(36) + .fontSize(12) + .margin({ bottom: 8 }) + .onChange((value: string) => { + this.userSourceInput = value + }) + + Row() { + Button('手动请求来源') + .fontSize(12) + .height(32) + .layoutWeight(1) + .onClick(() => { + UserSourceManager.getInstance().loadUserSource(true) + this.refreshKaihuDebugInfo() + }) + Button('保存来源') + .fontSize(12) + .height(32) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => { + UserSourceManager.getInstance().saveUserSourceForDebug(this.userSourceInput.trim()) + this.refreshKaihuDebugInfo() + }) + } + .width('100%') + .margin({ bottom: 8 }) + + Button('刷新') + .fontSize(12) + .height(32) + .width('100%') + .onClick(() => { + this.refreshKaihuDebugInfo() + }) + } + .padding({left: 20, right: 20, top: 4, bottom: 0}) + .width('100%') + } + + /** + * 页面构建 + */ + build() { + Column() { + // 搜索框 + Row() { + Search({ placeholder: '搜索后门入口...' }) + .width('100%') + .height(40) + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 12, right: 12 }) + .placeholderColor('#999999') + .placeholderFont({ size: 14 }) + .onChange((value: string) => { + this.searchKeyword = value + // 搜索关键字变化时更新过滤结果 + this.updateFilteredEntries() + }) + } + .width('100%') + .padding({ bottom: 12 }) + + // 表格列表 + if (this.filteredEntriesList.length > 0) { + List({ space: 8 }) { + ForEach(this.filteredEntriesList, (entry: DebugEntry) => { + ListItem() { + this.buildEntryRow(entry) + } + }, (entry: DebugEntry) => entry.id) + } + .width('100%') + .layoutWeight(1) + .divider({ + strokeWidth: 1, + color: '#F0F0F0', + startMargin: 0, + endMargin: 0 + }) + } + + // 无结果提示 + if (this.filteredEntriesList.length === 0) { + Column() { + Text('未找到匹配的入口') + .fontSize(14) + .fontColor('#999999') + Text('试试其他关键字') + .fontSize(12) + .fontColor('#CCCCCC') + .margin({ top: 4 }) + } + .width('100%') + .height(100) + .justifyContent(FlexAlign.Center) + } + } + .width('100%') + .height('100%') + .alignItems(HorizontalAlign.Start) + } + + /** + * 构建单个入口行 + */ + @Builder + buildEntryRow(entry: DebugEntry) { + Row() { + Column() { + // 入口名称 + Text(entry.name) + .fontSize(15) + .fontColor('#333333') + .fontWeight(FontWeight.Medium) + .width('100%') + .textAlign(TextAlign.Start) + + // 入口描述 + Text(entry.description) + .fontSize(12) + .fontColor('#999999') + .width('100%') + .textAlign(TextAlign.Start) + .margin({ top: 4 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + // 右侧箭头 + Text('›') + .fontSize(24) + .fontColor('#CCCCCC') + .width(24) + } + .width('100%') + .padding({ left: 16, right: 16, top: 12, bottom: 12 }) + .backgroundColor(Color.White) + .borderRadius(8) + .onClick(() => { + entry.action() + }) + .stateStyles({ + pressed: { + .backgroundColor('#F5F5F5') + } + }) + } + + // --------------跳转到Debug控制页面---------------- + + /** + * 跳转到灰度后门页面 + */ + private navigateToDebugControl(url: string) { + router.pushUrl({ + url: url + }) + .then(() => { + console.info(`[DebugPanel] Navigate to ${url} success`) + }) + .catch((err: Error) => { + console.error(`[DebugPanel] Navigate to ${url} failed: ${err.message}`) + }) + } + + /** + * 跳转url + * @param url + */ + private clickJumpUrl(url: string): boolean { + // 空判断 + if (url.trim().length == 0) { + prompt.showToast({ message: '输入url为空,请输入正确的地址'}) + return false + } + + // client协议 + if (url.indexOf('client.html') !== -1) { + const info = WebViewUrlUtil.getUrlParams(url, 'client.html?', '^') + const infoUrl: string = info['url'] + if (infoUrl && infoUrl.startsWith('http')) { + this.isExpanded = false + + AppUtil.getWindowStage().getMainWindowSync().getUIContext().getRouter().pushUrl({ + url: 'pages/WebViewComponentPage', params: { + url: infoUrl, + useCookie: true, + isLoginRefreshWeb: true, + refreshOnWidthChange: true + } + }) + return true + } + } else { + this.isExpanded = false + AppUtil.getWindowStage().getMainWindowSync().getUIContext().getRouter().pushUrl({ + url: 'pages/WebViewComponentPage', params: { + url: url, + useCookie: true, + isLoginRefreshWeb: true, + refreshOnWidthChange: true + } + }) + return true + } + prompt.showToast({ message: '输入url不合要求'}) + return false + } + + /** + * 跳转url + * @param url + */ + private clickConfirmModifyPreference(): boolean { + if (this.preferenceFileName && this.preferenceKey && this.preferenceValue) { + const preferenceStore = preferences.getPreferencesSync( + AppUtil.getContext().getApplicationContext(), + { + name: this.preferenceFileName + } + ) + preferenceStore.putSync(this.preferenceKey, this.preferenceValue) + preferenceStore.flushSync() + return true + } + return false + } + + private refreshNewUserDebugInfo(): void { + this.forceNewUser = NewUserManager.getInstance().isForceNewUser() + this.newUserDebugInfo = NewUserManager.getInstance().getDebugInfo() + } + + private refreshKaihuDebugInfo(): void { + const manager:KaihuFlagManager = KaihuFlagManager.getInstance() + this.kaihuInputQsid = manager.getKaihuFlag() + this.kaihuDebugInfo = manager.getDebugInfo() + const userSourceManager: UserSourceManager = UserSourceManager.getInstance() + const source = userSourceManager.getUserSource() + this.userSourceInput = source + this.userSourceDebugInfo = userSourceManager.getDebugInfo() + } + + /** + * 显示站点配置弹窗 + */ + private showServerConfig() { + const dialogContext: DialogHolderContext = new DialogHolderContext( + this.getUIContext(), + new ComponentContent(this.getUIContext(), wrapBuilder(buildDebugPopup)), + { + alignment: DialogAlignment.Center, + offset: { dx: 0, dy: 0 }, + autoCancel: true, + maskColor: '#99000000', + } + ) + PromptActionClass.setDialogHolderContext(dialogContext) + PromptActionClass.openDialog() + } + + /** + * 显示主题颜色切换弹窗 + */ + private showChangeThemeColor() { + const dialogContext: DialogHolderContext = new DialogHolderContext( + this.getUIContext(), + new ComponentContent(this.getUIContext(), wrapBuilder(buildChangeThemeColorUI)), + { + alignment: DialogAlignment.Center, + offset: { dx: 0, dy: 0 }, + autoCancel: true, + maskColor: '#99000000', + } + ) + PromptActionClass.setDialogHolderContext(dialogContext) + PromptActionClass.openDialog() + } + + /** + * 显示埋点调试窗口 + */ + private showCBASDebugWindow() { + const context = getContext(this) as common.UIAbilityContext; + DebugService.getInstance()?.showWindow(context); + } +} diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugSwitchConfigPage.ets b/future_entry_demo/src/main/ets/pages/debug/DebugSwitchConfigPage.ets new file mode 100644 index 0000000..d0c7065 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugSwitchConfigPage.ets @@ -0,0 +1,356 @@ +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' +import { DebugToolUtil } from 'biz_debug' +import { PreferenceService } from 'biz_common'; +import { HXLog } from 'biz_common' +import { TradeTimeStateManager } from 'biz_trade'; +import { CJHBPushHandler } from 'biz_trade'; + +/** + * 开关配置项数据结构 + */ +interface SwitchConfigItem { + id: string + name: string + description: string + enabled: boolean + onChange?: (enabled: boolean) => void // 可选的状态变化回调 +} + +@Entry +@Component +struct DebugSwitchConfigPage { + /** + * 搜索关键字 + */ + @State searchKeyword: string = '' + /** + * 过滤后的配置列表 + */ + @State private filteredConfigList: SwitchConfigItem[] = [] + /** + * 所有开关配置项(硬编码测试数据) + */ + private allConfigs: SwitchConfigItem[] = [ + { + id: 'open_trade_entry', + name: '开启交易入口', + description: '开启后显示交易入口(审核关闭交易时生效),需要重启', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_HIDE_TRADE, + false + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_HIDE_TRADE, + enabled, + true + ) + } + }, + { + id: 'log_enable', + name: '日志开关', + description: '控制日志输出,开启后可查看日志信息', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_LOG_ENABLE, + true + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_LOG_ENABLE, + enabled, + true + ) + HXLog.setEnable(enabled) + } + }, + { + id: 'group_push_notify', + name: '分组推送提示', + description: '开启后显示分组推送提示', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_GROUP_PUSH_NOTIFY, + false + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_GROUP_PUSH_NOTIFY, + enabled, + true + ) + } + }, + { + id: 'sdk_push', + name: 'sdk交易回报推送', + description: '关闭后将拦截sdk的回报推送', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_SDK_PUSH, + true + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_SDK_PUSH, + enabled, + true + ) + CJHBPushHandler.getInstance().isInterceptSDKPush = !enabled; + } + }, + { + id: 'sub_push', + name: '主站订阅交易回报推送', + description: '关闭后将拦截主站订阅交易回报推送', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_SUB_PUSH, + true + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_SUB_PUSH, + enabled, + true + ) + CJHBPushHandler.getInstance().isInterceptSubPush = !enabled; + } + }, + { + id: 'local_date', + name: '本地时间', + description: '开启后服务器时间将用本地时间', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_LOCAL_DATE, + false + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_LOCAL_DATE, + enabled, + true + ) + } + }, { + id: 'use_server_time', + name: '使用服务器时间(交易时间提醒)', + description: '关闭后不再使用服务器时间,使用系统时间,方便盘后验证', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_DEBUG_TOOL_USE_SERVER_TIME, + true + ), + onChange: (enabled: boolean) => { + TradeTimeStateManager.getInstance().useServerTime = enabled + } + }, + { + id: 'inner_network_simulator', + name: '内网模拟器配置', + description: '开启后启用内网模拟器连接', + enabled: PreferenceService.getBooleanValueSync( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_INNER_NETWORK_SIMULATOR, + false + ), + onChange: (enabled: boolean) => { + PreferenceService.putVal( + DebugToolUtil.DEBUG_TOOL_SP_FILE_NAME, + DebugToolUtil.SP_KEY_INNER_NETWORK_SIMULATOR, + enabled, + true + ) + } + } + ] + + /** + * 组件初始化 + */ + aboutToAppear() { + this.updateFilteredConfigs() + } + + /** + * 更新过滤后的配置列表 + */ + private updateFilteredConfigs() { + if (!this.searchKeyword || this.searchKeyword.trim() === '') { + this.filteredConfigList = this.allConfigs + return + } + + const keyword = this.searchKeyword.toLowerCase() + this.filteredConfigList = this.allConfigs.filter(config => + config.name.toLowerCase().includes(keyword) || + config.description.toLowerCase().includes(keyword) + ) + } + + build() { + Column() { + StatusBar() + + Column() { + // 导航栏 - 渐变背景 + Row() { + // 返回按钮 + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back() + }) + + Text('开关配置') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + } + .width('100%') + .height(56) + .padding({ left: 12, right: 16 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + .shadow({ + radius: 8, + color: 'rgba(0, 0, 0, 0.1)', + offsetY: 2 + }) + + // 内容区域 + Column() { + // 搜索框 + Row() { + Search({ placeholder: '搜索开关配置...' }) + .width('100%') + .height(40) + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 12, right: 12 }) + .placeholderColor('#999999') + .placeholderFont({ size: 14 }) + .onChange((value: string) => { + this.searchKeyword = value + this.updateFilteredConfigs() + }) + } + .width('100%') + .padding({ + left: 16, + right: 16, + top: 0, + bottom: 0 + }) + + // 开关配置列表 + if (this.filteredConfigList.length > 0) { + List({ space: 0 }) { + ForEach(this.filteredConfigList, (item: SwitchConfigItem) => { + ListItem() { + this.buildConfigItem(item) + } + }, (item: SwitchConfigItem) => item.id) + } + .width('100%') + .layoutWeight(1) + .padding({ bottom: 34 }) + .divider({ + strokeWidth: 1, + color: '#F0F0F0', + startMargin: 16, + endMargin: 16 + }) + } + + // 无结果提示 + if (this.filteredConfigList.length === 0) { + Column() { + Text('未找到匹配的配置项') + .fontSize(14) + .fontColor('#999999') + Text('试试其他关键字') + .fontSize(12) + .fontColor('#CCCCCC') + .margin({ top: 4 }) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } + } + .layoutWeight(1) + .backgroundColor('#FAFAFA') + } + .layoutWeight(1) + } + .width('100%') + .height('100%') + .backgroundColor('#FAFAFA') + } + + /** + * 构建单个配置项 + */ + @Builder + buildConfigItem(item: SwitchConfigItem) { + Row() { + // 左侧:主文案 + 副文本 + Column() { + Text(item.name) + .fontSize(15) + .fontColor('#333333') + .fontWeight(FontWeight.Medium) + .width('100%') + .textAlign(TextAlign.Start) + + Text(item.description) + .fontSize(12) + .fontColor('#999999') + .width('100%') + .textAlign(TextAlign.Start) + .margin({ top: 4 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + // 右侧:开关 + Toggle({ type: ToggleType.Switch, isOn: item.enabled }) + .selectedColor('#4A90E2') + .onChange((isOn: boolean) => { + item.enabled = isOn + // 触发回调,用于处理持久化等逻辑 + item.onChange?.(isOn) + }) + } + .width('100%') + .padding({ + left: 16, + right: 16, + top: 16, + bottom: 16 + }) + .backgroundColor(Color.White) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/DebugYYBConfigPage.ets b/future_entry_demo/src/main/ets/pages/debug/DebugYYBConfigPage.ets new file mode 100644 index 0000000..3e1c17f --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/DebugYYBConfigPage.ets @@ -0,0 +1,657 @@ +import { router } from '@kit.ArkUI' +import { StatusBar } from '../../widget/StatusBar' +import { DebugYYBConfigUtil, IpPortInfo } from 'biz_trade/src/main/ets/utils/DebugYYBConfigUtil' +import { DebugYYBInfo, FuturesCompanyManager } from "@b2c-f/futures_ohos_trade_sdk"; +import { StrUtil, ToastUtil } from '@pura/harmony-utils'; + +/** + * 新增营业部弹窗 + */ +@CustomDialog +struct AddYYBDialog { + controller: CustomDialogController + @State qsName: string = '' + @State qsType: string = '' + @State qsId: string = '' + @State wtId: string = '' + @State testIp: string = '' + @State testPort: string = '' + @State counter: string = '' + counterDes: string = '' + onConfirm?: (qsName: string, qsType: string, qsId: string, wtId: string, testIp: string, testPort: number, counter: string) => void + + build() { + Column() { + // 标题栏 + Row() { + Text('新增营业部') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor('#1A1A1A') + .layoutWeight(1) + + Text('✕') + .fontSize(22) + .fontColor('#999999') + .onClick(() => { + this.controller.close() + }) + } + .width('100%') + .margin({ bottom: 16 }) + + // 表单内容 + Column() { + this.buildField('期商名称', this.qsName, (val) => { this.qsName = val }, '例:银河仿真') + this.buildField('期商类型', this.qsType, (val) => { this.qsType = val }, '例:CTP,需正确填写否则被过滤') + this.buildField('期商ID', this.qsId, (val) => { this.qsId = val }, 'qsId,需注意实际是按qsId分组的') + this.buildField('账户ID', this.wtId, (val) => { this.wtId = val }, 'wtId') + this.buildField('测试IP', this.testIp, (val) => { this.testIp = val }, '例:172.19.80.16') + this.buildField('测试端口', this.testPort, (val) => { this.testPort = val }, '例:5050') + + if (this.counterDes.length == 0) { + this.buildField('分组', this.counter, (val) => { this.counter = val }, '例:测试柜台') + } else { + Text('分组将继承: ' + this.counterDes) + .fontSize(12) + .fontColor('#999999') + .margin({ top: 8 }) + } + } + + // 按钮行 + Row({ space: 12 }) { + Button('取消') + .fontSize(14) + .layoutWeight(1) + .height(44) + .fontColor('#666666') + .backgroundColor('#F5F5F5') + .borderRadius(22) + .onClick(() => { + this.controller.close() + }) + + Button('保存') + .fontSize(14) + .layoutWeight(1) + .height(44) + .fontColor(Color.White) + .backgroundColor('#4A90E2') + .borderRadius(22) + .onClick(() => { + if (StrUtil.isEmpty(this.qsName) || StrUtil.isEmpty(this.qsType) || + StrUtil.isEmpty(this.qsId) || StrUtil.isEmpty(this.wtId) || + StrUtil.isEmpty(this.testIp) || StrUtil.isEmpty(this.testPort) || + (StrUtil.isEmpty(this.counterDes) && StrUtil.isEmpty(this.counter))) { + ToastUtil.showToast('填写信息不完整') + return + } + const port = Number.parseInt(this.testPort, 10) + if (isNaN(port)) { + ToastUtil.showToast('port填写不对') + return + } + if (this.onConfirm) { + this.onConfirm(this.qsName, this.qsType, this.qsId, this.wtId, this.testIp, port, this.counterDes ?? this.counter) + } + this.controller.close() + }) + } + .margin({ top: 16 }) + } + .padding(20) + .backgroundColor(Color.White) + .borderRadius(16) + } + + @Builder + buildField(label: string, value: string, onChange: (val: string) => void, placeholder: string) { + Column() { + Text(label) + .fontSize(12) + .fontColor('#666666') + .margin({ bottom: 6 }) + .align(Alignment.Start) + + TextInput({ placeholder: placeholder, text: value }) + .fontSize(14) + .height(44) + .padding({ left: 12 }) + .backgroundColor('#F5F7FA') + .borderRadius(8) + .onChange(onChange) + } + .width('100%') + .margin({ bottom: 12 }) + .alignItems(HorizontalAlign.Start) + } +} + +/** + * 营业部后门配置页面 + */ +@Entry +@Component +struct DebugYYBConfigPage { + @State groupedList: Map = new Map() + @State expandedGroups: Set = new Set() + @State editingItemKey: string = '' + @State editingIp: string = '' + @State editingPort: string = '' + @State searchText: string = '' + @State filteredList: Map = new Map() + + private configUtil = DebugYYBConfigUtil.getInstance() + private addDialogController: CustomDialogController | null = null + + aboutToAppear(): void { + this.loadData() + } + + private loadData(): void { + this.configUtil.initConfig() + this.groupedList = this.configUtil.getGroupedYYBList() + this.filterData() + const newExpanded = new Set() + this.groupedList.forEach((_: DebugYYBInfo[], key: string) => { + newExpanded.add(key) + }) + this.expandedGroups = newExpanded + } + + private filterData(): void { + if (StrUtil.isEmpty(this.searchText)) { + this.filteredList = this.groupedList + return + } + const lowerSearch = this.searchText.toLowerCase() + const filtered = new Map() + this.groupedList.forEach((items: DebugYYBInfo[], key: string) => { + const matched: DebugYYBInfo[] = [] + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.wtDes.toLowerCase().includes(lowerSearch) || + item.qsId.toLowerCase().includes(lowerSearch) || + item.wtId.toLowerCase().includes(lowerSearch) || + item.testIp.toLowerCase().includes(lowerSearch)) { + matched.push(item) + } + } + if (matched.length > 0) { + filtered.set(key, matched) + } + }) + this.filteredList = filtered + } + + private startEditing(item: DebugYYBInfo): void { + this.editingItemKey = item.qsId + '_' + item.wtId + this.editingIp = item.testIp + this.editingPort = item.testPort.toString() + } + + private cancelEditing(): void { + this.editingItemKey = '' + this.editingIp = '' + this.editingPort = '' + } + + private confirmEditing(): void { + if (this.editingItemKey === '') { + return + } + const parts = this.editingItemKey.split('_') + if (parts.length !== 2) { + return + } + const qsId = parts[0] + const wtId = parts[1] + const port = Number.parseInt(this.editingPort, 10) + if (isNaN(port) || port <= 0) { + ToastUtil.showToast('端口必须是正整数') + return + } + this.configUtil.modifyItem(qsId, wtId, this.editingIp, port) + this.loadData() + this.cancelEditing() + ToastUtil.showToast('修改成功') + } + + private getSelectOptions(): SelectOption[] { + const options: SelectOption[] = [] + const presets = this.configUtil.getPresetIpPortList() + for (let i = 0; i < presets.length; i++) { + const option: SelectOption = { + value: presets[i].ip + ':' + presets[i].port.toString(), + symbolIcon: undefined + } + options.push(option) + } + return options + } + + @Builder + buildYYBItem(item: DebugYYBInfo) { + Column() { + // 标题行 + Row() { + Text(item.wtDes) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor('#1A1A1A') + .layoutWeight(1) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + + if (this.editingItemKey !== item.qsId + '_' + item.wtId) { + Button('编辑') + .fontSize(12) + .height(28) + .padding({ left: 12, right: 12 }) + .fontColor('#4A90E2') + .backgroundColor('#E8F4FD') + .borderRadius(14) + .onClick(() => { + this.startEditing(item) + }) + + Button('删除') + .fontSize(12) + .height(28) + .padding({ left: 12, right: 12 }) + .margin({ left: 8 }) + .fontColor('#F44336') + .backgroundColor('#FFEBEE') + .borderRadius(14) + .onClick(() => { + this.configUtil.deleteItem(item.qsId, item.wtId) + this.loadData() + ToastUtil.showToast('删除成功') + }) + } + } + .width('100%') + .margin({ bottom: 10 }) + + // ID行 + Row() { + Text('qsId: ' + item.qsId + ' wtId: ' + item.wtId) + .fontSize(12) + .fontColor('#999999') + } + .width('100%') + .margin({ bottom: 12 }) + + // IP行 + if (this.editingItemKey === item.qsId + '_' + item.wtId) { + Column() { + // 编辑模式 + Row() { + TextInput({ text: this.editingIp }) + .fontSize(14) + .layoutWeight(2) + .height(40) + .padding({ left: 12 }) + .backgroundColor('#F5F7FA') + .borderRadius(8) + .onChange((value: string) => { + this.editingIp = value + }) + + Text(':') + .fontSize(14) + .fontColor('#666666') + .margin({ left: 8, right: 8 }) + + TextInput({ text: this.editingPort }) + .fontSize(14) + .layoutWeight(1) + .height(40) + .padding({ left: 12 }) + .backgroundColor('#F5F7FA') + .borderRadius(8) + .onChange((value: string) => { + this.editingPort = value + }) + } + .width('100%') + + // 预设IP下拉选择 + Row() { + Text('预设IP') + .fontSize(12) + .fontColor('#666666') + .margin({ right: 8 }) + + Select(this.getSelectOptions()) + .selected(0) + .value('选择预设IP') + .backgroundColor('#F5F7FA') + .borderRadius(8) + .layoutWeight(1) + .height(36) + .onSelect((index: number) => { + const preset = this.configUtil.getPresetIpPortList()[index] + if (preset) { + this.editingIp = preset.ip + this.editingPort = preset.port.toString() + } + }) + } + .width('100%') + .margin({ top: 10 }) + + // 确认取消按钮 + Row() { + Button('取消') + .fontSize(13) + .layoutWeight(1) + .height(36) + .fontColor('#666666') + .backgroundColor('#F5F5F5') + .borderRadius(8) + .onClick(() => { + this.cancelEditing() + }) + + Button('保存') + .fontSize(13) + .layoutWeight(1) + .height(36) + .margin({ left: 10 }) + .fontColor('#FFFFFF') + .backgroundColor('#4A90E2') + .borderRadius(8) + .onClick(() => { + this.confirmEditing() + }) + } + .width('100%') + .margin({ top: 12 }) + } + } else { + // 显示模式 + Row() { + Row() { + Text('ip: ') + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor('#1A1A1A') + + Text(item.testIp + ':' + item.testPort.toString()) + .fontSize(14) + .fontColor('#4A90E2') + .fontWeight(FontWeight.Medium) + .backgroundColor('#E8F4FD') + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + .borderRadius(6) + } + .onClick(() => { + this.startEditing(item) + }) + } + .width('100%') + .bindContextMenu(this.buildContextMenu(item), ResponseType.LongPress) + } + } + .width('100%') + .padding(14) + .backgroundColor('#FFFFFF') + .borderRadius(12) + .shadow({ + radius: 8, + color: 'rgba(0, 0, 0, 0.04)', + offsetY: 2 + }) + } + + @Builder + buildContextMenu(item: DebugYYBInfo) { + Column() { + Text('快速选择IP') + .fontSize(13) + .fontColor('#666666') + .fontWeight(FontWeight.Medium) + .padding(12) + .width('100%') + + Divider().color('#EEEEEE') + + ForEach(this.configUtil.getPresetIpPortList(), (preset: IpPortInfo) => { + Text(preset.ip + ':' + preset.port.toString()) + .fontSize(14) + .fontColor('#1A1A1A') + .width('100%') + .padding(12) + .onClick(() => { + this.configUtil.modifyItem(item.qsId, item.wtId, preset.ip, preset.port) + this.loadData() + ToastUtil.showToast('已应用: ' + preset.ip + ':' + preset.port.toString()) + }) + }, (preset: IpPortInfo) => preset.ip + '_' + preset.port.toString()) + } + .width(180) + .backgroundColor(Color.White) + .clip(true) + .borderRadius(12) + .shadow({ + radius: 12, + color: 'rgba(0,0,0,0.15)', + offsetY: 4 + }) + } + + @Builder + buildGroupSection(counterDes: string, items: DebugYYBInfo[]) { + Column() { + // 分组标题 + Row() { + Text(this.expandedGroups.has(counterDes) ? '▼' : '▶') + .fontSize(12) + .fontColor('#666666') + .width(24) + + Text(counterDes) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor('#333333') + .layoutWeight(1) + + Text(items.length + ' 个配置') + .fontSize(12) + .fontColor('#999999') + + Button('+ 新增') + .fontSize(12) + .height(26) + .padding({ left: 10, right: 10 }) + .margin({ left: 10 }) + .fontColor('#4A90E2') + .backgroundColor('#E8F4FD') + .borderRadius(13) + .onClick(() => { + this.openAddDialog(counterDes) + }) + } + .width('100%') + .padding({ left: 12, right: 12, bottom: 12 }) + .backgroundColor('#F5F7FA') + .borderRadius(12) + .onClick(() => { + if (this.expandedGroups.has(counterDes)) { + const newSet = new Set() + this.expandedGroups.forEach((v: string) => { + if (v !== counterDes) { + newSet.add(v) + } + }) + this.expandedGroups = newSet + } else { + const newSet = new Set() + this.expandedGroups.forEach((v: string) => newSet.add(v)) + newSet.add(counterDes) + this.expandedGroups = newSet + } + }) + + // 分组内容 + if (this.expandedGroups.has(counterDes)) { + Column({ space: 10 }) { + ForEach(items, (item: DebugYYBInfo) => { + this.buildYYBItem(item) + }, (item: DebugYYBInfo) => item.qsId + '_' + item.wtId) + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + } + .width('100%') + .margin({ bottom: 16 }) + } + + private openAddDialog(counterDes: string): void { + this.addDialogController = new CustomDialogController({ + builder: AddYYBDialog({ + counterDes: counterDes, + onConfirm: (qsName: string, qsType: string, qsId: string, wtId: string, testIp: string, testPort: number, counter: string) => { + this.configUtil.addItem(qsName, qsType, counter, qsId, wtId, testIp, testPort) + this.loadData() + FuturesCompanyManager.getInstance().init(); + ToastUtil.showToast('新增成功') + } + }), + alignment: DialogAlignment.Center, + customStyle: true + }) + this.addDialogController.open() + } + + build() { + Stack() { + Column() { + StatusBar() + + // 导航栏 + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back() + }) + + Text('营业部后门配置') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + + Button('保存') + .fontSize(13) + .height(32) + .fontColor('#FFFFFF') + .backgroundColor('rgba(255, 255, 255, 0.2)') + .borderRadius(16) + .onClick(() => { + this.configUtil.saveConfig() + ToastUtil.showToast('保存成功') + }) + } + .width('100%') + .height(56) + .padding({ left: 12, right: 16 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + .shadow({ + radius: 8, + color: 'rgba(0, 0, 0, 0.1)', + offsetY: 2 + }) + + // 搜索框 + Column() { + // 提示文字 + Text('💡 编辑/新增仅临时生效,点击右上角"保存"才会持久化') + .fontSize(11) + .fontColor('#FF9800') + .margin({ top: 6 }) + .width('100%') + .textAlign(TextAlign.Start) + + Search({ placeholder: '搜索名称、ID或IP', value: this.searchText }) + .width('100%') + .height(44) + .backgroundColor('#FFFFFF') + .borderRadius(10) + .border({ width: 1.5, color: '#E0E0E0' }) + .placeholderColor('#999999') + .placeholderFont({ size: 13 }) + .onChange((value: string) => { + this.searchText = value + this.filterData() + }) + } + .width('100%') + .padding(16) + .backgroundColor('#F5F7FA') + + // 底部操作按钮 + Row({ space: 10 }) { + Button('重置默认') + .fontSize(13) + .layoutWeight(1) + .height(40) + .fontColor('#F44336') + .backgroundColor('#FFEBEE') + .borderRadius(8) + .onClick(() => { + this.configUtil.resetConfig() + this.loadData() + ToastUtil.showToast('已重置为默认配置') + }) + + Button('新增配置') + .fontSize(13) + .layoutWeight(1) + .height(40) + .fontColor('#FFFFFF') + .backgroundColor('#4A90E2') + .borderRadius(8) + .onClick(() => { + this.openAddDialog('') + }) + } + .width('100%') + .padding({ left: 16, right: 16, bottom: 10 }) + + // 列表 + Scroll() { + Column() { + ForEach(Array.from(this.filteredList.entries()), (entry: [string, DebugYYBInfo[]]) => { + this.buildGroupSection(entry[0], entry[1]) + }) + } + .width('100%') + } + .layoutWeight(1) + .backgroundColor('#F5F7FA') + .scrollBar(BarState.Auto) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#F5F7FA') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/GrayTestDebugPage.ets b/future_entry_demo/src/main/ets/pages/debug/GrayTestDebugPage.ets new file mode 100644 index 0000000..e365de7 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/GrayTestDebugPage.ets @@ -0,0 +1,17 @@ +import { GrayTestDebugPage } from 'lib_graytestmanager' +import { StatusBar } from '../../widget/StatusBar'; + +@Entry +@Component +struct GrayTestDebugPageWrapper { + build() { + Column() { + StatusBar() + GrayTestDebugPage() + .padding({bottom: 34}) + .width('100%') + } + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/debug/IBSPointDetailDebugPage.ets b/future_entry_demo/src/main/ets/pages/debug/IBSPointDetailDebugPage.ets new file mode 100644 index 0000000..bc78e5f --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/IBSPointDetailDebugPage.ets @@ -0,0 +1,697 @@ +/** + * IBSPointDetail 程序详情测试页面 + * 用于添加、编辑、删除 IBSPointDetail 测试数据 + */ + +import { router } from '@kit.ArkUI'; +import { StatusBar } from '../../widget/StatusBar'; +import { BSPointDetailItem, DebugKLineBSPointMockDataManager } from 'biz_debug'; + +@Entry +@Component +struct IBSPointDetailDebugPage { + @State dataList: BSPointDetailItem[] = []; + @State showAddDialog: boolean = false; + @State editingItem: BSPointDetailItem | null = null; + + // 确认弹窗 + @State showConfirmDialog: boolean = false; + @State confirmDialogTitle: string = ''; + @State confirmDialogMessage: string = ''; + @State confirmActionType: string = ''; + @State confirmActionId: string = ''; + + // 表单数据 + @State formInstrument: string = ''; + @State formDealDate: string = ''; + @State formDirection: string = '买'; + @State formCombOffset: string = '开'; + @State formVolume: string = '1'; + @State formPrice: string = ''; + @State formProfit: string = '0'; + + // Picker 相关 + @State showPicker: boolean = false; + @State pickerType: string = ''; // 'direction' | 'combOffset' + + // 方向选项 + private directionOptions: string[] = ['买', '卖']; + // 开平选项 + private combOffsetOptions: string[] = ['开', '平']; + + // 数据管理器 + private dataManager: DebugKLineBSPointMockDataManager = DebugKLineBSPointMockDataManager.getInstance(); + + aboutToAppear(): void { + this.loadData(); + } + + private async loadData(): Promise { + // 从 DebugKLineBSPointMockDataManager 加载数据 + this.dataList = this.dataManager.getBSPointDetailMockList(); + } + + // 获取当前日期 + private getCurrentDate(): string { + const now = new Date(); + const year = now.getFullYear().toString(); + const month = (now.getMonth() + 1).toString().padStart(2, '0'); + const day = now.getDate().toString().padStart(2, '0'); + return year + month + day; + } + + // 打开新增/编辑弹窗 + openDialog(item?: BSPointDetailItem): void { + if (item) { + this.editingItem = item; + this.formInstrument = item.instrument; + this.formDealDate = item.dealDate; + this.formDirection = item.direction; + this.formCombOffset = item.combOffset; + this.formVolume = item.volume.toString(); + this.formPrice = item.price.toString(); + this.formProfit = item.profit; + } else { + this.editingItem = null; + // 新增时:如果有数据,默认填充最后一条 + if (this.dataList.length > 0) { + const lastItem = this.dataList[this.dataList.length - 1]; + this.formInstrument = lastItem.instrument; + this.formDealDate = lastItem.dealDate; + this.formDirection = lastItem.direction; + this.formCombOffset = lastItem.combOffset; + this.formVolume = lastItem.volume.toString(); + this.formPrice = lastItem.price.toString(); + this.formProfit = lastItem.profit; + } else { + this.formInstrument = ''; + this.formDealDate = this.getCurrentDate(); + this.formDirection = '买'; + this.formCombOffset = '开'; + this.formVolume = '1'; + this.formPrice = ''; + this.formProfit = '0'; + } + } + this.showAddDialog = true; + } + + // 关闭弹窗 + closeDialog(): void { + this.showAddDialog = false; + this.editingItem = null; + this.showPicker = false; + } + + // 保存数据 + async saveData(): Promise { + if (!this.formInstrument.trim()) { + AlertDialog.show({ message: '请输入合约名称' }); + return; + } + if (!this.formDealDate.trim()) { + AlertDialog.show({ message: '请输入成交日期' }); + return; + } + if (!this.formPrice.trim()) { + AlertDialog.show({ message: '请输入成交价格' }); + return; + } + + const volume = parseInt(this.formVolume); + const price = parseFloat(this.formPrice); + + if (isNaN(volume) || volume <= 0) { + AlertDialog.show({ message: '请输入有效的手数' }); + return; + } + if (isNaN(price) || price <= 0) { + AlertDialog.show({ message: '请输入有效的价格' }); + return; + } + + const item = new BSPointDetailItem() + item.id = this.editingItem ? this.editingItem.id : '' + item.instrument = this.formInstrument.trim() + item.dealDate = this.formDealDate.trim() + item.direction = this.formDirection + item.combOffset = this.formCombOffset + item.volume = volume + item.price = price + item.profit = this.formProfit || '0' + + let success: boolean; + if (this.editingItem) { + success = await this.dataManager.updateBSPointDetailMockData(item); + } else { + success = await this.dataManager.addBSPointDetailMockData(item); + } + + if (success) { + this.dataList = this.dataManager.getBSPointDetailMockList(); + } + this.closeDialog(); + } + + // 删除数据 + deleteData(id: string): void { + this.confirmDialogTitle = '确认删除'; + this.confirmDialogMessage = '确定删除该数据吗?'; + this.confirmActionType = 'delete'; + this.confirmActionId = id; + this.showConfirmDialog = true; + } + + // 清空所有数据 + clearAll(): void { + this.confirmDialogTitle = '确认清空'; + this.confirmDialogMessage = '确定清空所有数据吗?此操作不可恢复'; + this.confirmActionType = 'clear'; + this.confirmActionId = ''; + this.showConfirmDialog = true; + } + + // 处理删除确认 + async handleDelete(): Promise { + await this.dataManager.deleteBSPointDetailMockData(this.confirmActionId); + this.dataList = this.dataManager.getBSPointDetailMockList(); + } + + // 处理清空确认 + async handleClear(): Promise { + await this.dataManager.clearAllBSPointDetailMockData(); + this.dataList = this.dataManager.getBSPointDetailMockList(); + } + + // 格式化日期显示 + formatDate(date: string): string { + if (date.length === 8) { + return `${date.substring(0, 4)}-${date.substring(4, 6)}-${date.substring(6, 8)}`; + } + return date; + } + + build() { + Column() { + StatusBar() + this.buildHeader() + this.buildContent() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.background_f5f5f5')) + } + + @Builder + buildHeader(): void { + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back(); + }) + + Text(`成交明细测试数据 (${this.dataList.length})`) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + + if (this.dataList.length > 0) { + Text('清空') + .fontSize(14) + .fontColor('#FFFFFF') + .onClick(() => { + this.clearAll(); + }) + } + } + .width('100%') + .height(56) + .padding({ left: 12, right: 12 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + } + + @Builder + buildContent(): void { + Stack({ alignContent: Alignment.TopStart }) { + if (this.dataList.length === 0) { + this.buildEmptyView() + } else { + this.buildDataList() + } + + // 底部添加按钮 + Row() { + Button('+ 添加成交明细') + .width('90%') + .height(44) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .onClick(() => { + this.openDialog(); + }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Bottom) + .padding({ bottom: 24 }) + .position({ y: '100%' }) + .translate({ y: -80 }) + + // 新增/编辑弹窗 + if (this.showAddDialog) { + this.buildDialog() + } + + // 确认弹窗 + if (this.showConfirmDialog) { + this.buildConfirmDialog() + } + + // 选择器弹窗 + if (this.showPicker) { + this.buildPicker() + } + } + .width('100%') + .layoutWeight(1) + } + + @Builder + buildEmptyView(): void { + Column() { + Text('暂无成交明细数据') + .fontSize(16) + .fontColor($r('app.color.elements_text_secondary')) + Text('点击下方按钮添加成交明细测试数据') + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .margin({ top: 8 }) + + Column() { + Text('数据格式说明') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 8 }) + Text('• 合约名称:如 IF2406') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 成交日期:yyyyMMdd(如 20260415)') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 买卖方向:买/卖') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 开平方向:开/平') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 成交价格:交易价格') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 成交量:交易手数') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 平仓盈亏:平仓时显示') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + } + .alignItems(HorizontalAlign.Start) + .margin({ top: 24, left: 20, right: 20 }) + .padding(16) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .padding({ bottom: 100 }) + } + + @Builder + buildDataList(): void { + Column() { + List({ space: 8 }) { + ForEach(this.dataList, (item: BSPointDetailItem) => { + ListItem() { + this.buildDataItem(item) + } + .padding({ left: 16, right: 16 }) + }, (item: BSPointDetailItem) => item.id) + } + .width('100%') + .layoutWeight(1) + .padding({ top: 8, bottom: 80 }) + } + .width('100%') + } + + @Builder + buildDataItem(item: BSPointDetailItem): void { + Row() { + Column() { + Row({ space: 8 }) { + Text(item.instrument) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.elements_text_primary_02')) + Text(`${item.direction}${item.combOffset}`) + .fontSize(12) + .fontColor(this.getDirectionColor(item.direction)) + } + + Row({ space: 8 }) { + Text(this.formatDate(item.dealDate)) + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text(`价格: ${item.price}`) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + Text(`手数: ${item.volume}`) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + } + .margin({ top: 4 }) + + if (item.combOffset === '平' && item.profit) { + Text(`盈亏: ${item.profit}`) + .fontSize(12) + .fontColor(item.profit.startsWith('-') ? '#07ab4a' : '#ff2436') + .margin({ top: 2 }) + } + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Row({ space: 12 }) { + Text('编辑') + .fontSize(14) + .fontColor('#4A90E2') + .onClick(() => { + this.openDialog(item); + }) + + Text('删除') + .fontSize(14) + .fontColor('#FF6B6B') + .onClick(() => { + this.deleteData(item.id); + }) + } + } + .width('100%') + .padding(12) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + } + + @Builder + buildConfirmDialog(): void { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.showConfirmDialog = false; + }) + + Column() { + Text(this.confirmDialogTitle) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 12 }) + + Text(this.confirmDialogMessage) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 20 }) + + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor('#FFFFFF') + .onClick(() => { + this.showConfirmDialog = false; + }) + + Button('确定') + .layoutWeight(1) + .height(40) + .backgroundColor('#FF6B6B') + .fontColor('#FFFFFF') + .onClick(() => { + this.showConfirmDialog = false; + setTimeout(() => { + if (this.confirmActionType === 'delete') { + this.handleDelete(); + } else if (this.confirmActionType === 'clear') { + this.handleClear(); + } + }, 100); + }) + } + } + .width('80%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '10%', y: '40%' }) + } + + @Builder + buildDialog(): void { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.closeDialog(); + }) + + Scroll() { + Column() { + Text(this.editingItem ? '编辑成交明细' : '新增成交明细') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 20 }) + + // 合约名称 + this.buildFormItemTextInput('合约名称', this.formInstrument, (val: string) => { + this.formInstrument = val; + }, InputType.Normal, '例如: IF2406') + + // 成交日期 + this.buildFormItemTextInput('成交日期', this.formDealDate, (val: string) => { + this.formDealDate = val; + }, InputType.Number, '例如: 20260415') + + // 买卖方向 + this.buildFormItemSelect('买卖方向', 'direction', () => { + this.pickerType = 'direction'; + this.showPicker = true; + }) + + // 开平方向 + this.buildFormItemSelect('开平方向', 'combOffset', () => { + this.pickerType = 'combOffset'; + this.showPicker = true; + }) + + // 成交价格 + this.buildFormItemTextInput('成交价格', this.formPrice, (val: string) => { + this.formPrice = val; + }, InputType.Number, '例如: 3850.0') + + // 成交量 + this.buildFormItemTextInput('成交量', this.formVolume, (val: string) => { + this.formVolume = val; + }, InputType.Number, '例如: 1') + + // 平仓盈亏 + this.buildFormItemTextInput('平仓盈亏', this.formProfit, (val: string) => { + this.formProfit = val; + }, InputType.Normal, '例如: 100') + + // 按钮 + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor('#FFFFFF') + .onClick(() => { + this.closeDialog(); + }) + + Button('保存') + .layoutWeight(1) + .height(40) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .onClick(() => { + this.saveData(); + }) + } + .margin({ top: 20 }) + } + .width('90%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + } + .width('100%') + .height('60%') + .position({ x: '5%', y: '20%' }) + .scrollBar(BarState.Auto) + } + + @Builder + buildFormItemTextInput( + label: string, + value: string, + onChange: (val: string) => void, + inputType: InputType = InputType.Normal, + placeholder: string = '' + ): void { + Row() { + Text(label) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(80) + + TextInput({ text: value, placeholder: placeholder }) + .type(inputType) + .onChange((val: string) => { + onChange(val); + }) + .width(150) + } + .width('100%') + .margin({ bottom: 12 }) + } + + @Builder + buildFormItemSelect(label: string, fieldName: 'direction' | 'combOffset', onClick: () => void): void { + Row() { + Text(label) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(80) + + Column() { + Text(fieldName === 'direction' ? this.formDirection : this.formCombOffset) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width(150) + .height(36) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(6) + .borderWidth(1) + .borderColor($r('app.color.border_color_ffe0e0e0')) + .onClick(onClick) + } + } + .width('100%') + .margin({ bottom: 12 }) + } + + @Builder + buildPicker(): void { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.showPicker = false; + }) + + Column() { + Text(this.pickerType === 'direction' ? '选择买卖方向' : '选择开平方向') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 16 }) + + List() { + ForEach( + this.pickerType === 'direction' ? this.directionOptions : this.combOffsetOptions, + (option: string) => { + ListItem() { + Row() { + Text(option) + .fontSize(14) + .fontColor( + (this.pickerType === 'direction' ? this.formDirection : this.formCombOffset) === option + ? Color.White + : $r('app.color.elements_text_primary_02') + ) + .fontWeight( + (this.pickerType === 'direction' ? this.formDirection : this.formCombOffset) === option + ? FontWeight.Medium + : FontWeight.Normal + ) + + if ((this.pickerType === 'direction' ? this.formDirection : this.formCombOffset) === option) { + Text('✓') + .fontSize(14) + .fontColor(Color.White) + .margin({ left: 8 }) + } + } + .width('100%') + .padding({ top: 12, bottom: 12 }) + .justifyContent(FlexAlign.Center) + .backgroundColor( + (this.pickerType === 'direction' ? this.formDirection : this.formCombOffset) === option + ? $r('app.color.blue_color_5370C6') + : Color.Transparent + ) + .borderRadius(8) + } + .onClick(() => { + if (this.pickerType === 'direction') { + this.formDirection = option; + } else { + this.formCombOffset = option; + } + this.showPicker = false; + }) + }, + (option: string) => option + ) + } + .width('80%') + .height(120) + } + .width('70%') + .padding(16) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '15%', y: '30%' }) + } + + // 获取方向颜色 + private getDirectionColor(direction: string): ResourceColor { + if (direction === '买') { + return '#ff2436'; + } else { + return '#07ab4a'; + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/KLineBSPointMockDataPage.ets b/future_entry_demo/src/main/ets/pages/debug/KLineBSPointMockDataPage.ets new file mode 100644 index 0000000..fccfdc6 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/KLineBSPointMockDataPage.ets @@ -0,0 +1,711 @@ +/** + * K线成交点模拟数据管理页面 + * 独立的页面,用于添加、编辑、删除 K 线成交点模拟数据 + */ + +import { router } from '@kit.ArkUI'; +import { DebugKLineBSPointMockDataManager, DebugKLineBSPointMockData, KLineBSPointMockDataJson, TradeOperationType, TradeTypeOption } from 'biz_debug'; +import { ToastUtil } from '@pura/harmony-utils'; +import { GlobalContext } from 'biz_common'; +import { StatusBar } from '../../widget/StatusBar'; + +type MockDataItem = KLineBSPointMockDataJson; + +@Entry +@Component +struct KLineBSPointMockDataPage { + @State mockDataList: MockDataItem[] = []; + @State isLoading: boolean = false; + @State showAddDialog: boolean = false; + @State editingItem: MockDataItem | null = null; + @State enableMockData: boolean = false; + + // 确认弹窗 + @State showConfirmDialog: boolean = false; + @State confirmDialogTitle: string = ''; + @State confirmDialogMessage: string = ''; + @State confirmActionType: string = ''; // 'delete' or 'clear' + @State confirmActionId: string = ''; + + // 表单数据 + @State formCode: string = ''; + @State formDate: string = ''; + @State formPrice: string = ''; + @State formCount: string = '1'; + @State formType: TradeOperationType = TradeOperationType.BUY_OPEN; + + // Picker 相关 + @State showTypePicker: boolean = false; + @State selectedTypeIndex: number = 0; + + // 类型选项 + private typeOptions: TradeTypeOption[] = DebugKLineBSPointMockData.getTypeOptions(); + + private dataManager: DebugKLineBSPointMockDataManager = DebugKLineBSPointMockDataManager.getInstance(); + + aboutToAppear(): void { + this.initData(); + } + + async initData(): Promise { + this.isLoading = true; + try { + await this.dataManager.init(GlobalContext.get()); + await this.loadData(); + } catch (error) { + ToastUtil.showToast('初始化失败'); + } finally { + this.isLoading = false; + } + } + + async loadData(): Promise { + const list = this.dataManager.getMockDataList(); + this.mockDataList = list.map(item => item.toJson()); + // 加载开关状态 + this.enableMockData = this.dataManager.isEnableMockData(); + } + + // 获取当前日期 yyyyMMdd + private getCurrentDate(): string { + const now = new Date(); + const year = now.getFullYear().toString(); + const month = (now.getMonth() + 1).toString().padStart(2, '0'); + const day = now.getDate().toString().padStart(2, '0'); + return year + month + day; + } + + // 打开新增/编辑弹窗 + openDialog(item?: MockDataItem): void { + if (item) { + // 编辑模式 + this.editingItem = item; + this.formCode = item.code; + this.formDate = item.date; + this.formPrice = item.price; + this.formCount = item.count.toString(); + this.formType = item.type; + // 更新 Picker 索引 + const typeIndex = this.typeOptions.findIndex(opt => opt.value === item.type); + this.selectedTypeIndex = typeIndex >= 0 ? typeIndex : 0; + } else { + // 新增模式 + this.editingItem = null; + this.formCode = 'ec2604'; + this.formDate = this.getCurrentDate(); + this.formPrice = ''; + this.formCount = '1'; + this.formType = TradeOperationType.BUY_OPEN; + this.selectedTypeIndex = 0; + + // 根据最后一个数据设置默认值 + if (this.mockDataList.length > 0) { + const lastItem = this.mockDataList[this.mockDataList.length - 1]; + this.formCode = lastItem.code; + this.formPrice = lastItem.price; + this.formDate = lastItem.date; + } + } + this.showAddDialog = true; + } + + // 关闭弹窗 + closeDialog(): void { + this.showAddDialog = false; + this.editingItem = null; + } + + // 保存数据 + async saveData(): Promise { + if (!this.formCode.trim()) { + ToastUtil.showToast('请输入合约代码'); + return; + } + if (!this.formDate.trim()) { + ToastUtil.showToast('请输入交易日期'); + return; + } + // 日期格式校验 yyyyMMdd + const dateRegex = /^\d{8}$/; + if (!dateRegex.test(this.formDate.trim())) { + ToastUtil.showToast('日期格式应为 yyyyMMdd'); + return; + } + if (!this.formPrice.trim()) { + ToastUtil.showToast('请输入价格'); + return; + } + + const count = parseInt(this.formCount); + if (isNaN(count) || count <= 0) { + ToastUtil.showToast('请输入有效的手数'); + return; + } + + const mockData = new DebugKLineBSPointMockData( + this.formCode.trim(), + this.formDate.trim(), + this.formPrice.trim(), + count, + this.formType + ); + + let success = false; + if (this.editingItem) { + mockData.id = this.editingItem.id; + success = await this.dataManager.updateMockData(mockData); + if (success) { + ToastUtil.showToast('更新成功'); + } + } else { + success = await this.dataManager.addMockData(mockData); + if (success) { + ToastUtil.showToast('添加成功'); + } + } + + if (success) { + await this.loadData(); + this.closeDialog(); + } else { + ToastUtil.showToast('保存失败'); + } + } + + // 删除数据 + deleteData(id: string): void { + this.confirmDialogTitle = '确认删除'; + this.confirmDialogMessage = '确定删除该数据吗?'; + this.confirmActionType = 'delete'; + this.confirmActionId = id; + this.showConfirmDialog = true; + } + + // 清空所有数据 + clearAll(): void { + this.confirmDialogTitle = '确认清空'; + this.confirmDialogMessage = '确定清空所有K线模拟数据吗?此操作不可恢复'; + this.confirmActionType = 'clear'; + this.confirmActionId = ''; + this.showConfirmDialog = true; + } + + // 处理删除确认 + async handleDelete(id: string): Promise { + const success = await this.dataManager.deleteMockData(id); + if (success) { + ToastUtil.showToast('删除成功'); + await this.loadData(); + } else { + ToastUtil.showToast('删除失败'); + } + } + + // 处理清空确认 + async handleClear(): Promise { + const success = await this.dataManager.clearAllMockData(); + if (success) { + ToastUtil.showToast('已清空所有数据'); + await this.loadData(); + } else { + ToastUtil.showToast('清空失败'); + } + } + + // 格式化显示文本 + getDisplayText(item: MockDataItem): string { + const typeName = DebugKLineBSPointMockData.getTypeNameByValue(item.type); + return `${item.code} ${item.price} ${typeName} ${item.count}手`; + } + + // 格式化日期显示 + formatDateDisplay(date: string): string { + if (date.length === 8) { + return `${date.substring(0, 4)}-${date.substring(4, 6)}-${date.substring(6, 8)}`; + } + return date; + } + + // 获取类型名称 + getTypeName(type: TradeOperationType): string { + return DebugKLineBSPointMockData.getTypeNameByValue(type); + } + + build() { + Column() { + StatusBar() + // 顶部导航 + this.buildHeader() + // 开关控制面板 + this.buildSwitchPanel() + // 内容区域 + this.buildContent() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.ffffff_2b2b2b')) + } + + @Builder + buildHeader(): void { + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back(); + }) + + Text(`K线成交点模拟数据 (${this.mockDataList.length})`) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + + if (this.mockDataList.length > 0) { + Text('清空') + .fontSize(14) + .fontColor('#FFFFFF') + .onClick(() => { + this.clearAll(); + }) + } + } + .width('100%') + .height(56) + .padding({ left: 12, right: 12 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + } + + @Builder + buildSwitchPanel(): void { + Row() { + Text('启用模拟数据') + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + Blank() + Toggle({ type: ToggleType.Switch, isOn: this.enableMockData }) + .selectedColor('#4A90E2') + .onChange((isOn: boolean) => { + this.enableMockData = isOn; + this.dataManager.setEnableMockData(isOn); + }) + } + .width('100%') + .padding({ left: 16, right: 16, top: 12, bottom: 12 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + buildContent(): void { + Stack({ alignContent: Alignment.TopStart }) { + if (this.isLoading) { + Progress({ value: 0, type: ProgressType.Ring }) + .width(48) + .height(48) + .alignSelf(ItemAlign.Center) + } else if (this.mockDataList.length === 0) { + this.buildEmptyView() + } else { + this.buildDataList() + } + + // 底部添加按钮 + Row() { + Button('+ 添加模拟数据') + .width('90%') + .height(44) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .onClick(() => { + this.openDialog(); + }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Bottom) + .padding({ bottom: 24 }) + .position({ y: '100%' }) + .translate({ y: -80 }) + + // 新增/编辑弹窗 + if (this.showAddDialog) { + this.buildDialog() + } + + // 确认弹窗 + if (this.showConfirmDialog) { + this.buildConfirmDialog() + } + } + .width('100%') + .layoutWeight(1) + } + + @Builder + buildConfirmDialog(): void { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.showConfirmDialog = false; + }) + + Column() { + Text(this.confirmDialogTitle) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 12 }) + + Text(this.confirmDialogMessage) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 20 }) + + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor('#FFFFFF') + .onClick(() => { + this.showConfirmDialog = false; + }) + + Button('确定') + .layoutWeight(1) + .height(40) + .backgroundColor('#FF6B6B') + .fontColor('#FFFFFF') + .onClick(() => { + const actionType = this.confirmActionType; + const actionId = this.confirmActionId; + this.showConfirmDialog = false; + setTimeout(() => { + if (actionType === 'delete') { + this.handleDelete(actionId); + } else if (actionType === 'clear') { + this.handleClear(); + } + }, 100); + }) + } + } + .width('80%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '10%', y: '40%' }) + } + + @Builder + buildEmptyView(): void { + Column() { + Text('暂无K线模拟数据') + .fontSize(16) + .fontColor($r('app.color.elements_text_secondary')) + Text('点击下方按钮添加K线成交点模拟数据') + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .margin({ top: 8 }) + + Column() { + Text('数据格式说明') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 8 }) + Text('• 合约代码:如 ec2604') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 交易日期:yyyyMMdd(如 20260415)') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 价格:交易价格') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 手数:交易数量') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text('• 类型:交易类型(买开/卖开/买平/卖平等)') + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + } + .alignItems(HorizontalAlign.Start) + .margin({ top: 24, left: 20, right: 20 }) + .padding(16) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .padding({ bottom: 100 }) + } + + @Builder + buildDataList(): void { + Column() { + List({ space: 8 }) { + ForEach(this.mockDataList, (item: MockDataItem) => { + ListItem() { + this.buildDataItem(item) + } + .padding({ left: 16, right: 16 }) + }, (item: MockDataItem) => item.id) + } + .width('100%') + .layoutWeight(1) + .padding({ top: 8, bottom: 80 }) + } + .width('100%') + } + + @Builder + buildDataItem(item: MockDataItem): void { + Row() { + Column() { + Row({ space: 8 }) { + Text(this.formatDateDisplay(item.date)) + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + Text(item.code) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.elements_text_primary_02')) + Text(item.price) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + } + + Row({ space: 8 }) { + Text(this.getDisplayText(item)) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + } + .margin({ top: 4 }) + + Text(this.getTypeName(item.type)) + .fontSize(11) + .fontColor('#4A90E2') + .margin({ top: 2 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Row({ space: 12 }) { + Text('编辑') + .fontSize(14) + .fontColor('#4A90E2') + .onClick(() => { + this.openDialog(item); + }) + + Text('删除') + .fontSize(14) + .fontColor('#FF6B6B') + .onClick(() => { + this.deleteData(item.id); + }) + } + } + .width('100%') + .padding(12) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(8) + } + + @Builder + buildDialog(): void { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.closeDialog(); + }) + + Column() { + Text(this.editingItem ? '编辑K线模拟数据' : '新增K线模拟数据') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 20 }) + + // 合约代码 + this.buildFormItemTextInput('合约代码', this.formCode, (val: string) => { + this.formCode = val; + }, InputType.Normal, '例如: fu2605') + + // 交易日期 + this.buildFormItemTextInput('交易日期', this.formDate, (val: string) => { + this.formDate = val; + }, InputType.Number, '例如: 20260415') + + // 价格 + this.buildFormItemTextInput('价格', this.formPrice, (val: string) => { + this.formPrice = val; + }, InputType.Number, '例如: 4700') + + // 手数 + this.buildFormItemTextInput('手数', this.formCount, (val: string) => { + this.formCount = val; + }, InputType.Number, '') + + // 交易类型 Picker + this.buildFormItemPicker() + + // 按钮 + Row({ space: 12 }) { + Button('取消') + .layoutWeight(1) + .height(40) + .backgroundColor(Color.Gray) + .fontColor($r('app.color.elements_text_primary_02')) + .onClick(() => { + this.closeDialog(); + }) + + Button('保存') + .layoutWeight(1) + .height(40) + .backgroundColor('#4A90E2') + .fontColor('#FFFFFF') + .onClick(() => { + this.saveData(); + }) + } + .margin({ top: 20 }) + } + .width('90%') + .padding(20) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '5%', y: '25%' }) + } + + @Builder + buildFormItemTextInput( + label: string, + value: string, + onChange: (val: string) => void, + inputType: InputType = InputType.Normal, + placeholder: string = '' + ): void { + Row() { + Text(label) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(80) + + TextInput({ text: value, placeholder: placeholder }) + .type(inputType) + .onChange((val: string) => { + onChange(val); + }) + .width(150) + } + .width('100%') + .margin({ bottom: 12 }) + } + + @Builder + buildFormItemPicker(): void { + Row() { + Text('交易类型') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width(80) + + Column() { + Text(this.getTypeName(this.formType)) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width(150) + .height(36) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(6) + .borderWidth(1) + .borderColor($r('app.color.border_color_ffe0e0e0')) + .onClick(() => { + // 打开 Picker + this.showTypePicker = true; + }) + } + } + .width('100%') + .margin({ bottom: 12 }) + + // 类型选择弹窗 + if (this.showTypePicker) { + Column() + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .onClick(() => { + this.showTypePicker = false; + }) + + Column() { + Text('选择交易类型') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ bottom: 16 }) + + List() { + ForEach(this.typeOptions, (option: TradeTypeOption, index: number) => { + ListItem() { + Row() { + Text(option.name) + .fontSize(14) + .fontColor(this.formType === option.value + ? Color.White + : $r('app.color.elements_text_primary_02')) + .fontWeight(this.formType === option.value ? FontWeight.Medium : FontWeight.Normal) + + if (this.formType === option.value) { + Text('✓') + .fontSize(14) + .fontColor(Color.White) + .margin({ left: 8 }) + } + } + .width('100%') + .padding({ top: 12, bottom: 12 }) + .justifyContent(FlexAlign.Center) + .backgroundColor(this.formType === option.value + ? $r('app.color.blue_color_5370C6') + : Color.Transparent) + .borderRadius(8) + } + .onClick(() => { + this.formType = option.value; + this.selectedTypeIndex = index; + this.showTypePicker = false; + }) + }, (option: TradeTypeOption) => option.value.toString()) + } + .width('80%') + .height(280) + } + .width('70%') + .padding(16) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(16) + .position({ x: '15%', y: '30%' }) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/LeakMonitorPage.ets b/future_entry_demo/src/main/ets/pages/debug/LeakMonitorPage.ets new file mode 100644 index 0000000..53d3a4d --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/LeakMonitorPage.ets @@ -0,0 +1,25 @@ +import { buildLeakMonitorPage, LeakWatcherConfig } from '@kernel/ohos_leak_watcher'; +import { StatusBar } from '../../widget/StatusBar'; + +@Builder +export function LeakMonitorPageBuilder() { + LeakMonitorPage() +} + +@Entry +@Component +struct LeakMonitorPage { + + aboutToAppear(): void { + + } + + build() { + Column() { + StatusBar() + buildLeakMonitorPage() + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/TradeCloudStorageDebugPage.ets b/future_entry_demo/src/main/ets/pages/debug/TradeCloudStorageDebugPage.ets new file mode 100644 index 0000000..7f4af24 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/TradeCloudStorageDebugPage.ets @@ -0,0 +1,684 @@ +import { router } from '@kit.ArkUI' +import { StatusBar } from '../../widget/StatusBar' +import { TradeSettingManager } from 'biz_trade/src/main/ets/setting/TradeSettingManager' +import { emitter } from '@kit.BasicServicesKit' +import { TradeSettingEvents } from 'biz_trade' +import { AppUtil } from '@pura/harmony-utils' + +/** + * 云存储 Key-Value 数据项结构 + * 用于在列表中展示每个配置项 + */ +interface KeyValueItem { + /** 唯一标识符,由 key 和 value 生成 */ + id: string + /** 配置项的键名 */ + key: string + /** 配置项的值(JSON字符串形式) */ + value: string +} + +/** + * 交易云存储调试页面 + * 用于开发和调试交易设置的云端存储功能 + * 功能: + * - 查看云端存储的所有配置项(Key-Value) + * - 新增/修改/删除配置项 + * - 从云端刷新最新数据 + * - 直接编辑整个JSON + * + * 注意:仅用于调试环境,生产环境不应暴露此页面 + */ +@Entry +@Component +struct TradeCloudStorageDebugPage { + @State jsonString: string = '' + @State dataList: KeyValueItem[] = [] + @State selectedKey: string = '' + @State editValue: string = '' + @State newKey: string = '' + @State newValue: string = '' + @State editJsonContent: string = '' + @State expandedKeys: string[] = [] + @State showKeyPicker: boolean = false + @State showAddKeyDialog: boolean = false + @State showEditJsonDialog: boolean = false + // 自定义 Toast + @State toastMessage: string = '' + @State showToast: boolean = false + + /** + * 交易云存储更新事件处理器 + * 当云端设置发生变更时触发,重新加载数据并关闭所有弹窗 + */ + private onSettingUpdate = async () => { + this.showCustomToast(AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_cloud_upload_version_local_less_than_cloud'))) + this.showKeyPicker = false + this.showAddKeyDialog = false + this.showEditJsonDialog = false + await this.loadData() + } + + /** + * 页面即将显示时触发 + * 订阅云存储更新事件,并加载初始数据 + */ + aboutToAppear(): void { + emitter.on({ eventId: TradeSettingEvents.SETTING_UPDATE }, this.onSettingUpdate) + this.loadData() + } + + /** + * 页面即将销毁时触发 + * 取消订阅云存储更新事件,防止内存泄漏 + */ + aboutToDisappear() { + emitter.off(TradeSettingEvents.SETTING_UPDATE, this.onSettingUpdate) + } + + /** + * 从云端加载交易设置数据 + * 获取原始JSON字符串并解析为Key-Value列表用于展示 + */ + private async loadData(): Promise { + // 先清空,触发 UI 更新 + this.dataList = [] + this.expandedKeys = [] + + this.jsonString = await TradeSettingManager.getInstance().getRawJsonString() + + try { + const parsed: Record = JSON.parse(this.jsonString) as Record + // 按首字母排序 + const keys = Object.keys(parsed).sort() + const items: KeyValueItem[] = [] + keys.forEach((key: string) => { + const value: Object = parsed[key] + const valueStr: string = typeof value === 'object' ? JSON.stringify(value) : String(value) + // 使用 key+value 生成唯一 id,确保值变化时触发重建 + const id: string = key + '_' + valueStr.length + '_' + valueStr.substring(0, 10) + const item: KeyValueItem = { id: id, key: key, value: valueStr } + items.push(item) + }) + this.dataList = items + } catch (err) { + const error = err as Error + this.showCustomToast('数据加载失败: ' + error.message) + } + } + + /** + * 切换指定Key的展开/收起状态 + * @param key 要切换的键名 + */ + private toggleExpand(key: string): void { + const index = this.expandedKeys.indexOf(key) + if (index >= 0) { + // 存在则删除 + this.expandedKeys = this.expandedKeys.slice(0, index).concat(this.expandedKeys.slice(index + 1)) + } else { + // 不存在则添加 + this.expandedKeys = this.expandedKeys.concat([key]) + } + } + + /** + * 显示自定义Toast提示 + * @param message 提示消息内容 + * @param duration 显示时长(毫秒),默认1500ms + */ + // 显示自定义 Toast(适用于 SubWindow) + private showCustomToast(message: string, duration: number = 1500): void { + this.toastMessage = message + this.showToast = true + setTimeout(() => { + this.showToast = false + }, duration) + } + + /** + * 构建页面顶部标题栏 + * 包含返回按钮、页面标题和刷新按钮 + */ + @Builder + buildHeader() { + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor($r('app.color.elements_text_on_color')) + .fontWeight(FontWeight.Bold) + } + .backgroundColor($r('app.color.elements_inverse_quaternary')) + .width(36) + .height(36) + .onClick(() => { + router.back() + }) + + Text('交易云存储设置') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor($r('app.color.elements_text_on_color')) + .layoutWeight(1) + .margin({ left: 12 }) + + Text('刷新') + .fontSize(14) + .fontColor($r('app.color.elements_text_on_color')) + .onClick(() => { + this.showCustomToast('正在从云端下载...') + TradeSettingManager.getInstance().downloadDataFromCloudStore(async () => { + await this.loadData() + // 延时确保 Toast 显示 + setTimeout(() => { + this.showCustomToast('刷新成功') + }, 100) + }, (errorMsg: string) => { + this.showCustomToast('刷新失败: ' + errorMsg) + }) + }) + } + .width('100%') + .height(56) + .padding({ left: 12, right: 16 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + } + + /** + * 构建数据列表展示区域 + * 显示所有Key-Value配置项,支持空状态提示 + */ + @Builder + buildDataList() { + Column() { + if (this.dataList.length === 0) { + Text('暂无数据') + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .width('100%') + .textAlign(TextAlign.Center) + .padding(20) + } else { + List() { + ForEach(this.dataList, (item: KeyValueItem, index: number) => { + ListItem() { + this.buildDataItem(item) + } + }, (item: KeyValueItem) => item.id) + } + .width('100%') + .height('100%') + .cachedCount(0) + .scrollBar(BarState.Auto) + } + } + .width('100%') + .layoutWeight(1) + } + + /** + * 构建单个数据项的UI + * 展示Key和Value,支持展开查看完整Value,提供修改和删除操作 + * @param item 要展示的数据项 + */ + @Builder + buildDataItem(item: KeyValueItem) { + Column() { + Row() { + Column() { + Text(item.key) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.elements_text_primary_02')) + + Text(this.getDisplayValue(item.value)) + .fontSize(12) + .fontColor($r('app.color.elements_text_secondary')) + .maxLines(this.expandedKeys.indexOf(item.key) >= 0 ? 100 : 1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Row({ space: 8 }) { + Text(this.expandedKeys.indexOf(item.key) >= 0 ? '收起' : '展开') + .fontSize(12) + .fontColor($r('app.color.elements_text_link')) + + Text('修改') + .fontSize(12) + .fontColor($r('app.color.elements_text_link')) + } + .onClick(() => { + this.toggleExpand(item.key) + }) + } + .width('100%') + .padding(12) + .backgroundColor($r('app.color.surface_layer3_background')) + .borderRadius(8) + .margin({ bottom: 8 }) + .onClick(() => { + this.toggleExpand(item.key) + }) + + if (this.expandedKeys.indexOf(item.key) >= 0) { + Row() { + Button('修改此Key') + .fontSize(12) + .layoutWeight(1) + .margin({ right: 8 }) + .onClick(() => { + this.selectedKey = item.key + this.editValue = item.value + }) + + Button('删除') + .fontSize(12) + .layoutWeight(1) + .backgroundColor($r('app.color.status_error_01')) + .onClick(() => { + this.deleteKey(item.key) + }) + } + .width('100%') + .padding({ left: 12, right: 12, bottom: 12 }) + } + } + .width('100%') + } + + /** + * 获取用于展示的值(截断过长内容) + * @param value 原始值 + * @return 截断后的值,超过50字符会添加"..." + */ + private getDisplayValue(value: string): string { + if (value.length > 50) { + return value.substring(0, 50) + '...' + } + return value + } + + /** + * 删除指定的配置项 + * 从JSON中移除指定Key,然后上传到云端 + * @param key 要删除的键名 + */ + private deleteKey(key: string): void { + try { + const parsed: Record = JSON.parse(this.jsonString) as Record + // ArkTS 不支持 delete 操作符,需要创建新对象排除要删除的键 + const newParsed: Record = {} + Object.keys(parsed).forEach((k: string) => { + if (k !== key) { + newParsed[k] = parsed[k] + } + }) + const newJson = JSON.stringify(newParsed) + + TradeSettingManager.getInstance().uploadJsonString(newJson, async () => { + this.showCustomToast('删除成功') + await this.loadData() + }, (errorMsg: string) => { + this.showCustomToast('删除失败: ' + errorMsg) + }) + } catch (err) { + const error = err as Error + this.showCustomToast('操作失败: ' + error.message) + } + } + + /** + * 构建底部操作按钮区域 + * 包含"新增Key"和"修改整个JSON"两个按钮 + */ + @Builder + buildActionButtons() { + Row() { + Button('新增Key') + .fontSize(14) + .layoutWeight(1) + .margin({ right: 8 }) + .onClick(() => { + this.newKey = '' + this.newValue = '' + this.showAddKeyDialog = true + }) + + Button('修改整个JSON') + .fontSize(14) + .layoutWeight(1) + .onClick(() => { + // 格式化JSON显示 + try { + const parsed: Record = JSON.parse(this.jsonString) as Record + this.editJsonContent = JSON.stringify(parsed, null, 2) + } catch { + this.editJsonContent = this.jsonString + } + this.showEditJsonDialog = true + }) + } + .width('100%') + .height(44) + .margin({ top: 16 }) + } + + /** + * 构建修改Key的弹窗 + * 显示当前Key和Value,提供编辑和保存功能 + */ + @Builder + buildEditKeyDialog() { + Column() { + Text('修改 Key: ' + this.selectedKey) + .fontSize(16) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + Text('当前值:') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 8 }) + + TextInput({ text: this.editValue }) + .fontSize(14) + .onChange((value: string) => { + this.editValue = value + }) + + Row() { + Button('取消') + .layoutWeight(1) + .margin({ right: 8 }) + .onClick(() => { + this.selectedKey = '' + }) + + Button('保存并上传') + .layoutWeight(1) + .onClick(() => { + this.saveKeyAndUpload() + }) + } + .width('100%') + .margin({ top: 24 }) + } + .width('100%') + .padding(24) + .backgroundColor($r('app.color.surface_layer3_background')) + .borderRadius(16) + } + + /** + * 构建新增Key的弹窗 + * 提供Key和Value输入框,用于添加新的配置项 + */ + @Builder + buildAddKeyDialog() { + Column() { + Text('新增 Key-Value') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + Text('Key:') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ bottom: 8 }) + + TextInput({ placeholder: '请输入 Key' }) + .fontSize(14) + .onChange((value: string) => { + this.newKey = value + }) + + Text('Value:') + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .margin({ top: 16, bottom: 8 }) + + TextInput({ placeholder: '请输入 Value' }) + .fontSize(14) + .onChange((value: string) => { + this.newValue = value + }) + + Row() { + Button('取消') + .layoutWeight(1) + .margin({ right: 8 }) + .onClick(() => { + this.showAddKeyDialog = false + }) + + Button('保存并上传') + .layoutWeight(1) + .onClick(() => { + this.addKeyAndUpload() + }) + } + .width('100%') + .margin({ top: 24 }) + } + .width('100%') + .padding(24) + .backgroundColor($r('app.color.surface_layer3_background')) + .borderRadius(16) + } + + /** + * 构建编辑整个JSON的弹窗 + * 提供TextArea用于直接编辑完整的JSON字符串 + */ + @Builder + buildEditJsonDialog() { + Column() { + Text('修改整个 JSON') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + TextArea({ text: this.editJsonContent }) + .fontSize(12) + .height(300) + .padding(8) + .backgroundColor($r('app.color.surface_layer1_background')) + .onChange((value: string) => { + this.editJsonContent = value + }) + + Row() { + Button('取消') + .layoutWeight(1) + .margin({ right: 8 }) + .onClick(() => { + this.showEditJsonDialog = false + }) + + Button('保存并上传') + .layoutWeight(1) + .onClick(() => { + this.saveJsonAndUpload() + }) + } + .width('100%') + .margin({ top: 24 }) + } + .width('100%') + .padding(24) + .backgroundColor($r('app.color.surface_layer3_background')) + .borderRadius(16) + } + + build() { + Stack() { + Column() { + StatusBar() + + this.buildHeader() + + Column() { + this.buildDataList() + + this.buildActionButtons() + } + .width('100%') + .layoutWeight(1) + .padding(16) + + // Edit key dialog + if (this.selectedKey.length > 0) { + Column() { + Blank() + .backgroundColor($r('app.color.mask_level2')) + .onClick(() => { + this.selectedKey = '' + }) + this.buildEditKeyDialog() + } + .width('100%') + .height('100%') + .position({ x: 0, y: 0 }) + } + + // Add key dialog + if (this.showAddKeyDialog) { + Column() { + Blank() + .backgroundColor($r('app.color.mask_level2')) + .onClick(() => { + this.showAddKeyDialog = false + }) + this.buildAddKeyDialog() + } + .width('100%') + .height('100%') + .position({ x: 0, y: 0 }) + } + + // Edit JSON dialog + if (this.showEditJsonDialog) { + Column() { + Blank() + .backgroundColor($r('app.color.mask_level2')) + .onClick(() => { + this.showEditJsonDialog = false + }) + this.buildEditJsonDialog() + } + .width('100%') + .height('100%') + .position({ x: 0, y: 0 }) + } + + // 自定义 Toast(适用于 SubWindow) + if (this.showToast) { + Text(this.toastMessage) + .fontSize(14) + .fontColor(Color.White) + .padding({ + left: 16, + right: 16, + top: 10, + bottom: 10 + }) + .backgroundColor($r('app.color.mask_level1')) + .borderRadius(8) + .position({ x: '50%', y: 80 }) + .translate({ x: '-50%' }) + } + } + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_background')) + } + + /** + * 保存修改后的Key并上传到云端 + * 修改指定Key的Value,然后同步到云存储 + */ + private saveKeyAndUpload(): void { + if (!this.selectedKey || !this.editValue) { + this.showCustomToast('Key 或 Value 不能为空') + return + } + + try { + const parsed: Record = JSON.parse(this.jsonString) as Record + parsed[this.selectedKey] = this.editValue + const newJson = JSON.stringify(parsed) + + TradeSettingManager.getInstance().uploadJsonString(newJson, async () => { + this.showCustomToast('上传成功') + await this.loadData() + this.selectedKey = '' + }, (errorMsg: string) => { + this.showCustomToast('上传失败: ' + errorMsg) + }) + } catch (err) { + const error = err as Error + this.showCustomToast('JSON 处理失败: ' + error.message) + } + } + + /** + * 新增Key-Value并上传到云端 + * 验证输入有效性,添加新配置项并同步到云存储 + */ + private addKeyAndUpload(): void { + if (!this.newKey || !this.newValue) { + this.showCustomToast('Key 或 Value 不能为空') + return + } + + try { + const parsed: Record = JSON.parse(this.jsonString) as Record + parsed[this.newKey] = this.newValue + const newJson = JSON.stringify(parsed) + + TradeSettingManager.getInstance().uploadJsonString(newJson, async () => { + this.showCustomToast('新增成功并上传') + await this.loadData() + this.showAddKeyDialog = false + }, (errorMsg: string) => { + this.showCustomToast('上传失败: ' + errorMsg) + }) + } catch (err) { + const error = err as Error + this.showCustomToast('JSON 处理失败: ' + error.message) + } + } + + /** + * 保存编辑后的整个JSON并上传到云端 + * 验证JSON格式有效性,然后同步到云存储 + */ + private saveJsonAndUpload(): void { + if (!this.editJsonContent) { + this.showCustomToast('JSON 内容不能为空') + return + } + + try { + JSON.parse(this.editJsonContent) + + TradeSettingManager.getInstance().uploadJsonString(this.editJsonContent, async () => { + this.showCustomToast('上传成功') + await this.loadData() + this.showEditJsonDialog = false + }, (errorMsg: string) => { + this.showCustomToast('上传失败: ' + errorMsg) + }) + } catch (err) { + const error = err as Error + this.showCustomToast('JSON 格式无效: ' + error.message) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/debug/TradeDebugPage.ets b/future_entry_demo/src/main/ets/pages/debug/TradeDebugPage.ets new file mode 100644 index 0000000..a31ae35 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/debug/TradeDebugPage.ets @@ -0,0 +1,735 @@ +import { StatusBar } from '../../widget/StatusBar' +import { promptAction, router } from '@kit.ArkUI' +import { GlobalContext, PreferenceService } from 'biz_common' +import { TradeOverWarningManager } from 'biz_trade/src/main/ets/pages/trademanager/riskControl/TradeOverWarningManager' +import { + TradeDebugManager, + TradeHighFrequentManager, + TradeDebugFeatureStatus, + TradeSettlementBillManager +} from 'biz_trade' +import { ToastUtil } from '@pura/harmony-utils' +import { AccountProvider } from 'biz_trade/src/main/ets/features/account/models/AccountProvider' +import { requestConditionUnbind } from 'biz_tradecondition/src/main/ets/network/ConditionHttpApi' +import { MoniFutureAccountManager } from 'biz_trade/src/main/ets/moni/manager/MoniFutureAccountManager' +import { TradeDateCenter } from '@b2c-f/futures_ohos_trade_sdk' + +@CustomDialog +struct TradeDateDialog { + controller: CustomDialogController + tradeDate: string = '' + currentTradeDate: string = '' + onConfirm?: (tradeDate: string) => boolean + + build() { + Column() { + Row() { + Text('修改当前交易日(注:重新登录后会被覆盖)') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor('#1A1A1A') + .layoutWeight(1) + + Text('✕') + .fontSize(22) + .fontColor('#999999') + .onClick(() => { + this.controller.close() + }) + } + .width('100%') + .margin({ bottom: 16 }) + + Column({ space: 10 }) { + Text('当前交易日') + .fontSize(12) + .fontColor('#666666') + .width('100%') + + Text(this.currentTradeDate.length > 0 ? this.currentTradeDate : '--') + .fontSize(16) + .fontWeight(FontWeight.Medium) + .fontColor('#1A1A1A') + .width('100%') + + TextInput({ placeholder: '请输入8位交易日 yyyyMMdd', text: this.tradeDate }) + .fontSize(14) + .height(44) + .padding({ left: 12 }) + .backgroundColor('#F5F7FA') + .borderRadius(8) + .type(InputType.Number) + .onChange((value: string) => { + this.tradeDate = value + }) + } + .width('100%') + + Row({ space: 12 }) { + Button('取消') + .fontSize(14) + .layoutWeight(1) + .height(44) + .fontColor('#666666') + .backgroundColor('#F5F5F5') + .borderRadius(22) + .onClick(() => { + this.controller.close() + }) + + Button('确认') + .fontSize(14) + .layoutWeight(1) + .height(44) + .fontColor(Color.White) + .backgroundColor('#4A90E2') + .borderRadius(22) + .onClick(() => { + if (this.onConfirm) { + const isSuccess = this.onConfirm(this.tradeDate.trim()) + if (isSuccess) { + this.controller.close() + } + } + }) + } + .margin({ top: 16 }) + } + .padding(20) + .backgroundColor(Color.White) + .borderRadius(16) + } +} + +/** + * 单行配置项 + */ +@Component +struct SettingRow { + @Require title: string + @Prop description?: string + handleClick: () => void = () => {} + + build() { + Row() { + Column() { + Text(this.title) + .fontSize(14) + .fontColor('#1A1A1A') + + if (this.description) { + Text(this.description) + .fontSize(11) + .fontColor('#999999') + .margin({ top: 2 }) + } + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('执行') + .fontSize(12) + .padding({ left: 12, right: 12, top: 5, bottom: 5 }) + .fontColor('#4A90E2') + .backgroundColor('#E8F4FD') + .borderRadius(12) + } + .width('100%') + .padding(12) + .backgroundColor(Color.White) + .borderRadius(10) + .onClick(() => { + this.handleClick() + }) + } +} + +/** + * 开关配置项 + */ +@Component +struct SettingSwitchRow { + @Require title: string + @Prop description?: string + @Link isOn: boolean + onChange: (isOn: boolean) => void = () => {} + + build() { + Row() { + Column() { + Text(this.title) + .fontSize(14) + .fontColor('#1A1A1A') + + if (this.description) { + Text(this.description) + .fontSize(11) + .fontColor('#999999') + .margin({ top: 2 }) + } + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Toggle({ type: ToggleType.Switch, isOn: this.isOn }) + .selectedColor('#4A90E2') + .onChange((value: boolean) => { + this.isOn = value + this.onChange(value) + }) + } + .width('100%') + .padding(12) + .backgroundColor(Color.White) + .borderRadius(10) + } +} + +/** + * 导航配置项 + */ +@Component +struct SettingNavRow { + @Require title: string + @Prop description?: string + handleClick: () => void = () => {} + + build() { + Row() { + Column() { + Text(this.title) + .fontSize(14) + .fontColor('#1A1A1A') + + if (this.description) { + Text(this.description) + .fontSize(11) + .fontColor('#999999') + .margin({ top: 2 }) + } + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('>') + .fontSize(18) + .fontColor('#CCCCCC') + } + .width('100%') + .padding(12) + .backgroundColor(Color.White) + .borderRadius(10) + .onClick(() => { + this.handleClick() + }) + } +} + +/** + * 三按钮选择项 + */ +@Component +struct SettingTriStateRow { + @Require title: string + @Link currentState: TradeDebugFeatureStatus + onUnset: () => void = () => {} + onEnable: () => void = () => {} + onDisable: () => void = () => {} + + build() { + Column() { + Text(this.title) + .fontSize(14) + .fontColor('#1A1A1A') + .margin({ bottom: 10 }) + + Row() { + Button('未设置') + .fontSize(12) + .height(36) + .layoutWeight(1) + .fontColor(this.currentState === TradeDebugFeatureStatus.UNSET ? '#FFFFFF' : '#666666') + .backgroundColor(this.currentState === TradeDebugFeatureStatus.UNSET ? '#4A90E2' : '#F5F5F5') + .borderRadius(8) + .onClick(() => { + this.currentState = TradeDebugFeatureStatus.UNSET + this.onUnset() + }) + + Button('开启') + .fontSize(12) + .height(36) + .layoutWeight(1) + .margin({ left: 8, right: 8 }) + .fontColor(this.currentState === TradeDebugFeatureStatus.ENABLE ? '#FFFFFF' : '#666666') + .backgroundColor(this.currentState === TradeDebugFeatureStatus.ENABLE ? '#4CAF50' : '#F5F5F5') + .borderRadius(8) + .onClick(() => { + this.currentState = TradeDebugFeatureStatus.ENABLE + this.onEnable() + }) + + Button('关闭') + .fontSize(12) + .height(36) + .layoutWeight(1) + .fontColor(this.currentState === TradeDebugFeatureStatus.DISABLE ? '#FFFFFF' : '#666666') + .backgroundColor(this.currentState === TradeDebugFeatureStatus.DISABLE ? '#F44336' : '#F5F5F5') + .borderRadius(8) + .onClick(() => { + this.currentState = TradeDebugFeatureStatus.DISABLE + this.onDisable() + }) + } + } + .width('100%') + .padding(12) + .backgroundColor(Color.White) + .borderRadius(10) + } +} + +@Entry +@Component +struct TradeDebugPage { + @State settlementConfirmExitPass: TradeDebugFeatureStatus = TradeDebugFeatureStatus.UNSET + @State isInnerSimAccount: boolean = false + @State skipConditionBindingQueryOnEmulator: boolean = false + @State currentTradeDate: string = '' + @State tradeDateInput: string = '' + private tradeDateDialogController: CustomDialogController | null = null + + aboutToAppear(): void { + this.settlementConfirmExitPass = TradeDebugManager.getInstance().getSettlementConfirmExitPass() + this.isInnerSimAccount = MoniFutureAccountManager.getInstance().isInnerSimAccountEnabled() + this.skipConditionBindingQueryOnEmulator = TradeDebugManager.getInstance().getConditionBindingQuerySkipOnEmulator() + this.refreshCurrentTradeDate() + } + + private getBottomSafeAreaHeight(): number { + return px2vp(GlobalContext.navigationBarHeight) + } + + private refreshCurrentTradeDate(): void { + const currentAccount = AccountProvider.getInstance().getCurrentAccount() + if (!currentAccount) { + this.currentTradeDate = '' + this.tradeDateInput = '' + return + } + this.currentTradeDate = TradeDateCenter.getInstance().getLoginTradingDate(currentAccount) ?? '' + this.tradeDateInput = this.currentTradeDate + } + + private isValidTradeDate(tradeDate: string): boolean { + return /^\d{8}$/.test(tradeDate) + } + + private openTradeDateDialog(): void { + const currentAccount = AccountProvider.getInstance().getCurrentAccount() + if (!currentAccount) { + ToastUtil.showToast('当前无有效交易账号,无法修改交易日') + return + } + this.refreshCurrentTradeDate() + this.tradeDateInput = this.currentTradeDate + this.tradeDateDialogController = new CustomDialogController({ + builder: TradeDateDialog({ + tradeDate: this.tradeDateInput, + currentTradeDate: this.currentTradeDate, + onConfirm: (tradeDate: string) => { + return this.handleUpdateTradeDate(tradeDate) + } + }), + alignment: DialogAlignment.Center, + customStyle: true + }) + this.tradeDateDialogController.open() + } + + private handleUpdateTradeDate(tradeDate: string): boolean { + const currentAccount = AccountProvider.getInstance().getCurrentAccount() + if (!currentAccount) { + ToastUtil.showToast('当前无有效交易账号,无法修改交易日') + return false + } + if (!this.isValidTradeDate(tradeDate)) { + ToastUtil.showToast('请输入8位交易日,格式为yyyyMMdd') + return false + } + AccountProvider.getInstance().setCurrentTradeDate(tradeDate) + TradeDateCenter.getInstance().cacheLoginTradingDate(currentAccount, tradeDate) + this.currentTradeDate = tradeDate + this.tradeDateInput = tradeDate + ToastUtil.showToast(`交易日已更新为 ${tradeDate}`) + return true + } + + private handleConditionUnbind() { + const currentAccount = AccountProvider.getInstance().getCurrentAccount() + if (!currentAccount || !currentAccount.account || !currentAccount.qsId || !currentAccount.wtId) { + ToastUtil.showToast('当前无有效交易账号,无法解绑') + return + } + requestConditionUnbind( + { + tradeAccountId: currentAccount.account, + qsid: currentAccount.qsId, + wtid: currentAccount.wtId + }, + () => { + ToastUtil.showToast('条件单解绑成功') + }, + (errMsg: string) => { + ToastUtil.showToast(`条件单解绑失败: ${errMsg}`) + } + ) + } + + // ==================== 模块 Builder ==================== + + @Builder + buildModuleHeader(icon: string, title: string) { + Text(icon + ' ' + title) + .fontSize(13) + .fontWeight(FontWeight.Bold) + .fontColor('#333333') + .width('100%') + .margin({ top: 16, bottom: 8 }) + .padding({ left: 4 }) + } + + @Builder + buildAccountSection() { + Column() { + this.buildModuleHeader('📱', '账号体系') + + Column({ space: 8 }) { + SettingSwitchRow({ + title: '内网模拟账号', + description: '开启后使用内网仿真账号 666000000003', + isOn: $isInnerSimAccount, + onChange: (value: boolean) => { + MoniFutureAccountManager.getInstance().setInnerSimAccountEnabled(value) + ToastUtil.showToast(value ? '已开启内网模拟账号' : '已关闭') + } + }) + + SettingNavRow({ + title: '营业部后门配置', + description: '配置测试用营业部IP和端口', + handleClick: () => { + router.pushUrl({ url: 'pages/debug/DebugYYBConfigPage' }) + } + }) + + SettingSwitchRow({ + title: '跳过查询条件单签署绑定(仅模拟器支持)', + isOn: $skipConditionBindingQueryOnEmulator, + onChange: (value: boolean) => { + TradeDebugManager.getInstance().setConditionBindingQuerySkipOnEmulator(value) + ToastUtil.showToast(value ? '已开启跳过查询' : '已关闭跳过查询') + } + }) + + SettingRow({ + title: '条件单解绑(当前账号)', + description: '解除当前账号的条件单绑定', + handleClick: () => { + this.handleConditionUnbind() + } + }) + + SettingRow({ + title: '修改当前交易日', + description: this.currentTradeDate.length > 0 ? `当前值:${this.currentTradeDate}` : '当前账号未设置交易日', + handleClick: () => { + this.openTradeDateDialog() + } + }) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + } + + @Builder + buildConditionSection() { + Column() { + this.buildModuleHeader('🧩', '条件单') + + Column({ space: 8 }) { + SettingSwitchRow({ + title: '跳过查询条件单签署绑定(仅模拟器支持)', + isOn: $skipConditionBindingQueryOnEmulator, + onChange: (value: boolean) => { + TradeDebugManager.getInstance().setConditionBindingQuerySkipOnEmulator(value) + ToastUtil.showToast(value ? '已开启跳过查询' : '已关闭跳过查询') + } + }) + + SettingRow({ + title: '条件单解绑(当前账号)', + description: '解除当前账号的条件单绑定', + handleClick: () => { + this.handleConditionUnbind() + } + }) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + } + + @Builder + buildSettlementSection() { + Column() { + this.buildModuleHeader('📋', '结算单') + + Column({ space: 8 }) { + SettingTriStateRow({ + title: '结算单确认退出跳过', + currentState: $settlementConfirmExitPass, + onUnset: () => { + TradeDebugManager.getInstance().setSettlementConfirmExitPass(TradeDebugFeatureStatus.UNSET) + }, + onEnable: () => { + TradeDebugManager.getInstance().setSettlementConfirmExitPass(TradeDebugFeatureStatus.ENABLE) + }, + onDisable: () => { + TradeDebugManager.getInstance().setSettlementConfirmExitPass(TradeDebugFeatureStatus.DISABLE) + } + }) + + SettingRow({ + title: '清除结算单确认标识(当前账号)', + handleClick: () => { + TradeSettlementBillManager.getInstance().clearConfirmTradeDate() + ToastUtil.showToast('清除成功') + } + }) + + SettingRow({ + title: '清除结算单确认标识(所有账号)', + handleClick: () => { + PreferenceService.getPreference('TradeSettlement').clearSync() + ToastUtil.showToast('清除成功') + } + }) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + } + + @Builder + buildWarningSection() { + Column() { + this.buildModuleHeader('⚠️', '交易预警') + + Column({ space: 8 }) { + SettingRow({ + title: '清除交易预警设置(所有账号)', + handleClick: () => { + TradeOverWarningManager.deleteAllSetting() + ToastUtil.showToast('清除成功') + } + }) + + SettingRow({ + title: '清除交易预警设置(当前账号)', + handleClick: () => { + TradeOverWarningManager.deleteCurrentAccountSetting() + ToastUtil.showToast('清除成功') + } + }) + + SettingRow({ + title: '清除报撤单统计(所有账号)', + handleClick: () => { + TradeHighFrequentManager.getInstance().deleteAllData() + ToastUtil.showToast('清除成功') + } + }) + + SettingRow({ + title: '清除报撤单统计(当前账号)', + handleClick: () => { + TradeHighFrequentManager.getInstance().deleteCurrentAccountData() + ToastUtil.showToast('清除成功') + } + }) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + } + + @Builder + buildOtherSection() { + Column() { + this.buildModuleHeader('🔧', '其他') + + Column({ space: 8 }) { + SettingNavRow({ + title: '交易云存储', + description: '查看/修改云存储设置', + handleClick: () => { + router.pushUrl({ url: 'pages/debug/TradeCloudStorageDebugPage' }) + } + }) + + SettingNavRow({ + title: '交易日志', + description: '查看交易日志', + handleClick: () => { + router.pushNamedRoute({ name: 'TradeDailyPage' }) + } + }) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + } + + @State clearCancelTime: string = TradeDebugManager.getInstance().getCancelTimeoutMs().toString() + + @Builder + buildClearCancelSetting() { + Column() { + this.buildModuleHeader('🧹', '一键清仓撤单') + + Column({ space: 8 }) { + Column() { + Text('撤单超时时间设置') + .fontSize(14) + .fontColor('#1A1A1A') + .textAlign(TextAlign.Start) + .width('100%') + + Row({ space: 8 }) { + TextInput({ placeholder: '请输入数字,单位:毫秒', text: this.clearCancelTime }) + .layoutWeight(1) + .height(40) + .padding({ left: 12, right: 12 }) + .borderRadius(8) + .backgroundColor('#F5F7FA') + .type(InputType.Number) + .onChange((value: string) => { + this.clearCancelTime = value + }) + + Button('确认') + .width(72) + .height(40) + .fontSize(13) + .fontColor(Color.White) + .backgroundColor('#4A90E2') + .borderRadius(8) + .onClick(() => { + TradeDebugManager.getInstance().setCancelTimeoutMs(parseInt(this.clearCancelTime)) + promptAction.showToast({ + message: '设置成功' + }) + }) + } + } + .padding(12) + .backgroundColor(Color.White) + .borderRadius(10) + } + .width('100%') + .padding({ left: 12, right: 12 }) + } + .width('100%') + } + + build() { + Column() { + StatusBar() + + // 导航栏 + Row() { + Button({ type: ButtonType.Circle }) { + Text('←') + .fontSize(22) + .fontColor('#FFFFFF') + .fontWeight(FontWeight.Bold) + } + .backgroundColor('rgba(255, 255, 255, 0.2)') + .width(36) + .height(36) + .onClick(() => { + router.back() + }) + + Text('交易Debug控制') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .layoutWeight(1) + .margin({ left: 12 }) + } + .width('100%') + .height(56) + .padding({ left: 12, right: 16 }) + .linearGradient({ + angle: 135, + colors: [[0x4A90E2, 0.0], [0x357ABD, 1.0]] + }) + .shadow({ + radius: 8, + color: 'rgba(0, 0, 0, 0.1)', + offsetY: 2 + }) + + // 配置列表 + List({ space: 12 }) { + ListItem() { + this.buildAccountSection() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + this.buildConditionSection() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + this.buildSettlementSection() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + this.buildWarningSection() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + this.buildClearCancelSetting() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + this.buildOtherSection() + } + .padding({ left: 12, right: 12 }) + + ListItem() { + Column().height(34) + } + } + .layoutWeight(1) + .width('100%') + .backgroundColor('#F5F7FA') + .scrollBar(BarState.Auto) + + // 底部安全区域占位 + Blank() + .height(this.getBottomSafeAreaHeight()) + } + .width('100%') + .height('100%') + .backgroundColor('#F5F7FA') + } +} diff --git a/future_entry_demo/src/main/ets/pages/future/ContractListPage.ets b/future_entry_demo/src/main/ets/pages/future/ContractListPage.ets new file mode 100644 index 0000000..031bdf6 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/future/ContractListPage.ets @@ -0,0 +1,86 @@ +import { router } from '@kit.ArkUI' +import { GlobalContext } from 'biz_common' +import { PageTitleBar } from 'biz_common/src/main/ets/component/PageTitleBar' +import { ContractList, FutureMarketDataManager } from 'biz_market' +import { FutureKeyMapGroup } from 'biz_market' +import { StatusBar } from '../../widget/StatusBar' + +@Entry +@Component +export struct ContractListPage { + private stockCode: string = '' + private varietyCode: string = '' + private marketString: string = '' + + @State groupData: FutureKeyMapGroup = {name:'', key:'', member: []} + @State isPageForeground: boolean = false + @State hasInit: boolean = false + + aboutToAppear(): void { + let params = router.getParams() as Record + this.stockCode = params['stockCode'] as string ?? '' + this.varietyCode = params['varietyCode'] as string ?? '' + this.marketString = params['marketString'] as string ?? '' + + let groupData = FutureMarketDataManager.get().getGroupDataByKeyCode(this.marketString) + if (groupData) { + this.groupData = groupData + } + this.hasInit = true + } + + onPageShow(): void { + this.isPageForeground = true + } + + onPageHide(): void { + this.isPageForeground = false + } + + build() { + Column() { + StatusBar() + if (this.hasInit) { + PageTitleBar({ + title: this.groupData.name, + titleRightBuilder: this.buildSearchButton + }) + + ContractList({ + onForeground: this.isPageForeground, + futureKeyMapGroup: this.groupData, + varietyCode: this.varietyCode + }) + } + } + .padding({bottom: GlobalContext.navigationBarHeightVp}) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .width('100%') + .layoutWeight(1) + } + + @Builder + buildSearchButton() { + Column() { + Image($r("app.media.icon_search_24")) + .width(24) + .height(24) + .fillColor($r('app.color.elements_icon_primary_02')) + } + .width(44) + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + router.pushUrl({ + url: 'pages/SearchPage' // 目标url + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }); + }) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/future/HQPage.ets b/future_entry_demo/src/main/ets/pages/future/HQPage.ets new file mode 100644 index 0000000..0c55b32 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/future/HQPage.ets @@ -0,0 +1,190 @@ +import { EmitterConstants, SelfCodePageTabManager } from 'biz_common' +import { InsideHQImpl, OuterHQImpl, ArbitrageHQImpl, HQContainerPage } from 'biz_market' +import { SelfCodePage } from '../selfcode/SelfCodePage' +import { router } from '@kit.ArkUI' +import { RealDataManager, RequestHelper } from '@kernel/lib_communication' +import { OverViewPage } from '../overview/OverViewPage' +import { OptionPage } from '../option/OptionPage' +import { HQPageConstants } from 'biz_common/src/main/ets/constants/HQPageConstants' +import { emitter } from '@kit.BasicServicesKit' + + +@Component +export struct HQPage { + private HQ_PAGE_TABS = ['总览', '自选', '国内', '国际', '期权', '套利'] + @ObjectLink tabManager: SelfCodePageTabManager + @Consume @Watch("onPageSubTabChange") hqPageSubTabIndex: number + @State @Watch('onTabIndexChange') currentSwiperIndex: number = this.hqPageSubTabIndex + @State isVisible: boolean = false + private swiperController = new SwiperController() + + onPageSubTabChange() { + this.currentSwiperIndex = this.hqPageSubTabIndex + } + + onTabIndexChange() { + this.hqPageSubTabIndex = this.currentSwiperIndex + emitter.emit({ eventId: EmitterConstants.TABLE_INDEX_CHANGE }) + this.swiperController.changeIndex(this.currentSwiperIndex) + } + + aboutToAppear(): void { + this.tabManager.registerVisibilityChangeListener((isVisible) => { + this.isVisible = isVisible + + if (!this.isVisible) { + //此处不能直接removeAll,需要移除client请求,而不是removeAll(),并不会停止实时数据并且会把其他实时数据无法接收 + //RealDataManager.getInstance().removeAll() + } + }) + } + + build() { + Flex({ direction: FlexDirection.Column }) { + this.buildHQPageTitle() + this.buildPageContent() + } + .layoutWeight(1) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildPageContent() { + Swiper(this.swiperController) { + OverViewPage({ + onForeground: this.isVisible && this.currentSwiperIndex === 0 + }) + //自选股 + SelfCodePage({ + onForeground: this.isVisible && this.currentSwiperIndex === 1 + }) + //国内行情 + HQContainerPage({ + onForeground: this.isVisible && this.currentSwiperIndex === 2, + businessDelegate: new InsideHQImpl() + }) + //国外行情 + HQContainerPage({ + onForeground: this.isVisible && this.currentSwiperIndex === 3, + businessDelegate: new OuterHQImpl() + }) + //期权 + OptionPage({ + onForeground: this.isVisible && this.currentSwiperIndex === 4 + }) + //套利行情 + HQContainerPage({ + onForeground: this.isVisible && this.currentSwiperIndex === 5, + businessDelegate: new ArbitrageHQImpl() + }) + } + .index(this.currentSwiperIndex) + .layoutWeight(1) + .width('100%') + .indicator(false) + .disableSwipe(true) + } + + @Builder + private buildHQPageTitle() { + Row({ space: 20 }) { + Stack({ alignContent: Alignment.TopStart }) { + Scroll() { + Row({ space: 12 }) { + ForEach(this.HQ_PAGE_TABS, (item: string, index: number) => { + this.buildSingleTabTitle(index) + }) + } + .alignItems(VerticalAlign.Bottom) + .height('100%') + } + .height(22) + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .align(Alignment.Start) + .layoutWeight(1) + + Row() { + Column() + .width(12) + .height(22) + .linearGradient({ + direction: GradientDirection.Right, + colors: [[$r('app.color.surface_layer1_foreground_0'), 0], [$r('app.color.surface_layer1_foreground'), 1]] + }) + } + .width('100%') + .justifyContent(FlexAlign.End) + .hitTestBehavior(HitTestMode.Transparent) + + } + .height(22) + .layoutWeight(1) + + Row() { + Image($r("app.media.icon_search_24")) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(24) + .height(24) + .margin({ right: 16 }) + .onClick(() => { + router.pushUrl({ + url: 'pages/SearchPage' + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + }); + }) + + Image($r("app.media.icon_setting_24")) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(24) + .height(24) + .onClick(() => { + router.pushUrl({ + url: 'pages/setting/HQTableSettingPage' + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + }); + }) + } + .height(24) + .justifyContent(FlexAlign.End) + .alignSelf(ItemAlign.Center) + .alignItems(VerticalAlign.Center) + } + .alignItems(VerticalAlign.Center) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .alignItems(VerticalAlign.Bottom) + .justifyContent(FlexAlign.Start) + .height(44) + .width('100%') + .padding({ left: 16, right: 16, top: 8, bottom: 8 }) + } + + @Builder + private buildSingleTabTitle(index: number) { + if (this.currentSwiperIndex === index) { + Text(this.HQ_PAGE_TABS[index]) + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(22) + .height(22) + .fontWeight(500) + } else { + Text(this.HQ_PAGE_TABS[index]) + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(16) + .height(16) + .fontWeight(400) + .onClick(() => { + //RealDataManager.getInstance().removeAll() + this.currentSwiperIndex = index + }) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/future/MarketHeatPage.ets b/future_entry_demo/src/main/ets/pages/future/MarketHeatPage.ets new file mode 100644 index 0000000..1ede985 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/future/MarketHeatPage.ets @@ -0,0 +1,2329 @@ +import { CellData, HeaderItem, OriginalData, RowData, SortStatus, buildHeaderRowData } from '@b2b/hq_table'; +import { ColorUtils, TableConstants } from '@b2c/lib_baseui'; +import { StuffBaseStruct, StuffCurveStruct } from '@kernel/lib_communication'; +import { ColourModeStatus } from '@kernel/theme_manager'; +import { EmitterConstants, GlobalContext, HQPageConstants, IEventData, ProtocolDef, RequestClient, + TabManagerConstants, Markets, ZS } from 'biz_common'; +import { PageTitleBar } from 'biz_common/src/main/ets/component/PageTitleBar'; +import { HQTableComponent, TOP_TAB_BAR_HEIGHT, TopTabBarItem } from 'biz_market'; +import { router } from '@kit.ArkUI'; +import { emitter } from '@kit.BasicServicesKit'; +import promptAction from '@ohos.promptAction'; +import { GetCodeList, GetMarketList } from 'service_watchlist'; +import { StatusBar } from '../../widget/StatusBar'; + +const MARKET_HEAT_FRAME_ID: number = 2381; +const MARKET_HEAT_CURVE_PAGE_ID: number = 1229; +const MARKET_HEAT_INDEX_PAGE_ID: number = 4083; +const MARKET_HEAT_ALL_PAGE_ID: number = 4068; +const MARKET_HEAT_SELF_PAGE_ID: number = 1264; +const MARKET_HEAT_DEFAULT_SORT_ID: number = 68; +const MARKET_HEAT_CURVE_DATA_ID: number = 68; +const MARKET_HEAT_DEFAULT_CODE: string = '850001'; +const MARKET_HEAT_DEFAULT_MARKET: string = '64'; +const MARKET_HEAT_DEFAULT_NAME: string = '同花顺商品'; +const MARKET_HEAT_COLUMN_ORDER: string = '55|68|67|10|34818|34821|4|126|34338|36103'; +const MARKET_HEAT_CURVE_EMPTY_TEXT: string = '--'; +const MARKET_HEAT_PRICE_DATA_ID: number = 10; +const MARKET_HEAT_TIME_DATA_ID: number = 1; +const MARKET_HEAT_PRE_CLOSE_DATA_ID: number = ProtocolDef.SM_HQ_PREVCLOSE; +const MARKET_HEAT_HIGH_PRICE_DATA_ID: number = ProtocolDef.SM_HQ_HIGHPRICE; +const MARKET_HEAT_LOW_PRICE_DATA_ID: number = ProtocolDef.SM_HQ_LOWPRICE; +const MARKET_HEAT_DECIMAL_EX_ID: number = 0x8005; +const MARKET_HEAT_MARKET_TIME_EX_ID: number = ProtocolDef.EX_HQ_MARKETTIME; +const MARKET_HEAT_DATA_ID_EX_ID: number = 0x8004; +const MARKET_HEAT_STOCK_CODE_EX_ID: number = TableConstants.DATA_ID_CODE_4; +const MARKET_HEAT_STOCK_MARKET_EX_ID: number = TableConstants.DATA_ID_MARKET; +const MARKET_HEAT_TIME_AXIS_HEIGHT: number = 15; +const MARKET_HEAT_TIME_AXIS_GAP: number = 1; +const MARKET_HEAT_CHART_BORDER_WIDTH: number = 0.5; +const MARKET_HEAT_CHART_HEIGHT: number = 156; +const MARKET_HEAT_PLOT_HEIGHT: number = 140; +const MARKET_HEAT_AXIS_LABEL_WIDTH: number = 100; +const MARKET_HEAT_AXIS_LABEL_HEIGHT: number = 14; +const MARKET_HEAT_TIME_LABEL_WIDTH: number = 80; +const MARKET_HEAT_LEGEND_NAME_WIDTH: number = 125; +const MARKET_HEAT_RIGHT_LEGEND_MIN_WIDTH: number = 50; +const MARKET_HEAT_RIGHT_LEGEND_MAX_WIDTH: number = 125; +const MARKET_HEAT_LEGEND_COLOR_SIZE: number = 10; +const MARKET_HEAT_LEGEND_COLOR_GAP: number = 5; +const MARKET_HEAT_BOTTOM_ARROW_WIDTH: number = 10; +const MARKET_HEAT_BOTTOM_ARROW_HEIGHT: number = 5; +const MARKET_HEAT_TOP_GRID_ROW_COUNT: number = 4; +const MARKET_HEAT_GRID_COL_GAP: number = 60; +const MARKET_HEAT_MIN_ROSE_PERCENT: number = 0.06; +const MARKET_HEAT_GRID_LINE_WIDTH: number = 0.5; +const MARKET_HEAT_GRID_DASH_LINE_WIDTH: number = 0.5; +const MARKET_HEAT_GRID_DASH_LENGTH: number = 1; +const MARKET_HEAT_GRID_DASH_GAP: number = 1; +const MARKET_HEAT_AXIS_FONT_SIZE: number = 10; +const MARKET_HEAT_TIME_AXIS_FONT_SIZE: number = 12; +const MARKET_HEAT_CURSOR_TIME_LABEL_HEIGHT: number = 14; +const MARKET_HEAT_CURSOR_TIME_LABEL_RADIUS: number = 3; +const MARKET_HEAT_INVALID_INT32_MIN: number = -2147483648; +const MARKET_HEAT_INVALID_INT32_MAX: number = 2147483647; +const MARKET_HEAT_INVALID_INT32_OVERFLOW: number = 2147483648; +const MARKET_HEAT_INVALID_NEGATIVE_INT32_MAX: number = -2147483647; +const MARKET_HEAT_INVALID_UINT32_MAX: number = 4294967295; +const MARKET_HEAT_PRICE_OVERFLOW_MIN: number = 2147483647; +const MARKET_HEAT_LONG_PRESS_DELAY: number = 250; +const MARKET_HEAT_TOUCH_MOVE_THRESHOLD: number = 10; + +interface MarketHeatTabItem { + title: string; + pageId: number; + extRequestText: string; +} + +interface MarketHeatSelectedStock { + code: string; + market: string; + name: string; + fund: string; + price: string; + changePercent: string; +} + +@Entry +@Component +struct MarketHeatPage { + @State currentIndex: number = 0; + @State requestNotifier: number = 0; + @State curveRequestKey: number = 0; + @StorageProp('windowWidthVp') windowWidthVp: number = 0; + @State selectedStock: MarketHeatSelectedStock = { + code: MARKET_HEAT_DEFAULT_CODE, + market: MARKET_HEAT_DEFAULT_MARKET, + name: MARKET_HEAT_DEFAULT_NAME, + fund: '--', + price: '--', + changePercent: '--' + }; + @State tabSortDataIds: Array = [MARKET_HEAT_DEFAULT_SORT_ID, MARKET_HEAT_DEFAULT_SORT_ID, + MARKET_HEAT_DEFAULT_SORT_ID]; + @State tabSortStatuses: Array = [SortStatus.DESC, SortStatus.DESC, SortStatus.DESC]; + private tabs: MarketHeatTabItem[] = [ + { + title: '板块指数', + pageId: MARKET_HEAT_INDEX_PAGE_ID, + extRequestText: `columnorder=${MARKET_HEAT_COLUMN_ORDER}` + }, + { + title: '所有品种', + pageId: MARKET_HEAT_ALL_PAGE_ID, + extRequestText: `columnorder=${MARKET_HEAT_COLUMN_ORDER}\r\nmarketid=68` + }, + { + title: '自选合约', + pageId: MARKET_HEAT_SELF_PAGE_ID, + extRequestText: `columnorder=${MARKET_HEAT_COLUMN_ORDER}\r\nselfstockcustom=1` + } + ]; + + onPageShow() { + this.requestNotifier++; + } + + build() { + Column() { + StatusBar() + PageTitleBar({ + title: '期货市场热度', + barBackgroundColor: $r('app.color.market_heat_content_background'), + titleTextColor: $r('app.color.market_heat_title_text'), + bottomDividerColor: $r('app.color.market_heat_content_background'), + backIconColor: $r('app.color.market_heat_back_icon') + }) + MarketHeatHeader({ + stockName: this.selectedStock.name, + stockCode: this.selectedStock.code, + stockMarket: this.selectedStock.market, + curveRequestKey: this.curveRequestKey + }) + this.buildTabHeader() + Column() { + this.buildCurrentTabContent() + } + .layoutWeight(1) + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + .width('100%') + .height('100%') + .padding({ bottom: GlobalContext.navigationBarHeightVp }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildTabHeader() { + Column() { + Row() + .width('100%') + .height(0.5) + .backgroundColor($r('app.color.hq_table_divider_color')) + Row() { + ForEach(this.tabs, (item: MarketHeatTabItem, index: number) => { + TopTabBarItem({ + title: item.title, + isSelected: this.currentIndex === index, + fillWidth: true, + onSelect: () => { + this.onTabClick(index) + } + }) + }, (item: MarketHeatTabItem) => item.title) + } + .width('100%') + .height(TOP_TAB_BAR_HEIGHT) + } + .width('100%') + .height(TOP_TAB_BAR_HEIGHT + 0.5) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildCurrentTabContent() { + if (this.currentIndex === 0) { + this.buildMarketHeatList(this.tabs[0], 0) + } else if (this.currentIndex === 1) { + this.buildMarketHeatList(this.tabs[1], 1) + } else { + this.buildMarketHeatList(this.tabs[2], 2) + } + } + + @Builder + private buildMarketHeatList(item: MarketHeatTabItem, tabIndex: number) { + MarketHeatList({ + pageId: item.pageId, + extRequestText: item.extRequestText, + initialSortDataId: this.tabSortDataIds[tabIndex], + initialSortStatus: this.tabSortStatuses[tabIndex], + requestNotifier: this.requestNotifier, + windowWidthVp: this.windowWidthVp, + selectedStockCode: this.selectedStock.code, + selectedStockMarket: this.selectedStock.market, + rowClickHandler: this.onMarketHeatRowClick, + sortChangeHandler: (dataId: number, sortStatus: number) => { + this.onTabSortChange(tabIndex, dataId, sortStatus) + } + }) + } + + private onTabSortChange(tabIndex: number, dataId: number, sortStatus: number): void { + this.tabSortDataIds[tabIndex] = dataId; + this.tabSortStatuses[tabIndex] = sortStatus; + } + + private onTabClick(index: number): void { + if (this.currentIndex === index) { + return; + } + animateTo({ + duration: 200, + curve: Curve.EaseOut + }, () => { + this.currentIndex = index; + }); + this.requestNotifier++; + } + + private onMarketHeatRowClick = (stock: MarketHeatSelectedStock): void => { + if (stock.code.length === 0 || stock.market.length === 0) { + return; + } + if (!isSupportMarketHeatMarket(stock.market)) { + promptAction.showToast({ message: '该合约不支持查看资金曲线图' }); + return; + } + if (stock.code === this.selectedStock.code) { + return; + } + this.selectedStock = { + code: stock.code, + market: stock.market, + name: stock.name.length > 0 ? stock.name : MARKET_HEAT_DEFAULT_NAME, + fund: MARKET_HEAT_CURVE_EMPTY_TEXT, + price: MARKET_HEAT_CURVE_EMPTY_TEXT, + changePercent: MARKET_HEAT_CURVE_EMPTY_TEXT + }; + this.curveRequestKey++; + } +} + +@Component +struct MarketHeatHeader { + @Prop stockName: string = MARKET_HEAT_DEFAULT_NAME; + @Prop stockCode: string = MARKET_HEAT_DEFAULT_CODE; + @Prop stockMarket: string = MARKET_HEAT_DEFAULT_MARKET; + @Prop @Watch('onCurveRequestKeyChange') curveRequestKey: number = 0; + @State curveStatusText: string = '加载中'; + @State totalFundText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State totalFundColor: string = ''; + @State displayPriceText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State displayChangeText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State displayFundText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State displayPriceColor: string = ''; + @State displayFundColor: string = ''; + @State priceAxisTopText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State priceAxisMiddleText: string = '0.00'; + @State priceAxisBottomText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State fundAxisTopText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State fundAxisMiddleText: string = '0'; + @State fundAxisBottomText: string = MARKET_HEAT_CURVE_EMPTY_TEXT; + @State timeAxisLeftText: string = ''; + @State timeAxisMiddleText: string = ''; + @State timeAxisRightText: string = ''; + @State timeAxisMiddleLeft: number = 0; + @State isCursorShow: boolean = false; + @State cursorX: number = 0; + @State cursorY: number = 0; + private cursorIndex: number = -1; + private priceData: Array = []; + private fundData: Array = []; + private timeData: Array = []; + private sectionSeries: Array = []; + private totalCount: number = 0; + private closePrice: number = 0; + private highPrice: number = 0; + private lowPrice: number = 0; + private hasReceivedBaseCurveData: boolean = false; + private decimal: number = 2; + private fundValueScale: number = 1; + private totalFundValue: number = Number.MAX_SAFE_INTEGER; + private currentCurveDataId: number = Number.MIN_SAFE_INTEGER; + private curveClient?: RequestClient; + private totalFundClient?: RequestClient; + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + private pendingCursorTimer: number = 0; + private touchStartedWithCursor: boolean = false; + private touchMoved: boolean = false; + private touchStartX: number = 0; + private touchStartY: number = 0; + @StorageProp('enableDarkMode') @Watch('onDarkModeChange') enableDarkMode: boolean = ColourModeStatus.enableDarkMode(); + + aboutToAppear(): void { + this.totalFundColor = this.getNormalTextColor(); + this.displayPriceColor = this.getNormalTextColor(); + this.displayFundColor = this.getNormalTextColor(); + this.requestTotalFund(); + this.requestCurve(); + } + + aboutToDisappear(): void { + this.releaseCurveRequest(); + this.releaseTotalFundRequest(); + this.clearPendingCursorTimer(); + } + + private onCurveRequestKeyChange(): void { + this.clearPendingCursorTimer(); + setTimeout(() => { + this.requestCurve(); + }, 0) + } + + private onDarkModeChange(): void { + this.totalFundColor = getMarketHeatValueColor(this.totalFundValue); + if (this.hasEnoughCurveData()) { + this.updateDescribeValues(-1); + } else { + this.displayPriceColor = this.getNormalTextColor(); + this.displayFundColor = this.getNormalTextColor(); + } + this.requestDraw(); + } + + build() { + Stack({ alignContent: Alignment.Bottom }) { + Column() { + Row() { + Text(`${MARKET_HEAT_DEFAULT_NAME}资金净流入`) + .fontSize(16) + .fontWeight(FontWeight.Normal) + .fontFamily('Regular') + .fontColor($r('app.color.market_heat_primary_text')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text(this.totalFundText) + .fontSize(16) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.totalFundColor) + .width(120) + .textAlign(TextAlign.Start) + .margin({ left: 6 }) + } + .width('100%') + .height(44) + .padding({ left: 15, right: 15 }) + .alignItems(VerticalAlign.Center) + + Row() + .width('100%') + .height(10) + .backgroundColor($r('app.color.market_heat_section_background')) + + Row() { + Text('资金净流入分时图') + .fontSize(13) + .fontFamily('Regular') + .fontColor($r('app.color.market_heat_title_text')) + .height(20) + } + .width('100%') + .height(31) + .padding({ top: 6, bottom: 5 }) + .justifyContent(FlexAlign.Center) + + Stack() { + Canvas(this.canvasContext) + .width('100%') + .height('100%') + .onReady(() => { + this.drawCurve(); + }) + .onAreaChange(() => { + this.requestDraw(); + }) + .onTouch((event: TouchEvent) => { + this.handleChartTouch(event); + }) + this.buildChartAxisLabels() + this.buildChartTimeLabels() + this.buildCursorTimeLabel() + if (this.curveStatusText === '暂无数据') { + Text(this.curveStatusText) + .fontSize(12) + .fontColor($r('app.color.market_heat_empty_text')) + } + } + .width('100%') + .height(MARKET_HEAT_CHART_HEIGHT) + .padding({ left: 15, right: 15 }) + + Row() + .width('100%') + .height(10) + + Stack() { + Row() { + Row() + .width(MARKET_HEAT_LEGEND_COLOR_SIZE) + .height(MARKET_HEAT_LEGEND_COLOR_SIZE) + .borderRadius(2) + .backgroundColor(this.getPriceLegendColor()) + .margin({ top: 2 }) + Column() { + Text(this.getDisplayStockName()) + .fontSize(13) + .fontWeight(FontWeight.Normal) + .fontFamily('Regular') + .fontColor($r('app.color.market_heat_primary_text')) + .height(14) + .width(MARKET_HEAT_LEGEND_NAME_WIDTH) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Row() { + Text(this.displayPriceText) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.displayPriceColor) + Text(this.displayChangeText) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.displayPriceColor) + .margin({ left: 10 }) + } + .margin({ top: 5 }) + } + .margin({ left: MARKET_HEAT_LEGEND_COLOR_GAP }) + .alignItems(HorizontalAlign.Start) + } + .alignItems(VerticalAlign.Top) + .position({ left: 15, top: 0 }) + + Row() { + Row() + .width(MARKET_HEAT_LEGEND_COLOR_SIZE) + .height(MARKET_HEAT_LEGEND_COLOR_SIZE) + .borderRadius(2) + .backgroundColor(this.getFundLineResourceColor()) + .margin({ top: 2 }) + Column() { + Text(this.getFundLegendText()) + .fontSize(13) + .fontFamily('Regular') + .fontColor($r('app.color.market_heat_primary_text')) + .height(14) + .width(this.getRightLegendTextWidth()) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text(this.displayFundText) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.displayFundColor) + .height(14) + .width(this.getRightLegendTextWidth()) + .margin({ top: 5 }) + .maxLines(1) + .textAlign(TextAlign.Start) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .margin({ left: MARKET_HEAT_LEGEND_COLOR_GAP }) + .alignItems(HorizontalAlign.Start) + } + .alignItems(VerticalAlign.Top) + .width(this.getRightLegendBlockWidth()) + .position({ right: 15, top: 0 }) + } + .width('100%') + .height(45) + } + .width('100%') + .backgroundColor($r('app.color.market_heat_content_background')) + + Image($r('app.media.market_heat_arrow')) + .width(MARKET_HEAT_BOTTOM_ARROW_WIDTH) + .height(MARKET_HEAT_BOTTOM_ARROW_HEIGHT) + .hitTestBehavior(HitTestMode.Transparent) + } + .width('100%') + .backgroundColor($r('app.color.market_heat_content_background')) + } + + @Builder + private buildChartAxisLabels() { + if (this.priceAxisTopText.length > 0) { + Text(this.priceAxisTopText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getUpTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.Start) + .maxLines(1) + .position({ left: 2, top: 0 }) + .hitTestBehavior(HitTestMode.Transparent) + Text(this.priceAxisMiddleText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getAxisTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.Start) + .maxLines(1) + .position({ left: 2, top: MARKET_HEAT_PLOT_HEIGHT / 2 - 7 }) + .hitTestBehavior(HitTestMode.Transparent) + Text(this.priceAxisBottomText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getDownTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.Start) + .maxLines(1) + .position({ left: 2, top: MARKET_HEAT_PLOT_HEIGHT - 13 }) + .hitTestBehavior(HitTestMode.Transparent) + Text(this.fundAxisTopText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getUpTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.End) + .maxLines(1) + .position({ right: 2, top: 0 }) + .hitTestBehavior(HitTestMode.Transparent) + Text(this.fundAxisMiddleText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getAxisTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.End) + .maxLines(1) + .position({ right: 2, top: MARKET_HEAT_PLOT_HEIGHT / 2 - 7 }) + .hitTestBehavior(HitTestMode.Transparent) + Text(this.fundAxisBottomText) + .fontSize(MARKET_HEAT_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getDownTextColor()) + .height(MARKET_HEAT_AXIS_LABEL_HEIGHT) + .width(MARKET_HEAT_AXIS_LABEL_WIDTH) + .textAlign(TextAlign.End) + .maxLines(1) + .position({ right: 2, top: MARKET_HEAT_PLOT_HEIGHT - 13 }) + .hitTestBehavior(HitTestMode.Transparent) + } + } + + @Builder + private buildChartTimeLabels() { + Stack() { + Text(this.timeAxisLeftText) + .fontSize(MARKET_HEAT_TIME_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getAxisTextColor()) + .height(MARKET_HEAT_TIME_AXIS_HEIGHT) + .width(MARKET_HEAT_TIME_LABEL_WIDTH) + .textAlign(TextAlign.Start) + .maxLines(1) + .position({ left: 0, top: 0 }) + if (this.timeAxisMiddleText.length > 0) { + Text(this.timeAxisMiddleText) + .fontSize(MARKET_HEAT_TIME_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getAxisTextColor()) + .height(MARKET_HEAT_TIME_AXIS_HEIGHT) + .width(MARKET_HEAT_TIME_LABEL_WIDTH) + .textAlign(TextAlign.Center) + .maxLines(1) + .position({ left: this.timeAxisMiddleLeft, top: 0 }) + } + Text(this.timeAxisRightText) + .fontSize(MARKET_HEAT_TIME_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getAxisTextColor()) + .height(MARKET_HEAT_TIME_AXIS_HEIGHT) + .width(MARKET_HEAT_TIME_LABEL_WIDTH) + .textAlign(TextAlign.End) + .position({ right: 0, top: 0 }) + } + .width('100%') + .height(MARKET_HEAT_TIME_AXIS_HEIGHT) + .position({ left: 0, top: MARKET_HEAT_PLOT_HEIGHT + MARKET_HEAT_TIME_AXIS_GAP }) + .zIndex(10) + .hitTestBehavior(HitTestMode.Transparent) + } + + @Builder + private buildCursorTimeLabel() { + if (this.isCursorShow && this.getCursorTimeText().length > 0) { + Text(this.getCursorTimeText()) + .fontSize(MARKET_HEAT_TIME_AXIS_FONT_SIZE) + .fontWeight(FontWeight.Medium) + .fontFamily('Medium') + .fontColor(this.getNormalTextColor()) + .height(MARKET_HEAT_CURSOR_TIME_LABEL_HEIGHT) + .width(46) + .textAlign(TextAlign.Center) + .maxLines(1) + .backgroundColor(this.getCursorLabelBackgroundColor()) + .border({ width: 1, color: this.getNormalTextColor() }) + .borderRadius(MARKET_HEAT_CURSOR_TIME_LABEL_RADIUS) + .position({ left: this.getCursorTimeLabelLeft(), top: MARKET_HEAT_PLOT_HEIGHT }) + .zIndex(20) + .hitTestBehavior(HitTestMode.Transparent) + } + } + + private getDisplayStockName(): string { + if (this.stockCode.indexOf('8888') >= 0) { + return `${this.stockName}加权`; + } + return this.stockName; + } + + private getFundLegendText(): string { + return `${this.getDisplayStockName()}资金`; + } + + private getRightLegendTextWidth(): number { + const textWidth: number = this.getFundLegendText().length * 13; + return Math.min(MARKET_HEAT_RIGHT_LEGEND_MAX_WIDTH, + Math.max(MARKET_HEAT_RIGHT_LEGEND_MIN_WIDTH, textWidth)); + } + + private getRightLegendBlockWidth(): number { + return MARKET_HEAT_LEGEND_COLOR_SIZE + MARKET_HEAT_LEGEND_COLOR_GAP + this.getRightLegendTextWidth(); + } + + private getPriceLineColor(): string { + return ColorUtils.resourceColorToRGBHex(this.getPriceLineResourceColor()); + } + + private getPriceLineResourceColor(): ResourceColor { + return $r('app.color.market_heat_price_line'); + } + + private getPriceLegendColor(): ResourceColor { + return $r('app.color.market_heat_price_legend'); + } + + private getGridLineColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_grid_line')); + } + + private getFundLineColor(): string { + return ColorUtils.resourceColorToRGBHex(this.getFundLineResourceColor()); + } + + private getFundLineResourceColor(): ResourceColor { + return $r('app.color.market_heat_fund_line'); + } + + private getAxisTextColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_axis_text')); + } + + private getCursorLabelBackgroundColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_cursor_background')); + } + + private getUpTextColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_up_text')); + } + + private getDownTextColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_down_text')); + } + + private getNormalTextColor(): string { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_primary_text')); + } + + private requestDraw(): void { + setTimeout(() => { + this.drawCurve(); + }, 0) + } + + private requestCurve(): void { + if (this.stockCode.length === 0 || this.stockMarket.length === 0) { + this.setEmptyCurveData(); + return; + } + this.releaseCurveRequest(); + this.clearCurveState(); + this.curveStatusText = '加载中'; + this.requestDraw(); + const client: RequestClient = new RequestClient( + MARKET_HEAT_FRAME_ID, + MARKET_HEAT_CURVE_PAGE_ID, + this.createCurveRequestParam(), + (structData: StuffBaseStruct) => { + this.onReceiveCurveData(structData); + } + ); + client.setTimeout(true, 6000, () => { + this.onCurveRequestTimeout(); + }); + this.curveClient = client; + client.request(); + } + + private releaseCurveRequest(): void { + if (this.curveClient) { + this.curveClient.release(); + this.curveClient = undefined; + } + } + + private createCurveRequestParam(): string { + return `tech=QHZJ\r\nfstrend=1\r\nmarketcode=${this.stockMarket}\r\nstockcode=${this.stockCode}\r\n`; + } + + private createTotalFundRequestParam(): string { + return `tech=QHZJ\r\nfstrend=0\r\nmarketcode=${MARKET_HEAT_DEFAULT_MARKET}\r\nstockcode=${MARKET_HEAT_DEFAULT_CODE}\r\n`; + } + + private requestTotalFund(): void { + this.releaseTotalFundRequest(); + const client: RequestClient = new RequestClient( + MARKET_HEAT_FRAME_ID, + MARKET_HEAT_CURVE_PAGE_ID, + this.createTotalFundRequestParam(), + (structData: StuffBaseStruct) => { + this.onReceiveTotalFundData(structData); + } + ); + client.setTimeout(true, 6000, () => { + this.totalFundClient = undefined; + }); + this.totalFundClient = client; + client.request(); + } + + private releaseTotalFundRequest(): void { + if (this.totalFundClient) { + this.totalFundClient.release(); + this.totalFundClient = undefined; + } + } + + private onCurveRequestTimeout(): void { + this.curveClient = undefined; + if (!this.hasEnoughCurveData()) { + this.curveStatusText = '暂无数据'; + this.updateDescribeValues(-1); + this.requestDraw(); + } + } + + private onReceiveTotalFundData(structData: StuffBaseStruct): void { + if (!(structData instanceof StuffCurveStruct)) { + return; + } + const flowArray: Array = structData.getData(MARKET_HEAT_CURVE_DATA_ID) as Array; + const lastValue: number | null = getLastValidMarketHeatValue(flowArray); + if (lastValue === null) { + this.totalFundValue = Number.MAX_SAFE_INTEGER; + this.totalFundText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.totalFundColor = this.getNormalTextColor(); + return; + } + this.totalFundValue = lastValue; + this.totalFundText = formatMarketHeatAmountWithSign(lastValue); + this.totalFundColor = getMarketHeatValueColor(lastValue); + } + + private onReceiveCurveData(structData: StuffBaseStruct): void { + if (!(structData instanceof StuffCurveStruct)) { + this.setEmptyCurveData(); + return; + } + if (!this.isExpectedCurveStruct(structData)) { + return; + } + const timeArray: Array | null = structData.getData(MARKET_HEAT_TIME_DATA_ID); + const priceArray: Array | null = structData.getData(MARKET_HEAT_PRICE_DATA_ID); + const flowArray: Array | null = structData.getData(MARKET_HEAT_CURVE_DATA_ID); + const packetDataId: number = this.getCurvePacketDataId(structData); + if (this.shouldDropRealtimeCurvePacket(structData)) { + return; + } + if (this.shouldResetCurvePacket(structData, packetDataId, priceArray !== null)) { + this.clearCurveState(); + } + if (priceArray !== null && packetDataId !== Number.MIN_SAFE_INTEGER) { + this.currentCurveDataId = packetDataId; + } + this.applyCurveMetadata(structData); + if (!structData.isRealData) { + this.hasReceivedBaseCurveData = true; + } + const marketTimeArray: Array | null = this.getMarketTimeArray(structData); + if (marketTimeArray) { + this.applyMarketTimeData(marketTimeArray); + } + this.applyCurveData(timeArray, priceArray, flowArray, this.getCurveFirstIndex(structData)); + this.totalCount = Math.max(this.totalCount, this.priceData.length, this.fundData.length); + this.updateDefaultTotalFundValue(); + this.updateChartScaleState(); + this.updateDescribeValues(-1); + if (!this.hasEnoughCurveData()) { + this.curveStatusText = ''; + this.requestDraw(); + return; + } + this.curveStatusText = ''; + this.requestDraw(); + } + + private setEmptyCurveData(keepTimeScale: boolean = false): void { + const preservedTimeData: Array = this.timeData; + const preservedSectionSeries: Array = this.sectionSeries; + const preservedTotalCount: number = this.totalCount; + this.clearCurveState(); + if (keepTimeScale) { + this.timeData = preservedTimeData; + this.sectionSeries = preservedSectionSeries; + this.totalCount = preservedTotalCount; + } + this.curveStatusText = '暂无数据'; + this.requestDraw(); + } + + private clearCurveState(): void { + this.priceData = []; + this.fundData = []; + this.timeData = []; + this.sectionSeries = []; + this.totalCount = 0; + this.closePrice = 0; + this.highPrice = 0; + this.lowPrice = 0; + this.hasReceivedBaseCurveData = false; + this.fundValueScale = 1; + this.currentCurveDataId = Number.MIN_SAFE_INTEGER; + this.isCursorShow = false; + this.cursorX = 0; + this.cursorY = 0; + this.cursorIndex = -1; + this.displayPriceText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayChangeText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayFundText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayPriceColor = this.getNormalTextColor(); + this.displayFundColor = this.getNormalTextColor(); + this.clearChartScaleLabels(); + this.updateChartScaleState(); + } + + private applyCurveData(timeArray: Array | null | undefined, priceArray: Array | null | undefined, + fundArray: Array | null | undefined, firstIndex: number = 0): void { + if (!timeArray && !priceArray && !fundArray) { + return; + } + const timeLength: number = timeArray ? timeArray.length : 0; + const priceLength: number = priceArray ? priceArray.length : 0; + const fundLength: number = fundArray ? fundArray.length : 0; + const length: number = Math.max(timeLength, priceLength, fundLength); + let previousPrice: number = this.getPreviousPriceForMerge(firstIndex); + for (let index = 0; index < length; index++) { + const targetIndex: number = firstIndex + index; + if (priceArray && priceArray.length > index) { + let priceValue: number = priceArray[index]; + if (isMarketHeatInvalidPriceNumber(priceValue)) { + priceValue = previousPrice > 0 ? previousPrice : Number.MAX_SAFE_INTEGER; + } else { + previousPrice = priceValue; + } + this.setCurveDataAt(this.priceData, targetIndex, priceValue); + } + if (fundArray && fundArray.length > index) { + let fundValue: number = fundArray[index]; + if (isMarketHeatInvalidNumber(fundValue)) { + fundValue = Number.MAX_SAFE_INTEGER; + } + this.setCurveDataAt(this.fundData, targetIndex, fundValue); + } + if (timeArray && timeArray.length > index) { + this.setCurveDataAt(this.timeData, targetIndex, timeArray[index]); + } + } + } + + private isExpectedCurveStruct(structData: StuffCurveStruct): boolean { + return this.isExpectedCurveField(structData.getExtData(MARKET_HEAT_STOCK_CODE_EX_ID), this.stockCode) && + this.isExpectedCurveField(structData.getExtData(MARKET_HEAT_STOCK_MARKET_EX_ID), this.stockMarket); + } + + private isExpectedCurveField(value: string | number | object | null | undefined, expectedValue: string): boolean { + if (typeof value === 'undefined' || value === null) { + return true; + } + if (typeof value === 'number') { + return value.toString() === expectedValue; + } + if (typeof value === 'string') { + return value.trim() === expectedValue; + } + return true; + } + + private getCurvePacketDataId(structData: StuffCurveStruct): number { + const value: string | number | object | null | undefined = structData.getExtData(MARKET_HEAT_DATA_ID_EX_ID); + if (typeof value === 'number') { + return value; + } + return Number.MIN_SAFE_INTEGER; + } + + private shouldDropRealtimeCurvePacket(structData: StuffCurveStruct): boolean { + if (!structData.isRealData) { + return false; + } + return this.getCurveFirstIndex(structData) > 0 && !this.hasReceivedBaseCurveData; + } + + private shouldResetCurvePacket(structData: StuffCurveStruct, packetDataId: number, hasPriceData: boolean): boolean { + if (!hasPriceData) { + return false; + } + if (packetDataId === Number.MIN_SAFE_INTEGER) { + return false; + } + if (!structData.isRealData) { + return false; + } + return this.currentCurveDataId !== Number.MIN_SAFE_INTEGER && this.currentCurveDataId !== packetDataId && + this.getCurveFirstIndex(structData) === 0; + } + + private getCurveFirstIndex(structData: StuffCurveStruct): number { + return Math.max(0, structData.firstIndex); + } + + private getPreviousPriceForMerge(firstIndex: number): number { + if (firstIndex > 0 && this.priceData.length >= firstIndex) { + const previousPrice: number = this.priceData[firstIndex - 1]; + if (!isMarketHeatInvalidPriceNumber(previousPrice)) { + return previousPrice; + } + } + return !isMarketHeatInvalidPriceNumber(this.closePrice) && this.closePrice > 0 ? this.closePrice : 0; + } + + private setCurveDataAt(target: Array, index: number, value: number): void { + while (target.length < index) { + target.push(Number.MAX_SAFE_INTEGER); + } + if (target.length > index) { + target[index] = value; + } else { + target.push(value); + } + } + + private getMarketTimeArray(structData: StuffCurveStruct): Array | null { + const marketTimeData: object | null = structData.getExtData(MARKET_HEAT_MARKET_TIME_EX_ID); + if (Array.isArray(marketTimeData)) { + return marketTimeData as Array; + } + return null; + } + + private applyMarketTimeData(marketTimeArray: Array | null | undefined): void { + this.sectionSeries = []; + this.totalCount = 0; + if (!marketTimeArray || marketTimeArray.length < 2) { + return; + } + for (let index = 0; index < marketTimeArray.length; index++) { + const rawTime: number = marketTimeArray[index]; + if (isMarketHeatInvalidNumber(rawTime)) { + continue; + } + const sectionMinute: number = Math.floor(rawTime / 60); + this.sectionSeries.push(sectionMinute); + if (this.sectionSeries.length % 2 === 0) { + const startMinute: number = this.sectionSeries[this.sectionSeries.length - 2]; + this.totalCount += sectionMinute - startMinute; + } + } + if (this.sectionSeries.length % 2 !== 0) { + this.sectionSeries.pop(); + } + if (this.sectionSeries.length < 2) { + this.totalCount = 0; + } + } + + private getDecimalFromStruct(structData: StuffCurveStruct): number { + const value: string | number | object | undefined = structData.extDataTable.get(MARKET_HEAT_DECIMAL_EX_ID); + if (typeof value === 'number' && value >= 0 && value <= 6) { + return value; + } + return 2; + } + + private applyCurveMetadata(structData: StuffCurveStruct): void { + const decimalValue: number = this.getDecimalFromStruct(structData); + if (!structData.isRealData || decimalValue !== 2 || this.decimal === 2) { + this.decimal = decimalValue; + } + const preCloseValue: number = this.getCurveMetadataFirstNumber(structData, MARKET_HEAT_PRE_CLOSE_DATA_ID); + if (!isMarketHeatInvalidPriceNumber(preCloseValue) && preCloseValue !== 0) { + this.closePrice = preCloseValue; + } else if (isMarketHeatInvalidPriceNumber(this.closePrice) || this.closePrice === 0) { + const firstPriceValue: number = this.getFirstValidPriceNumber(structData.getData(MARKET_HEAT_PRICE_DATA_ID)); + if (!isMarketHeatInvalidPriceNumber(firstPriceValue) && firstPriceValue > 0) { + this.closePrice = firstPriceValue; + } + } + const highPriceValue: number = this.getCurveMetadataFirstNumber(structData, MARKET_HEAT_HIGH_PRICE_DATA_ID); + if (!isMarketHeatInvalidPriceNumber(highPriceValue) && highPriceValue > 0) { + this.highPrice = highPriceValue; + } else if (!structData.isRealData) { + this.highPrice = 0; + } + const lowPriceValue: number = this.getCurveMetadataFirstNumber(structData, MARKET_HEAT_LOW_PRICE_DATA_ID); + if (!isMarketHeatInvalidPriceNumber(lowPriceValue) && lowPriceValue > 0) { + this.lowPrice = lowPriceValue; + } else if (!structData.isRealData) { + this.lowPrice = 0; + } + } + + private getFirstValidPriceNumber(source: Array | null | undefined): number { + if (!source) { + return Number.MAX_SAFE_INTEGER; + } + for (let index = 0; index < source.length; index++) { + const value: number = source[index]; + if (!isMarketHeatInvalidPriceNumber(value) && value > 0) { + return value; + } + } + return Number.MAX_SAFE_INTEGER; + } + + private getCurveMetadataFirstNumber(structData: StuffCurveStruct, dataId: number): number { + const value: string | number | object | null | undefined = structData.getExtData(dataId); + if (typeof value === 'number' && !isMarketHeatInvalidPriceNumber(value) && value > 0) { + return value; + } + if (Array.isArray(value)) { + const extValue: number = this.getFirstValidPriceNumber(value); + if (!isMarketHeatInvalidPriceNumber(extValue) && extValue > 0) { + return extValue; + } + } + return this.getFirstValidPriceNumber(structData.getData(dataId)); + } + + private updateChartScaleState(): void { + if (this.hasValidPriceScaleData()) { + this.updatePriceAxisLabelState(this.getPriceRange()); + } else { + this.updateEmptyPriceAxisLabelState(); + } + if (this.hasValidCurveValue(this.fundData, false)) { + this.updateFundAxisLabelState(this.getFundRange()); + } else { + this.updateEmptyFundAxisLabelState(); + } + } + + private drawCurve(): void { + const width: number = this.canvasContext.width; + const height: number = this.canvasContext.height; + if (width <= 0 || height <= 0) { + return; + } + this.canvasContext.clearRect(0, 0, width, height); + const borderPlotHeight: number = this.getBorderPlotHeight(height); + const plotWidth: number = this.getPlotWidth(width); + const plotHeight: number = this.getPlotHeight(height); + this.drawCurveBackground(width, borderPlotHeight); + this.updateTimeLabelState(width); + if (!this.hasEnoughCurveData()) { + return; + } + const priceRange: MarketHeatCurveRange = this.getPriceRange(); + const fundRange: MarketHeatCurveRange = this.getFundRange(); + this.drawLine(this.priceData, priceRange, plotWidth, plotHeight, this.getPriceLineColor(), true); + this.drawLine(this.fundData, fundRange, plotWidth, plotHeight, this.getFundLineColor()); + if (this.isCursorShow) { + this.drawCursor(plotWidth, plotHeight); + } + } + + private drawCurveBackground(width: number, plotHeight: number): void { + this.canvasContext.setLineDash([]); + this.canvasContext.fillStyle = this.getGridLineColor(); + this.drawPixelRectWithThinLine(0, 0, width, plotHeight); + this.drawLandscapeGridLines(width, plotHeight); + this.drawPortraitGridLines(width, plotHeight); + } + + private drawLandscapeGridLines(width: number, plotHeight: number): void { + for (let index = 1; index < MARKET_HEAT_TOP_GRID_ROW_COUNT; index++) { + const y: number = plotHeight * index / MARKET_HEAT_TOP_GRID_ROW_COUNT; + if (index === MARKET_HEAT_TOP_GRID_ROW_COUNT / 2) { + this.drawPixelHorizontal(0, y, width, MARKET_HEAT_GRID_LINE_WIDTH); + } else { + this.drawDashedLineHorizontal(0, y, width); + } + } + this.canvasContext.setLineDash([]); + } + + private drawPortraitGridLines(plotWidth: number, plotHeight: number): void { + let sectionSeries: Array = this.sectionSeries; + let totalCount: number = this.totalCount; + if (totalCount <= 0 || sectionSeries.length === 0) { + sectionSeries = [570, 691, 780, 901]; + totalCount = 242; + } + let gridColsGap: number = MARKET_HEAT_GRID_COL_GAP; + if (totalCount / MARKET_HEAT_GRID_COL_GAP < 8) { + gridColsGap = MARKET_HEAT_GRID_COL_GAP; + } else if (totalCount / MARKET_HEAT_GRID_COL_GAP < 16) { + gridColsGap = MARKET_HEAT_GRID_COL_GAP * 2; + } else { + gridColsGap = MARKET_HEAT_GRID_COL_GAP * 4; + } + let colWidthIndex: number = 0; + const sectionCount: number = Math.floor(sectionSeries.length / 2); + for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) { + const startMinute: number = sectionSeries[sectionIndex * 2]; + const endMinute: number = sectionSeries[sectionIndex * 2 + 1]; + const sectionDots: number = Math.max(0, endMinute - 1 - startMinute); + const cellWidth: number = plotWidth * sectionDots / totalCount; + if (sectionDots > gridColsGap) { + let subColWidthIndex: number = 0; + const innerLineCount: number = Math.floor(sectionDots / gridColsGap); + for (let lineIndex = 0; lineIndex < innerLineCount; lineIndex++) { + subColWidthIndex += cellWidth / sectionDots * gridColsGap; + if (lineIndex === innerLineCount - 1 && sectionDots % gridColsGap === 0) { + continue; + } + this.drawVerticalGridLine(colWidthIndex + subColWidthIndex, plotHeight, true); + } + } + colWidthIndex += cellWidth; + if (sectionIndex < sectionCount - 1) { + this.drawVerticalGridLine(colWidthIndex, plotHeight, false); + } + } + this.canvasContext.setLineDash([]); + } + + private drawVerticalGridLine(x: number, plotHeight: number, dashed: boolean): void { + if (dashed) { + this.drawDashedLineVertical(x, 0, plotHeight); + return; + } + this.drawPixelVertical(x, 0, plotHeight, MARKET_HEAT_GRID_LINE_WIDTH); + } + + private drawPixelRectWithThinLine(x: number, y: number, width: number, height: number): void { + if (width <= 0 || height <= 0) { + return; + } + this.drawPixelHorizontal(x, y, Math.max(0, width - 1), MARKET_HEAT_GRID_LINE_WIDTH); + this.drawPixelHorizontal(x, y + height - 1, Math.max(0, width - 1), MARKET_HEAT_GRID_LINE_WIDTH); + this.drawPixelVertical(x, y, Math.max(0, height - MARKET_HEAT_GRID_LINE_WIDTH), MARKET_HEAT_GRID_LINE_WIDTH); + this.drawPixelVertical(x + width - 1, y, Math.max(0, height - MARKET_HEAT_GRID_LINE_WIDTH), + MARKET_HEAT_GRID_LINE_WIDTH); + } + + private drawPixelHorizontal(x: number, y: number, length: number, lineWidth: number): void { + if (length <= 0 || lineWidth <= 0) { + return; + } + this.canvasContext.fillRect(x, y, length, lineWidth); + } + + private drawPixelVertical(x: number, y: number, length: number, lineWidth: number): void { + if (length <= 0 || lineWidth <= 0) { + return; + } + this.canvasContext.fillRect(x, y, lineWidth, length); + } + + private drawDashedLineHorizontal(x: number, y: number, length: number): void { + let offset: number = 0; + const step: number = MARKET_HEAT_GRID_DASH_LENGTH + MARKET_HEAT_GRID_DASH_GAP; + while (offset < length) { + this.drawPixelHorizontal(x + offset, y, Math.min(MARKET_HEAT_GRID_DASH_LENGTH, length - offset), + MARKET_HEAT_GRID_DASH_LINE_WIDTH); + offset += step; + } + } + + private drawDashedLineVertical(x: number, y: number, length: number): void { + let offset: number = 0; + const step: number = MARKET_HEAT_GRID_DASH_LENGTH + MARKET_HEAT_GRID_DASH_GAP; + while (offset < length) { + this.drawPixelVertical(x, y + offset, Math.min(MARKET_HEAT_GRID_DASH_LENGTH, length - offset), + MARKET_HEAT_GRID_DASH_LINE_WIDTH); + offset += step; + } + } + + private drawLine(source: Array, range: MarketHeatCurveRange, width: number, plotHeight: number, + color: string, isPriceLine: boolean = false): void { + this.canvasContext.beginPath(); + let hasStarted: boolean = false; + source.forEach((value: number, index: number) => { + const isInvalidValue: boolean = isPriceLine ? isMarketHeatInvalidPriceNumber(value) : + isMarketHeatInvalidNumber(value); + if (isInvalidValue) { + hasStarted = false; + return; + } + if (!hasStarted) { + this.canvasContext.moveTo(this.getCurveX(index, width), this.getCurveY(value, range, plotHeight)); + hasStarted = true; + } else { + this.canvasContext.lineTo(this.getCurveX(index, width), this.getCurveY(value, range, plotHeight)); + } + }) + this.canvasContext.lineWidth = 1.2; + this.canvasContext.strokeStyle = color; + this.canvasContext.setLineDash([]); + this.canvasContext.stroke(); + } + + private drawCursor(width: number, plotHeight: number): void { + this.canvasContext.beginPath(); + this.canvasContext.moveTo(this.cursorX, 0); + this.canvasContext.lineTo(this.cursorX, plotHeight); + this.canvasContext.lineWidth = 1; + this.canvasContext.strokeStyle = this.getNormalTextColor(); + this.canvasContext.setLineDash([]); + this.canvasContext.stroke(); + } + + private getCursorTimeLabelLeft(): number { + const width: number = this.canvasContext.width; + const labelWidth: number = 46; + return Math.max(1, Math.min(this.cursorX - labelWidth / 2, width - labelWidth - 1)); + } + + private getCursorTimeText(): string { + if (this.cursorIndex < 0) { + return ''; + } + if (this.cursorIndex < this.timeData.length) { + const timeText: string = formatMarketHeatTime(this.timeData[this.cursorIndex]); + if (timeText.length > 0) { + return timeText; + } + } + return this.getTradeTimeTextByIndex(this.cursorIndex); + } + + private getTradeTimeTextByIndex(index: number): string { + let startIndex: number = 0; + const sectionCount: number = Math.floor(this.sectionSeries.length / 2); + for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) { + const startMinute: number = this.sectionSeries[sectionIndex * 2]; + const endMinute: number = this.sectionSeries[sectionIndex * 2 + 1]; + const sectionCountValue: number = Math.max(0, endMinute - startMinute); + if (index < startIndex + sectionCountValue) { + return formatMarketHeatTradeMinute(startMinute + index - startIndex); + } + startIndex += sectionCountValue; + } + return ''; + } + + private drawTimeAxisLabels(width: number, plotHeight: number): void { + if (this.timeAxisLeftText.length === 0 || this.timeAxisRightText.length === 0) { + return; + } + const topY: number = plotHeight + MARKET_HEAT_TIME_AXIS_GAP; + this.canvasContext.font = `${MARKET_HEAT_TIME_AXIS_FONT_SIZE}vp Medium`; + this.canvasContext.fillStyle = this.getAxisTextColor(); + this.canvasContext.textBaseline = 'top'; + this.canvasContext.textAlign = 'left'; + this.canvasContext.fillText(this.timeAxisLeftText, 0, topY); + if (this.timeAxisMiddleText.length > 0) { + this.canvasContext.textAlign = 'center'; + this.canvasContext.fillText(this.timeAxisMiddleText, this.getMiddleTimeLabelX(width), topY); + } + this.canvasContext.textAlign = 'right'; + this.canvasContext.fillText(this.timeAxisRightText, width, topY); + } + + private getPriceRange(): MarketHeatCurveRange { + const hasPriceData: boolean = this.hasValidCurveValue(this.priceData, true); + const range: MarketHeatCurveRange = this.getValidRange(this.priceData, true); + let minValue: number = hasPriceData ? range.min : Number.MAX_SAFE_INTEGER; + let maxValue: number = hasPriceData ? range.max : Number.MIN_SAFE_INTEGER; + if (!isMarketHeatInvalidPriceNumber(this.highPrice) && this.highPrice > 0) { + maxValue = Math.max(maxValue, this.highPrice); + } + if (!isMarketHeatInvalidPriceNumber(this.lowPrice) && this.lowPrice > 0) { + minValue = Math.min(minValue, this.lowPrice); + } + if (!isMarketHeatInvalidPriceNumber(this.closePrice) && this.closePrice !== 0) { + if (minValue === Number.MAX_SAFE_INTEGER) { + minValue = this.closePrice; + } + if (maxValue === Number.MIN_SAFE_INTEGER) { + maxValue = this.closePrice; + } + let gap: number = Math.max(Math.abs(maxValue - this.closePrice), Math.abs(minValue - this.closePrice)); + const minGap: number = Math.abs(this.closePrice) * MARKET_HEAT_MIN_ROSE_PERCENT / 100; + if (gap < minGap) { + gap = minGap; + } + maxValue = this.closePrice + gap; + minValue = this.closePrice - gap; + } + if (minValue === Number.MAX_SAFE_INTEGER && maxValue !== Number.MIN_SAFE_INTEGER) { + minValue = maxValue; + } + if (maxValue === Number.MIN_SAFE_INTEGER && minValue !== Number.MAX_SAFE_INTEGER) { + maxValue = minValue; + } + if (minValue === maxValue) { + const offset: number = Math.max(Math.abs(minValue) * 0.01, 1); + minValue -= offset; + maxValue += offset; + } + return { + min: minValue, + max: maxValue + }; + } + + private getFundRange(): MarketHeatCurveRange { + const range: MarketHeatCurveRange = this.getValidRange(this.fundData); + const maxAbsValue: number = Math.max(Math.abs(range.min), Math.abs(range.max), 1); + return { + min: -maxAbsValue, + max: maxAbsValue + }; + } + + private getCurveX(index: number, width: number): number { + const totalCount: number = this.getCurveTotalCount(); + const cellWidth: number = width / totalCount; + return MARKET_HEAT_CHART_BORDER_WIDTH + cellWidth / 2 + index * cellWidth; + } + + private getCurveY(value: number, range: MarketHeatCurveRange, height: number): number { + let y: number = height - (value - range.min) * height / (range.max - range.min); + if (y < 1) { + y = 0.5; + } + if (y === height) { + y -= MARKET_HEAT_CHART_BORDER_WIDTH; + } + if (y < height && y > height - 1) { + y = height - 1; + } + return y; + } + + private hasEnoughCurveData(): boolean { + let count: number = 0; + const length: number = Math.min(this.priceData.length, this.fundData.length); + for (let index = 0; index < length; index++) { + if (!isMarketHeatInvalidPriceNumber(this.priceData[index]) && !isMarketHeatInvalidNumber(this.fundData[index])) { + count++; + if (count >= 2) { + return true; + } + } + } + return false; + } + + private hasValidPriceScaleData(): boolean { + return this.hasValidCurveValue(this.priceData, true) || + (!isMarketHeatInvalidPriceNumber(this.highPrice) && this.highPrice > 0) || + (!isMarketHeatInvalidPriceNumber(this.lowPrice) && this.lowPrice > 0) || + (!isMarketHeatInvalidPriceNumber(this.closePrice) && this.closePrice !== 0); + } + + private hasValidCurveValue(source: Array, isPriceValue: boolean): boolean { + for (let index = 0; index < source.length; index++) { + const value: number = source[index]; + const isInvalidValue: boolean = isPriceValue ? isMarketHeatInvalidPriceNumber(value) : + isMarketHeatInvalidNumber(value); + if (!isInvalidValue) { + return true; + } + } + return false; + } + + private getPlotWidth(width: number): number { + return Math.max(1, width - MARKET_HEAT_CHART_BORDER_WIDTH * 2); + } + + private getPlotHeight(height: number): number { + return Math.max(1, this.getBorderPlotHeight(height) - MARKET_HEAT_CHART_BORDER_WIDTH * 2); + } + + private getBorderPlotHeight(height: number): number { + return Math.max(1, Math.min(MARKET_HEAT_PLOT_HEIGHT, + height - MARKET_HEAT_TIME_AXIS_HEIGHT - MARKET_HEAT_TIME_AXIS_GAP)); + } + + private getCurveTotalCount(): number { + if (this.totalCount > 0) { + return this.totalCount; + } + return Math.max(1, this.priceData.length, this.fundData.length); + } + + private getValidRange(source: Array, isPriceValue: boolean = false): MarketHeatCurveRange { + let minValue: number = Number.MAX_SAFE_INTEGER; + let maxValue: number = Number.MIN_SAFE_INTEGER; + source.forEach((value: number) => { + const isInvalidValue: boolean = isPriceValue ? isMarketHeatInvalidPriceNumber(value) : + isMarketHeatInvalidNumber(value); + if (!isInvalidValue) { + minValue = Math.min(minValue, value); + maxValue = Math.max(maxValue, value); + } + }) + if (minValue === Number.MAX_SAFE_INTEGER || maxValue === Number.MIN_SAFE_INTEGER) { + return { + min: -1, + max: 1 + }; + } + return { + min: minValue, + max: maxValue + }; + } + + private getPriceAxisLabels(range: MarketHeatCurveRange): Array { + const middleValue: number = !isMarketHeatInvalidPriceNumber(this.closePrice) && this.closePrice !== 0 ? + this.closePrice : (range.max + range.min) / 2; + const valueGap: number = this.getPriceAxisValueGap(range, middleValue); + return [ + formatMarketHeatPrice(middleValue + valueGap, this.decimal), + formatMarketHeatPrice(middleValue, this.decimal), + formatMarketHeatPrice(middleValue - valueGap, this.decimal) + ]; + } + + private getPriceAxisValueGap(range: MarketHeatCurveRange, middleValue: number): number { + let maxValue: number = range.max; + let minValue: number = range.min; + if (middleValue !== 0) { + const upLimit: number = (maxValue - middleValue) / middleValue * 100; + const downLimit: number = (minValue - middleValue) / middleValue * 100; + const averageLimit: number = (maxValue - minValue) / middleValue * 100; + if (Math.abs(upLimit) < MARKET_HEAT_MIN_ROSE_PERCENT && + Math.abs(downLimit) < MARKET_HEAT_MIN_ROSE_PERCENT && + Math.abs(averageLimit) < MARKET_HEAT_MIN_ROSE_PERCENT) { + maxValue = middleValue * (100 + MARKET_HEAT_MIN_ROSE_PERCENT) / 100; + minValue = middleValue * (100 - MARKET_HEAT_MIN_ROSE_PERCENT) / 100; + } + } + let valueGap: number = Math.max(Math.abs(maxValue - middleValue), Math.abs(minValue - middleValue)); + if (valueGap === 0) { + valueGap = Math.max(Math.abs(middleValue) * MARKET_HEAT_MIN_ROSE_PERCENT / 100, 1); + } + return valueGap; + } + + private getFundAxisLabels(range: MarketHeatCurveRange): Array { + return [ + formatMarketHeatAmount(range.max), + formatMarketHeatAmount(0), + formatMarketHeatAmount(range.min) + ]; + } + + private getTimeScaleLabels(): Array { + const sectionCount: number = Math.floor(this.sectionSeries.length / 2); + if (sectionCount === 2) { + return [ + formatMarketHeatTradeMinute(this.sectionSeries[0]), + formatMarketHeatTradeMinute(this.sectionSeries[1] - 1), + formatMarketHeatTradeMinute(this.sectionSeries[3] - 1) + ]; + } + if (sectionCount > 0) { + return [ + formatMarketHeatTradeMinute(this.sectionSeries[0]), + formatMarketHeatTradeMinute(this.sectionSeries[this.sectionSeries.length - 1] - 1) + ]; + } + const firstTimeText: string = this.getFirstValidTimeText(); + const lastTimeText: string = this.getLastValidTimeText(); + if (firstTimeText.length > 0 && lastTimeText.length > 0) { + return [ + firstTimeText, + lastTimeText + ]; + } + return [ + '09:30', + '11:30', + '15:00' + ]; + } + + private getTimeAxisDisplayText(index: number): string { + const labels: Array = this.getTimeScaleLabels(); + if (index >= 0 && index < labels.length) { + return labels[index]; + } + return ''; + } + + private hasMiddleTimeAxisDisplayText(): boolean { + return this.getTimeScaleLabels().length >= 3; + } + + private getTimeAxisRightLabelIndex(): number { + const labels: Array = this.getTimeScaleLabels(); + if (labels.length >= 3) { + return 2; + } + if (labels.length >= 2) { + return 1; + } + return 0; + } + + private getFirstValidTimeText(): string { + for (let index = 0; index < this.timeData.length; index++) { + const text: string = formatMarketHeatTime(this.timeData[index]); + if (text.length > 0) { + return text; + } + } + return ''; + } + + private getLastValidTimeText(): string { + for (let index = this.timeData.length - 1; index >= 0; index--) { + const text: string = formatMarketHeatTime(this.timeData[index]); + if (text.length > 0) { + return text; + } + } + return ''; + } + + private getMiddleTimeLabelX(plotWidth: number): number { + if (this.sectionSeries.length < 2 || this.totalCount <= 0) { + return plotWidth / 2; + } + const leftTradeSectionDots: number = Math.max(0, this.sectionSeries[1] - 1 - this.sectionSeries[0]); + return plotWidth * leftTradeSectionDots / this.totalCount; + } + + private updatePriceAxisLabelState(priceRange: MarketHeatCurveRange): void { + const priceLabels: Array = this.getPriceAxisLabels(priceRange); + if (priceLabels.length < 3) { + this.updateEmptyPriceAxisLabelState(); + return; + } + if (this.priceAxisTopText !== priceLabels[0]) { + this.priceAxisTopText = priceLabels[0]; + } + if (this.priceAxisMiddleText !== priceLabels[1]) { + this.priceAxisMiddleText = priceLabels[1]; + } + if (this.priceAxisBottomText !== priceLabels[2]) { + this.priceAxisBottomText = priceLabels[2]; + } + } + + private updateFundAxisLabelState(fundRange: MarketHeatCurveRange): void { + const fundLabels: Array = this.getFundAxisLabels(fundRange); + if (fundLabels.length < 3) { + this.updateEmptyFundAxisLabelState(); + return; + } + if (this.fundAxisTopText !== fundLabels[0]) { + this.fundAxisTopText = fundLabels[0]; + } + if (this.fundAxisMiddleText !== fundLabels[1]) { + this.fundAxisMiddleText = fundLabels[1]; + } + if (this.fundAxisBottomText !== fundLabels[2]) { + this.fundAxisBottomText = fundLabels[2]; + } + } + + private updateEmptyPriceAxisLabelState(): void { + this.priceAxisTopText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.priceAxisMiddleText = this.getInitialPriceText(); + this.priceAxisBottomText = MARKET_HEAT_CURVE_EMPTY_TEXT; + } + + private updateEmptyFundAxisLabelState(): void { + this.fundAxisTopText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.fundAxisMiddleText = '0'; + this.fundAxisBottomText = MARKET_HEAT_CURVE_EMPTY_TEXT; + } + + private getInitialPriceText(): string { + if (!isMarketHeatInvalidPriceNumber(this.closePrice) && this.closePrice !== 0) { + return formatMarketHeatPrice(this.closePrice, this.decimal); + } + return '0.00'; + } + + private updateTimeLabelState(width: number): void { + const labels: Array = this.getTimeScaleLabels(); + if (labels.length < 2) { + this.clearTimeLabelState(); + return; + } + let middleText: string = ''; + let rightText: string = labels[1]; + let middleLeft: number = 0; + if (labels.length === 3) { + middleText = labels[1]; + rightText = labels[2]; + middleLeft = this.getMiddleTimeLabelX(width) - MARKET_HEAT_TIME_LABEL_WIDTH / 2; + middleLeft = Math.max(0, Math.min(middleLeft, width - MARKET_HEAT_TIME_LABEL_WIDTH)); + } + if (this.timeAxisLeftText !== labels[0]) { + this.timeAxisLeftText = labels[0]; + } + if (this.timeAxisMiddleText !== middleText) { + this.timeAxisMiddleText = middleText; + } + if (this.timeAxisRightText !== rightText) { + this.timeAxisRightText = rightText; + } + if (this.timeAxisMiddleLeft !== middleLeft) { + this.timeAxisMiddleLeft = middleLeft; + } + } + + private clearChartScaleLabels(): void { + this.clearAxisLabelState(); + this.clearTimeLabelState(); + } + + private clearAxisLabelState(): void { + if (this.priceAxisTopText.length > 0) { + this.priceAxisTopText = ''; + } + if (this.priceAxisMiddleText.length > 0) { + this.priceAxisMiddleText = ''; + } + if (this.priceAxisBottomText.length > 0) { + this.priceAxisBottomText = ''; + } + if (this.fundAxisTopText.length > 0) { + this.fundAxisTopText = ''; + } + if (this.fundAxisMiddleText.length > 0) { + this.fundAxisMiddleText = ''; + } + if (this.fundAxisBottomText.length > 0) { + this.fundAxisBottomText = ''; + } + } + + private clearTimeLabelState(): void { + if (this.timeAxisLeftText.length > 0) { + this.timeAxisLeftText = ''; + } + if (this.timeAxisMiddleText.length > 0) { + this.timeAxisMiddleText = ''; + } + if (this.timeAxisRightText.length > 0) { + this.timeAxisRightText = ''; + } + if (this.timeAxisMiddleLeft !== 0) { + this.timeAxisMiddleLeft = 0; + } + } + + private handleChartTouch(event: TouchEvent): void { + if (!event || !event.touches || event.touches.length === 0) { + return; + } + if (this.priceData.length === 0 || this.fundData.length === 0) { + return; + } + const touch = event.touches[0]; + if (event.type === TouchType.Down) { + this.handleChartTouchDown(touch.x, touch.y); + } else if (event.type === TouchType.Move) { + this.handleChartTouchMove(touch.x, touch.y); + } else if (event.type === TouchType.Up) { + this.handleChartTouchEnd(); + } else if (event.type === TouchType.Cancel) { + this.clearPendingCursorTimer(); + } + } + + private handleChartTouchDown(x: number, y: number): void { + this.clearPendingCursorTimer(); + this.touchStartX = x; + this.touchStartY = y; + this.touchMoved = false; + this.touchStartedWithCursor = this.isCursorShow; + if (this.touchStartedWithCursor) { + return; + } + if (!this.isPointInPlot(x, y)) { + return; + } + this.pendingCursorTimer = setTimeout(() => { + this.pendingCursorTimer = 0; + if (!this.touchMoved) { + this.showCursorAt(this.touchStartX, this.touchStartY); + } + }, MARKET_HEAT_LONG_PRESS_DELAY); + } + + private handleChartTouchMove(x: number, y: number): void { + if (Math.abs(x - this.touchStartX) > MARKET_HEAT_TOUCH_MOVE_THRESHOLD || + Math.abs(y - this.touchStartY) > MARKET_HEAT_TOUCH_MOVE_THRESHOLD) { + this.touchMoved = true; + this.clearPendingCursorTimer(); + } + if (this.isCursorShow) { + this.showCursorAt(x, y); + } + } + + private handleChartTouchEnd(): void { + this.clearPendingCursorTimer(); + if (this.touchStartedWithCursor && !this.touchMoved && this.isCursorShow) { + this.hideCursor(); + } + } + + private clearPendingCursorTimer(): void { + if (this.pendingCursorTimer > 0) { + clearTimeout(this.pendingCursorTimer); + this.pendingCursorTimer = 0; + } + } + + private hideCursor(): void { + this.isCursorShow = false; + this.cursorX = 0; + this.cursorY = 0; + this.cursorIndex = -1; + this.updateDescribeValues(-1); + this.requestDraw(); + } + + private showCursorAt(x: number, y: number): void { + const width: number = this.canvasContext.width; + const plotWidth: number = this.getPlotWidth(width); + const plotHeight: number = this.getPlotHeight(this.canvasContext.height); + const index: number = this.getIndexByX(x, width); + this.cursorIndex = index; + this.cursorX = this.getCurveX(index, plotWidth); + this.cursorY = this.getClampedCursorY(y, plotHeight); + this.isCursorShow = true; + this.updateDescribeValues(index); + this.requestDraw(); + } + + private isPointInPlot(x: number, y: number): boolean { + const width: number = this.canvasContext.width; + const plotHeight: number = this.getPlotHeight(this.canvasContext.height); + return x >= 0 && x <= width && y >= 0 && y <= plotHeight; + } + + private getClampedCursorY(y: number, plotHeight: number): number { + if (y < 0) { + return 0; + } + if (y > plotHeight) { + return plotHeight; + } + return y; + } + + private getIndexByX(x: number, width: number): number { + const totalCount: number = this.getCurveTotalCount(); + const plotWidth: number = this.getPlotWidth(width); + if (this.priceData.length <= 1 || plotWidth <= 0) { + return 0; + } + const cellWidth: number = plotWidth / totalCount; + let index: number = Math.floor((x - 1) / cellWidth); + index = Math.max(0, index); + index = Math.min(this.priceData.length - 1, index); + return index; + } + + private updateDescribeValues(index: number): void { + let targetIndex: number = index; + if (targetIndex < 0) { + targetIndex = this.getLastValidCurveIndex(); + } + if (targetIndex < 0 || targetIndex >= this.priceData.length || targetIndex >= this.fundData.length) { + const priceIndex: number = this.isDefaultMarketHeatStock() ? this.getLastValidPriceIndex() : -1; + if (priceIndex >= 0) { + const priceValue: number = this.priceData[priceIndex]; + this.displayPriceText = formatMarketHeatPrice(priceValue, this.decimal); + this.displayChangeText = this.getChangePercentText(priceValue); + this.displayPriceColor = getMarketHeatChangeColor(this.getChangePercentValue(priceValue)); + } else { + this.displayPriceText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayChangeText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayPriceColor = this.getNormalTextColor(); + } + this.displayFundText = MARKET_HEAT_CURVE_EMPTY_TEXT; + this.displayFundColor = this.getNormalTextColor(); + return; + } + const priceValue: number = this.priceData[targetIndex]; + const fundValue: number = this.fundData[targetIndex]; + this.displayPriceText = formatMarketHeatPrice(priceValue, this.decimal); + this.displayChangeText = this.getChangePercentText(priceValue); + this.displayPriceColor = getMarketHeatChangeColor(this.getChangePercentValue(priceValue)); + this.displayFundText = formatMarketHeatAmountWithSign(fundValue / this.fundValueScale); + this.displayFundColor = getMarketHeatValueColor(fundValue); + } + + private getLastValidCurveIndex(): number { + const length: number = Math.min(this.priceData.length, this.fundData.length); + for (let index = length - 1; index >= 0; index--) { + if (!isMarketHeatInvalidPriceNumber(this.priceData[index]) && + !isMarketHeatInvalidNumber(this.fundData[index])) { + return index; + } + } + return -1; + } + + private getLastValidPriceIndex(): number { + for (let index = this.priceData.length - 1; index >= 0; index--) { + if (!isMarketHeatInvalidPriceNumber(this.priceData[index])) { + return index; + } + } + return -1; + } + + private isDefaultMarketHeatStock(): boolean { + return this.stockCode === MARKET_HEAT_DEFAULT_CODE && this.stockMarket === MARKET_HEAT_DEFAULT_MARKET; + } + + private updateDefaultTotalFundValue(): void { + if (this.stockCode !== MARKET_HEAT_DEFAULT_CODE || this.stockMarket !== MARKET_HEAT_DEFAULT_MARKET) { + return; + } + const index: number = this.getLastValidCurveIndex(); + if (index < 0) { + return; + } + const value: number = this.fundData[index] / this.fundValueScale; + this.totalFundValue = value; + this.totalFundText = formatMarketHeatAmountWithSign(value); + this.totalFundColor = getMarketHeatValueColor(value); + } + + private getChangePercentText(priceValue: number): string { + const changePercent: number = this.getChangePercentValue(priceValue); + if (isMarketHeatInvalidNumber(changePercent)) { + return MARKET_HEAT_CURVE_EMPTY_TEXT; + } + return `${changePercent.toFixed(2)}%`; + } + + private getChangePercentValue(priceValue: number): number { + if (isMarketHeatInvalidPriceNumber(priceValue) || isMarketHeatInvalidPriceNumber(this.closePrice) || + this.closePrice === 0) { + return Number.MAX_SAFE_INTEGER; + } + return (priceValue - this.closePrice) / Math.abs(this.closePrice) * 100; + } +} + +interface MarketHeatCurveRange { + min: number; + max: number; +} + +function isMarketHeatInvalidNumber(value: number): boolean { + return value === Number.MIN_SAFE_INTEGER || value === Number.MAX_SAFE_INTEGER || !isFinite(value) || + isMarketHeatProtocolInvalidNumber(value); +} + +function isMarketHeatProtocolInvalidNumber(value: number): boolean { + return value === MARKET_HEAT_INVALID_INT32_MIN || + value === MARKET_HEAT_INVALID_INT32_MAX || + value === MARKET_HEAT_INVALID_INT32_OVERFLOW || + value === MARKET_HEAT_INVALID_NEGATIVE_INT32_MAX || + value === MARKET_HEAT_INVALID_UINT32_MAX; +} + +function isMarketHeatInvalidPriceNumber(value: number): boolean { + if (isMarketHeatInvalidNumber(value)) { + return true; + } + return value >= MARKET_HEAT_PRICE_OVERFLOW_MIN || value <= -MARKET_HEAT_PRICE_OVERFLOW_MIN; +} + +function formatMarketHeatAmount(value: number): string { + if (isMarketHeatInvalidNumber(value)) { + return MARKET_HEAT_CURVE_EMPTY_TEXT; + } + if (value === 0) { + return '0.0'; + } + const absValue: number = Math.abs(value); + if (absValue >= 100000000) { + return `${(value / 100000000).toFixed(2)}亿`; + } + if (absValue >= 10000) { + return `${(value / 10000).toFixed(2)}万`; + } + return value.toFixed(2); +} + +function formatMarketHeatAmountWithSign(value: number): string { + const text: string = formatMarketHeatAmount(value); + if (text === MARKET_HEAT_CURVE_EMPTY_TEXT) { + return text; + } + if (value > 0) { + return `+${text}`; + } + return text; +} + +function formatMarketHeatPrice(value: number, decimal: number): string { + if (isMarketHeatInvalidPriceNumber(value)) { + return MARKET_HEAT_CURVE_EMPTY_TEXT; + } + return value.toFixed(decimal); +} + +function formatMarketHeatTime(value: number): string { + if (value <= 0 || isMarketHeatInvalidNumber(value)) { + return ''; + } + const timeValue: number = Math.floor(value); + const hour: number = (timeValue & 0x000007c0) >> 6; + const minute: number = timeValue & 0x0000003f; + if (hour > 23 || minute > 59) { + return ''; + } + const hourText: string = hour < 10 ? `0${hour}` : `${hour}`; + const minuteText: string = minute < 10 ? `0${minute}` : `${minute}`; + return `${hourText}:${minuteText}`; +} + +function formatMarketHeatTradeMinute(value: number): string { + if (isMarketHeatInvalidNumber(value)) { + return ''; + } + const normalizedValue: number = ((Math.floor(value) % 1440) + 1440) % 1440; + const hour: number = Math.floor(normalizedValue / 60); + const minute: number = normalizedValue % 60; + const hourText: string = hour < 10 ? `0${hour}` : `${hour}`; + const minuteText: string = minute < 10 ? `0${minute}` : `${minute}`; + return `${hourText}:${minuteText}`; +} + +function getLastValidMarketHeatValue(source: Array | null | undefined): number | null { + if (!source) { + return null; + } + for (let index = source.length - 1; index >= 0; index--) { + const value: number = source[index]; + if (!isMarketHeatInvalidNumber(value)) { + return value; + } + } + return null; +} + +function getMarketHeatValueColor(value: number): string { + if (isMarketHeatInvalidNumber(value)) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_primary_text')); + } + if (value > 0) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_up_text')); + } + if (value < 0) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_down_text')); + } + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_primary_text')); +} + +function getMarketHeatChangeColor(value: number): string { + if (isMarketHeatInvalidNumber(value)) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_primary_text')); + } + if (value > 0.001) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_up_text')); + } + if (value < 0) { + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_down_text')); + } + return ColorUtils.resourceColorToRGBHex($r('app.color.market_heat_primary_text')); +} + +function isSupportMarketHeatMarket(market: string): boolean { + if (market.length === 0) { + return false; + } + return market === ZS.SP_THS || market === '128' || Markets.isGuoNeiMarket(market); +} + +@Component +struct MarketHeatList { + pageId: number = 0; + extRequestText: string = ''; + initialSortDataId: number = MARKET_HEAT_DEFAULT_SORT_ID; + initialSortStatus: number = SortStatus.DESC; + rowClickHandler?: (stock: MarketHeatSelectedStock) => void; + sortChangeHandler?: (dataId: number, sortStatus: number) => void; + @Prop windowWidthVp: number = 0; + @Prop @Watch('onSelectedStockChange') selectedStockCode: string = ''; + @Prop @Watch('onSelectedStockChange') selectedStockMarket: string = ''; + @Prop @Watch('onRequestNotifierChange') requestNotifier: number = 0; + @State header: RowData = new RowData([], [], new OriginalData()); + @State actualExtRequestText: string = ''; + @State tableTotalRowCount: number = -1; + @State activeSelectedStockCode: string = ''; + @State activeSelectedStockMarket: string = ''; + private headerConfig: Array = createMarketHeatHeaderConfig(); + + aboutToAppear() { + this.activeSelectedStockCode = this.selectedStockCode; + this.activeSelectedStockMarket = this.selectedStockMarket; + this.actualExtRequestText = this.createExtRequestText(); + this.header = buildHeaderRowData(this.headerConfig); + this.initSortState(); + } + + private createExtRequestText(): string { + if (this.pageId !== MARKET_HEAT_SELF_PAGE_ID) { + return this.extRequestText; + } + const codeList: Array = GetCodeList(); + const marketList: Array = GetMarketList(); + return `columnorder=${MARKET_HEAT_COLUMN_ORDER}\r\nselfstockcustom=1\r\nstocklist=${codeList.join('|')}\r\nmarketlist=${marketList.join('|')}`; + } + + private onRequestNotifierChange(): void { + this.actualExtRequestText = this.createExtRequestText(); + this.tableTotalRowCount = -1; + } + + private onSelectedStockChange(): void { + this.activeSelectedStockCode = this.selectedStockCode; + this.activeSelectedStockMarket = this.selectedStockMarket; + } + + private initSortState() { + this.header.forEachCell((cellData: CellData) => { + if (cellData.dataId === this.initialSortDataId) { + cellData.sortStatus = this.initialSortStatus; + if (SortStatus.isSorting(cellData.sortStatus)) { + this.header.currentSortCell = cellData; + } + } + }) + } + + build() { + Stack({ alignContent: Alignment.TopStart }) { + HQTableComponent({ + frameId: MARKET_HEAT_FRAME_ID, + pageId: this.pageId, + header: this.header, + headerConfig: this.headerConfig, + extRequestText: this.actualExtRequestText, + requestNotifier: this.requestNotifier, + showColCount: 4, + fixColWidth: this.getFixColWidth(), + headerRowHeight: 40, + rowHeight: 48, + tableBackgroundColor: $r('app.color.surface_layer1_foreground'), + tablePrimaryTextColor: $r('app.color.market_heat_primary_text'), + tableSecondaryTextColor: $r('app.color.market_heat_primary_text'), + tableUpTextColor: $r('app.color.market_heat_up_text'), + tableDownTextColor: $r('app.color.market_heat_down_text'), + tableThemedTagTextColor: $r('app.color.market_heat_themed_tag_text'), + tableRealtimeTagTextColor: $r('app.color.market_heat_realtime_tag_text'), + tableSelectedRowColor: $r('app.color.market_heat_selected_row'), + isSupportSelectedRowOverlay: this.pageId !== MARKET_HEAT_INDEX_PAGE_ID, + requestRowCount: 24, + isSupportHeaderShowMoreIcon: true, + headerCellRightPadding: 15, + headerCellLeftPadding: 0, + headerSortIconLeftMargin: 4, + headerTextFontSize: 14, + isMarketHeatContentStyle: true, + isSupportFlag: true, + subscribeIds: MARKET_HEAT_SUBSCRIBE_IDS, + selectedRowCode: this.activeSelectedStockCode, + selectedRowMarket: this.activeSelectedStockMarket, + onDataStatusChange: (totalRowCount: number, visibleRowCount: number) => { + this.onTableDataStatusChange(totalRowCount, visibleRowCount) + }, + onSortChange: (dataId: number, sortStatus: number) => { + this.sortChangeHandler?.(dataId, sortStatus) + }, + rowClickHandler: (rowData: RowData, index: number) => { + this.handleRowClick(rowData, index) + } + }) + + if (this.shouldShowSelfEmptyView()) { + this.buildSelfEmptyView() + } else if (this.shouldShowCommonEmptyView()) { + this.buildCommonEmptyView() + } + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildSelfEmptyView() { + Column() { + Blank() + .height(60) + Row() { + Column() { + Blank() + .height(88) + Text('暂无自选合约') + .fontSize(12) + .fontWeight(FontWeight.Medium) + .fontColor($r('app.color.market_heat_self_empty_text')) + .textAlign(TextAlign.Center) + .maxLines(1) + } + .width(150) + .height(105) + } + .width('100%') + .justifyContent(FlexAlign.Center) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .onClick(() => { + this.onSelfEmptyClick() + }) + } + + @Builder + private buildCommonEmptyView() { + Column() { + Blank() + .layoutWeight(1) + Text('暂无期货合约') + .fontSize(14) + .fontColor($r('app.color.market_heat_empty_text')) + .textAlign(TextAlign.Center) + .maxLines(1) + Blank() + .layoutWeight(1) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .hitTestBehavior(HitTestMode.Transparent) + } + + private shouldShowSelfEmptyView(): boolean { + if (this.pageId !== MARKET_HEAT_SELF_PAGE_ID) { + return false; + } + return GetCodeList().length <= 0; + } + + private shouldShowCommonEmptyView(): boolean { + if (this.pageId === MARKET_HEAT_SELF_PAGE_ID) { + return false; + } + return this.tableTotalRowCount === 0; + } + + private onTableDataStatusChange(totalRowCount: number, visibleRowCount: number): void { + this.tableTotalRowCount = totalRowCount; + } + + private onSelfEmptyClick(): void { + let iEventData: IEventData = { data: {} }; + iEventData.data[EmitterConstants.EVENT_DATA_KEY_TAB_INDEX] = TabManagerConstants.INDEX_TAB_INDEX_QUOTE_PAGE; + iEventData.data[EmitterConstants.EVENT_DATA_KEY_HQ_PAGE_SUB_TAB_INDEX] = HQPageConstants.HQ_PAGE_INDEX_GUO_NEI_PAGE; + emitter.emit({ eventId: EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX }, iEventData); + router.back({ + url: 'pages/Index' + }); + } + + private getFixColWidth(): number { + if (this.windowWidthVp > 0 && this.windowWidthVp <= 320) { + return 90; + } + if (this.windowWidthVp === 0 || this.windowWidthVp <= 375) { + return 105; + } + return 116; + } + + private handleRowClick(rowData: RowData, index: number): void { + if (!this.rowClickHandler) { + return; + } + const stock: MarketHeatSelectedStock = { + code: this.getRowCellData(rowData, TableConstants.DATA_ID_CODE_4), + market: this.getRowCellData(rowData, TableConstants.DATA_ID_MARKET), + name: this.getRowCellData(rowData, TableConstants.DATA_ID_NAME), + fund: this.getRowCellData(rowData, MARKET_HEAT_DEFAULT_SORT_ID), + price: this.getRowCellData(rowData, TableConstants.DATA_ID_PRICE), + changePercent: this.getRowCellData(rowData, TableConstants.DATA_ID_ZF) + }; + if (stock.market.length === 0) { + stock.market = this.getRowCellData(rowData, TableConstants.DATA_ID_MARKET_OLD); + } + if (stock.code.length === 0 || stock.market.length === 0) { + return; + } + if (!isSupportMarketHeatMarket(stock.market)) { + promptAction.showToast({ message: '该合约不支持查看资金曲线图' }); + return; + } + if (stock.code === this.activeSelectedStockCode) { + return; + } + this.activeSelectedStockCode = stock.code; + this.activeSelectedStockMarket = stock.market; + this.rowClickHandler(stock); + } + + private getRowCellData(rowData: RowData, id: number): string { + return rowData.originalData.get(id)?.toString() ?? ''; + } +} + +function createMarketHeatHeaderConfig(): Array { + return [ + { + mobileid: TableConstants.DATA_ID_NAME, + name: '代码名称', + isFix: true, + sortable: false, + extDataIds: [ + TableConstants.DATA_ID_CODE_4, + 126, + TableConstants.DATA_ID_MARKET_OLD, + TableConstants.DATA_ID_MARKET + ] + }, + { + mobileid: MARKET_HEAT_DEFAULT_SORT_ID, + name: '资金流向', + sortable: true + }, + { + mobileid: 67, + name: '沉淀资金', + sortable: true + }, + { + mobileid: TableConstants.DATA_ID_PRICE, + name: '最新', + sortable: true, + isDamping: true + }, + { + mobileid: TableConstants.DATA_ID_ZF, + name: '涨幅', + sortable: true + }, + { + mobileid: TableConstants.DATA_ID_ZD, + name: '涨跌', + sortable: true + } + ]; +} + +const MARKET_HEAT_SUBSCRIBE_IDS: number[] = [ + TableConstants.DATA_ID_NAME, + MARKET_HEAT_DEFAULT_SORT_ID, + 67, + TableConstants.DATA_ID_PRICE, + TableConstants.DATA_ID_ZF, + TableConstants.DATA_ID_ZD, + TableConstants.DATA_ID_CODE_4, + 126, + TableConstants.DATA_ID_MARKET_OLD, + TableConstants.DATA_ID_MARKET +]; diff --git a/future_entry_demo/src/main/ets/pages/login/AccountSecurityPage.ets b/future_entry_demo/src/main/ets/pages/login/AccountSecurityPage.ets new file mode 100644 index 0000000..46cfa88 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/login/AccountSecurityPage.ets @@ -0,0 +1,33 @@ +import { AccountSecurityComponent } from 'biz_login/src/main/ets/pages/AccountSecurityComponent'; +import { common } from '@kit.AbilityKit'; +import { window } from '@kit.ArkUI'; +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +export struct AccountSecurityPage { + //可见性 + @State isVisible: boolean = false; + @State barHeight: number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + + onPageShow(): void { + this.isVisible = true; + } + + onPageHide(): void { + this.isVisible = false; + } + + + build() { + Column() { + Row().width('100%').height(px2vp(GlobalContext.statusBarHeight)).backgroundColor($r('app.color.start_window_background')).flexShrink(0) + AccountSecurityComponent(); + } + .constraintSize({ + minHeight: 500 + }) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/login/CheckNumberPage.ets b/future_entry_demo/src/main/ets/pages/login/CheckNumberPage.ets new file mode 100644 index 0000000..f749291 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/login/CheckNumberPage.ets @@ -0,0 +1,53 @@ +import { router, window } from '@kit.ArkUI'; +import { common } from '@kit.AbilityKit'; +import { CheckNumberRouter, CheckNumberPage as MobileAuthPage } from 'biz_login'; +import { GlobalContext } from 'biz_common'; + +/** + * 登录验证码承接页面 + * @author menghuiyuan@myhexin.com + */ +@Entry +@Component +export struct CheckNumberPage { + //可见性 + @State isVisible: boolean = false; + @State barHeight: number = 0; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + + onPageShow(): void { + this.isVisible = true; + } + + onPageHide(): void { + this.isVisible = false; + } + + + private getCheckObj():CheckNumberRouter{ + let checkObj = router.getParams() as CheckNumberRouter; + if (!checkObj) { + checkObj = new CheckNumberRouter(86, '',''); + } + return checkObj; + } + + build() { + Scroll() { + Column() { + Row().width('100%').height(px2vp(GlobalContext.statusBarHeight)).backgroundColor($r('app.color.surface_layer1_foreground')).flexShrink(0) + MobileAuthPage( + { + barHeight: GlobalContext.statusBarHeight, + isVisible: this.isVisible, + checkObj: this.getCheckObj() + }); + } + .constraintSize({ + minHeight:500 + }) + } + .scrollBar(BarState.Off) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/login/LoginMainPage.ets b/future_entry_demo/src/main/ets/pages/login/LoginMainPage.ets new file mode 100644 index 0000000..a6b6d71 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/login/LoginMainPage.ets @@ -0,0 +1,58 @@ +import { LoginMainPage as LoginMainPage} from 'biz_login' +import { window } from '@kit.ArkUI' +import { common } from '@kit.AbilityKit'; + +@Entry +@Component +export struct LoginHomePage { + + //顶部栏撑起高度 + @State barHeight: number = 0; + //可见状态 + @State isVisible: boolean = false; + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; + + async setSystemBar() { + // 获取当前应用窗口 + let windowClass: window.Window = await window.getLastWindow(this.context) + // 获取状态栏高度 + this.barHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height + // 将状态栏和导航栏的背景色设置为跟应用窗口相同的颜色 + await windowClass.setWindowSystemBarProperties({ + navigationBarColor: "#00000000", + statusBarColor: "#00000000", + navigationBarContentColor: "#FFFFFFFF", + statusBarContentColor: "#FFFFFFFF" + }) + } + + build() { + Scroll () { + Column() { + LoginMainPage( { isVisible : this.isVisible }) + } + .constraintSize({ + minHeight:800 + }) + + } + .scrollBar(BarState.Off) + + } + + aboutToAppear(): void { + + } + + aboutToDisappear(): void { + + } + + onPageShow(): void { + this.isVisible = true; + } + + onPageHide(): void { + this.isVisible = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/login/LoginPage.ets b/future_entry_demo/src/main/ets/pages/login/LoginPage.ets new file mode 100644 index 0000000..99a0283 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/login/LoginPage.ets @@ -0,0 +1,71 @@ +import {LoginPage as LoginFirstLevelPage, LoginPageRouter} from 'biz_login' +import router from '@ohos.router' +import { window } from '@kit.ArkUI' +import { common } from '@kit.AbilityKit' +import { GlobalContext, HXLog } from 'biz_common'; +import { L2DowngradeManager } from 'base_mobilepush'; +import { AuthCallbackWrapper, AuthEntity, LoginParamHelper, LoginType } from 'biz_auth'; +import { AccountProvider } from 'biz_trade'; +import { HXUserService } from 'biz_hxservice'; +import { emitter } from '@kit.BasicServicesKit'; +import { QuitAccountEvent } from 'biz_webview/src/main/ets/WebViewJavaScriptBridge/JSEvent/User/QuitAccountEvent'; + +@Entry +@Component +struct LoginPage { + //当前页面是否处于可见,用于传递给下层组件 + @State isVisible : boolean = false; + @State barHeight: number = 0; + @State flexSwitchFlag : boolean = false; + @Provide loginSourcePage:string = '' + context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext + + onPageShow(): void { + this.isVisible = true; + } + + onPageHide(): void { + this.isVisible = false; + } + + aboutToAppear() { + let routerParam = router.getParams() as LoginPageRouter; + if (routerParam) { + this.flexSwitchFlag = routerParam.flexSwitchFlag; + this.loginSourcePage = routerParam.sourcePage + } + this.quitAccountIfNeed() + } + + private quitAccountIfNeed(){ + if (L2DowngradeManager.getInstance().isDownGrade()) { + //降档后重新登陆登陆前需要退出账号,否则level sid权限能刷新出来, + // 但行情权限没有刷回来,此方案也会导致如果用户进入登陆页面不登陆返回就变成了临时账号 + LoginParamHelper.loginQuitAccount(new AuthCallbackWrapper( + (type : LoginType, isAuthSuccess : boolean, errCode : number, errMsg : string, authEty? : AuthEntity)=> { + if (isAuthSuccess) { + AccountProvider.getInstance().clearCurrentThsUserAllWeituoData() + HXUserService.getInstance().setMobileNum("") + emitter.emit({ + eventId: QuitAccountEvent.USER_LOGOUT, + priority: emitter.EventPriority.IMMEDIATE + }) + + } else { + HXLog.e("QuitAccountEvent","退出账号失败!") + } + })) + } + } + build() { + Scroll(){ + Column() { + Row().width('100%').height(px2vp(GlobalContext.statusBarHeight)).backgroundColor($r('app.color.surface_layer1_foreground')).flexShrink(0) + LoginFirstLevelPage ( { isVisible : this.isVisible }) + } + + } + .scrollBar(BarState.Off) + + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/option/OptionPage.ets b/future_entry_demo/src/main/ets/pages/option/OptionPage.ets new file mode 100644 index 0000000..14c7dcc --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/option/OptionPage.ets @@ -0,0 +1,148 @@ +import { + TQuote, + OptionDataAnalysisPage, + OptionStrategyAnalysisPage, + Contract, + Variety, + ChooseUtils, + CommonConstants +} from "biz_option"; +import { emitter } from "@kit.BasicServicesKit"; +import { EmitterConstants } from "biz_common"; +import { router } from "@kit.ArkUI"; + +@Component +export struct OptionPage { + private frameId: number = 2405; + private HQ_OPTION_TABS = ['T型报价', '策略分析', '数据分析']; + @Consume @Watch('onTopIndexChange') optionPageL3TabIndex: number; + @Prop @Watch('onShowStatusChange') onForeground: boolean; + private swiperController = new SwiperController(); + // 状态提升:统一管理品种、合约、合约列表 + @State currentVariety: Variety = new Variety(); + @State currentContract: Contract = new Contract(); + @State contracts: Contract[] = []; + + aboutToAppear(): void { + this.onTopIndexChange() + // 首次从分时页面跳转到T型报价,组件还未创建,监听不生效,需要获取路由数据进行切换合约 + const param = router.getParams() as emitter.EventData + this.onGotoOption(param) + ChooseUtils.getInstance().requestVariety(this.frameId, (model) => { + this.currentVariety = model.currentVariety + this.currentContract = model.currentContract + this.contracts = model.contracts + }, { currentVarietyId: this.currentVariety.key }) + // 跳转至行情-期权页面监听 + emitter.on({ eventId: EmitterConstants.EVENT_DATA_KEY_CHANGE_VARIETY_KEY_VALUE }, this.onGotoOption) + } + + aboutToDisappear(): void { + ChooseUtils.getInstance().removeVarietyRequest() + emitter.off(EmitterConstants.EVENT_DATA_KEY_CHANGE_VARIETY_KEY_VALUE, this.onGotoOption) + } + + onTopIndexChange() { + this.swiperController.changeIndex(this.optionPageL3TabIndex); + } + + onShowStatusChange() { + } + + private onGotoOption: (eventData: emitter.EventData) => void = (eventData: emitter.EventData) => { + if (eventData && eventData.data) { + let id: string | undefined = eventData.data[CommonConstants.CURRENT_VARIETY_ID]; + let currentContract: string | undefined = eventData.data[CommonConstants.CURRENT_CONTRACT]; + let currentMarketId: string | undefined = eventData.data[CommonConstants.CURRENT_MARKET_ID]; + if (id && id != "") { + const model = ChooseUtils.getInstance().changeVariety(id, currentContract, currentMarketId) + this.currentVariety = model.currentVariety + this.currentContract = model.currentContract + this.contracts = model.contracts + } + } + } + + build() { + Flex({ direction: FlexDirection.Column }) { + this.buildOptionTitle() + this.buildOptionContent() + } + .layoutWeight(1) + .backgroundColor($r('app.color.surface_layer1_background')) + } + + @Builder + private buildOptionContent() { + Swiper(this.swiperController) { + TQuote({ + frameId: this.frameId, + onForeground: this.onForeground && this.optionPageL3TabIndex === 0, + currentVariety: this.currentVariety, + currentContract: this.currentContract, + contracts: this.contracts, + }) + OptionStrategyAnalysisPage({ + frameId: this.frameId, + onForeground: this.onForeground && this.optionPageL3TabIndex === 1, + currentVariety: this.currentVariety, + currentContract: this.currentContract, + contracts: this.contracts + }) + OptionDataAnalysisPage({ + frameId: this.frameId, + onForeground: this.onForeground && this.optionPageL3TabIndex === 2, + currentVariety: this.currentVariety, + currentContract: this.currentContract, + contracts: this.contracts + }) + } + .index(this.optionPageL3TabIndex) + .layoutWeight(1) + .width('100%') + .indicator(false) + .disableSwipe(true) + .backgroundColor('#fff') + .margin({ top: 8 }) + } + + @Builder + private buildOptionTitle() { + Column() { + Row() { + ForEach(this.HQ_OPTION_TABS, (item: string, index: number) => { + this.buildSingleTabTitle(index) + }) + } + .width('100%') + .alignSelf(ItemAlign.Start) + .justifyContent(FlexAlign.SpaceBetween) + .margin({ top: 10 }) + .padding({ left: 16, right: 16 }) + } + .height(40) + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildSingleTabTitle(index: number) { + Column() { + Text(this.HQ_OPTION_TABS[index]) + .fontColor((this.optionPageL3TabIndex === index) ? $r('app.color.elements_text_primary_02') : + $r('app.color.elements_text_secondary')) + .fontSize(16) + .fontWeight((this.optionPageL3TabIndex === index) ? FontWeight.Medium : FontWeight.Normal) + .onClick(() => { + this.optionPageL3TabIndex = index; + }) + if (this.optionPageL3TabIndex === index) { + Text('') + .width(12) + .height(3) + .borderRadius(1) + .backgroundColor('#2E58EF') + } + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/overview/OverViewPage.ets b/future_entry_demo/src/main/ets/pages/overview/OverViewPage.ets new file mode 100644 index 0000000..c819969 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/overview/OverViewPage.ets @@ -0,0 +1,44 @@ +import { WebViewComponent, CustomWebviewController, NavigationBarViewModel } from 'biz_webview'; +import { handleBackEvent } from 'biz_common/src/main/ets/pc/PCAdapter'; +import { UnifyUrlManager } from '@b2c-f/url-modify-config'; + +@Component +export struct OverViewPage { + private webViewController: CustomWebviewController = new CustomWebviewController() + @Prop @Watch('onShowStatusChange') onForeground: boolean; + private navigationBarHandler: NavigationBarViewModel = new NavigationBarViewModel(); + private isLoadFinish:boolean = false + + onShowStatusChange() { + if (this.onForeground) { + if (!this.isLoadFinish && this.webViewController.isAttached) { + this.webViewController.loadUrl(UnifyUrlManager.getRealUrl('fu_marketoverview_url')) + } + } + } + + build() { + Column() { + WebViewComponent({ + urlString: UnifyUrlManager.getRealUrl('fu_marketoverview_url'), + onShow: this.onForeground, + webViewController: this.webViewController, + navigationBarHandler: this.navigationBarHandler, + onPageLoadFinished:(event)=>{ + if (event.eventType == "pageFinished") { + this.isLoadFinish = true + } + }, + overScrollMode: OverScrollMode.NEVER, + }) + .margin({ top: 0 }) + .layoutWeight(1) + } + .onKeyEvent((event: KeyEvent) => { + handleBackEvent(event) + }) + .height('100%') + .width('100%') + .id('biz_webview/container') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/pdf/PDFViewer.ets b/future_entry_demo/src/main/ets/pages/pdf/PDFViewer.ets new file mode 100644 index 0000000..dc2e36d --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/pdf/PDFViewer.ets @@ -0,0 +1,235 @@ +import { pdfService, pdfViewManager, PdfView } from '@kit.PDFKit' +import promptAction from '@ohos.promptAction' +import fs from '@ohos.file.fs' +import { HXLog } from 'biz_common' +import window from '@ohos.window' +import common from '@ohos.app.ability.common' +import display from '@ohos.display' +import { BusinessError } from '@kit.BasicServicesKit' +import { router } from '@kit.ArkUI' +import { StatusBar } from '../../widget/StatusBar' + + +const TAG = 'PDFViewer' + + +interface RouterParams { + filePath: string + title: string +} +@Builder +export function PDFViewerBuilder() { + PDFViewer() +} + +@Entry +@Component +export struct PDFViewer { + @State private filePath: string = '' + @State private title: string = ''; + @State private isLandscape: boolean = false + @State private showToolbar: boolean = true + @State private currentPage: number = 1; + @State private totalPages: number = 0; + @State private showPagination: boolean = false; + private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController() + private context = getContext(this) as common.UIAbilityContext + private windowClass: window.Window | null = null + private delayTimer: number = 0; + + aboutToAppear() { + const params = router.getParams() as RouterParams + if (params) { + this.filePath = params.filePath ?? ''; + this.title = params.title.length > 0 ? params.title : '公告详情'; + } + this.loadDocument() + this.setupOrientationListener() + this.setupController(); + } + + aboutToDisappear() { + if (this.windowClass) { + // 恢复默认方向 + this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT) + .catch((err: BusinessError) => { + HXLog.e(TAG, `Failed to reset orientation: ${err?.message || err}`); + }) + this.clearOrientationListener() + } + } + + private displayChange = (orientation: number) => { + this.isLandscape = orientation === 90 || orientation === 270 + // 在横屏模式下自动隐藏工具栏 + this.showToolbar = !this.isLandscape + } + + private async setupOrientationListener() { + try { + // 获取当前窗口 + const mainWindow = await this.context.windowStage.getMainWindow() + this.windowClass = mainWindow + + // 设置支持的屏幕方向 + await this.windowClass.setPreferredOrientation(window.Orientation.AUTO_ROTATION) + + // 监听屏幕旋转 + display.on('change', this.displayChange) + + } catch (err) { + HXLog.e(TAG, `Failed to setup window orientation: ${err?.message || err}`); + } + } + + private clearOrientationListener() { + display.off('change', this.displayChange) + } + + private setupController() { + this.controller.registerPageChangedListener((page: number) => { + // 默认是0开始的,所以需要+1 + this.currentPage = page + 1; + }); + this.controller.registerScrollListener((scrollParam: pdfViewManager.ScrollParam) => { + this.showPagination = true; + this.clearTimer(); + this.delayTimer = setTimeout(() => { + this.showPagination = false; + }, 2000); + }); + } + + private clearTimer() { + if (this.delayTimer) { + clearTimeout(this.delayTimer); + this.delayTimer = 0; + } + } + + async loadDocument() { + if (!this.filePath) { + HXLog.e(TAG, 'Invalid file path'); + this.showErrorToast('无效的文件路径') + return + } + + try { + // 检查文件是否存在 + try { + const stat = await fs.stat(this.filePath) + } catch (err) { + HXLog.e(TAG, `File does not exist: ${err}`); + this.showErrorToast('PDF文件不存在') + return + } + + // 尝试加载文档 + await this.controller.loadDocument(this.filePath) + this.totalPages = this.controller.getPageCount(); + } catch (err) { + HXLog.e(TAG, `Failed to load PDF: ${err}`); + this.showErrorToast('无法加载PDF文件') + } + } + + private showErrorToast(message: string) { + promptAction.showToast({ + message: message, + duration: 2000, + bottom: 50 + }) + setTimeout(() => { + router.back() + }, 2000) + } + + @Builder + ToolBar() { + Column() { + StatusBar() + RelativeContainer() { + Image($r('app.media.icon_arrow_back_24')) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(24) + .height(24) + .alignRules({ + center: { anchor: "__container__", align: VerticalAlign.Center }, + left: { anchor: "__container__", align: HorizontalAlign.Start } + }) + .margin({ left: 16 }) + .onClick(() => { + router.back() + }) + .id('left') + Text(this.title) + .fontSize(19) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: '70%' }) + .alignRules({ + center: { anchor: "__container__", align: VerticalAlign.Center }, + middle: { anchor: "__container__", align: HorizontalAlign.Center } + }) + } + .height(44).backgroundColor($r("app.color.surface_layer1_foreground")).width('100%') + } + .animation({ + duration: 250, + curve: Curve.EaseInOut + }) + } + + @Builder + Pagination() { + Stack() { + Image($r('app.media.pdf_pagination')) + .width('100%') + .height('100%') + + Text(`${this.currentPage}/${this.totalPages}`) + .fontSize(13) + .fontColor($r('app.color.common_theme_323232')) + .padding({ left: 4, right: 4}) + } + .width(43) + .height(32) + .position({ x: 10, y: 10 }) + } + + build() { + NavDestination() { + Column() { + if (this.showToolbar) { + this.ToolBar() + } + Stack({ alignContent: Alignment.Center }) { + PdfView({ + controller: this.controller, + pageFit: this.isLandscape ? pdfService.PageFit.FIT_HEIGHT : pdfService.PageFit.FIT_WIDTH + }) + .width('100%') + .height('100%') + .margin({ top: this.showToolbar ? 8 : 0 }) + .backgroundColor('#F5F5F5') + .onClick(() => { + if (this.isLandscape) { + this.showToolbar = !this.showToolbar + } + }) + if (this.showPagination) { + this.Pagination(); + } + } + .layoutWeight(1) + .width('100%') + } + .width('100%') + .height('100%') + .backgroundColor(Color.White) + } + .hideTitleBar(true) + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/ChartSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/ChartSettingPage.ets new file mode 100644 index 0000000..ba7ebae --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/ChartSettingPage.ets @@ -0,0 +1,553 @@ +/** + * 图表设置页面 + * 复刻iOS LJFSKLSettingViewController + * + * Created by LY on 2026/01/04. + * Copyright © 2026 hexin. All rights reserved. + */ + +import { PageTitleBar } from 'biz_common' +import { + ChartSettingView, + KLineStyle, + QuoteCustomSettingManager, + SettingConfigManager, + SettingHelper, + SettingItemId, + KLineInteractionType, + QuoteSettingField, + SettingDataSource, + StandardPriceTypeHelper, + StockInfo, + LatestPriceAnnotationSettingDialog +} from 'biz_quote' +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' +import { SettingConfigGroupEnum } from 'biz_quote/src/main/ets/components/widget/setting/model/SettingConfigModel' +import { BaseSettingItem } from 'biz_quote/src/main/ets/components/widget/setting/model/BaseSettingItem' +import { SettingItemSwitch } from 'biz_quote/src/main/ets/components/widget/setting/model/SettingItemSwitch' +import { BSPointAlertDialog, BSPointAlertType } from 'biz_trade/src/main/ets/pages/trademanager/BSPointAlertDialog' +import { KLineBSPointDataService } from 'biz_trade/src/main/ets/service/bspoint/KLineBSPointDataService' +import { TimelineBSPointDataService, BSPointUserAgreeStatus, TradeSettingEvents } from 'biz_trade' +import { SettingItemForward } from 'biz_quote/src/main/ets/components/widget/setting/model/SettingItemForward' +import { getResourceString } from '@b2c/first_page/src/main/ets/util/ResourceUtil' +import { AppUtil } from '@pura/harmony-utils' +import { emitter } from '@kit.BasicServicesKit' + +@Entry +@Component +export struct ChartSettingPage { + @Provide pageShow: boolean = false + @State currentSwiperIndex: number = 0 + @State @Watch('onTitleLocationChanged') titleLocationArray: Map = new Map() + @State tabMargin: number = 0 + @State totalTabWidth: number = 0 + @State tabContentWidth: number = 0 + @State commonSections: BaseSettingItem[] = [] + @State klineSections: BaseSettingItem[] = [] + @State fenshiSections: BaseSettingItem[] = [] + @State dataVersion: number = 0 + private titleArray = ['通用设置', 'K线设置', '分时设置']; + private swiperController = new SwiperController() + private jumpActions = new Map void>() + private duration = 300 + private stockInfo: StockInfo = new StockInfo('', '') + // 需要监听变化的配置项ID列表 + private watchedConfigItemIDList: string[] = [] + private configItemChangedObserver = (configItemId: string): void => { + this.refreshSections() + } + private onSettingUpdate = (): void => { + this.refreshSections() + } + + private shouldShowKLineBSPointAlert: boolean = false; + private latestPriceAnnotationDialogController?: CustomDialogController | null + + // 分时成交点说明弹窗控制器 + private timeLineBSPointDialogController: CustomDialogController = new CustomDialogController({ + builder: BSPointAlertDialog({ + params: { + alertType: BSPointAlertType.timeLine, + onConfirm: () => { + }, + onCancel: () => { + TimelineBSPointDataService.updateTimelineBSPointSwitchStatus(BSPointUserAgreeStatus.Undetermined) + // 延迟执行刷新,确保 @State 变量更新完成 + setTimeout(() => { + this.loadSections() + }, 0) + } + } + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + // K线成交点说明弹窗控制器 + private kLineBSPointDialogController: CustomDialogController = new CustomDialogController({ + builder: BSPointAlertDialog({ + params: { + alertType: BSPointAlertType.kLine, + onConfirm: () => { + KLineBSPointDataService.updateSwitchStatus(BSPointUserAgreeStatus.Agreed, (success: boolean) => { + if (!success) { + // 签约失败,延迟刷新确保状态更新完成 + setTimeout(() => { + this.loadSections() + }, 0) + } + }) + }, + onCancel: () => { + KLineBSPointDataService.updateSwitchStatus(BSPointUserAgreeStatus.Undetermined) + // 延迟执行刷新,确保 @State 变量更新完成 + setTimeout(() => { + this.loadSections() + }, 0) + }, + onProtocolClick: () => { + this.shouldShowKLineBSPointAlert = true + } + } + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }) + + onPageShow(): void { + this.pageShow = true + this.refreshSections() + if (this.shouldShowKLineBSPointAlert) { + this.kLineBSPointDialogController.open() + this.shouldShowKLineBSPointAlert = false + } + } + + onPageHide(): void { + this.pageShow = false + } + + aboutToAppear(): void { + this.initStockInfoFromParams() + this.loadSections() + this.addConfigItemChangedObserver() + emitter.on({ eventId: TradeSettingEvents.SETTING_UPDATE }, this.onSettingUpdate) + } + + aboutToDisappear(): void { + this.removeConfigItemChangedObserver() + this.latestPriceAnnotationDialogController?.close() + this.latestPriceAnnotationDialogController = null + emitter.off(TradeSettingEvents.SETTING_UPDATE, this.onSettingUpdate) + } + + /** + * 设置观察者,监听配置项变更 + */ + private addConfigItemChangedObserver(): void { + this.watchedConfigItemIDList.forEach(setID => { + QuoteCustomSettingManager.getInstance().subscribeToConfigChange(setID, this.configItemChangedObserver) + }) + } + + private removeConfigItemChangedObserver() { + this.watchedConfigItemIDList.forEach(setID => { + QuoteCustomSettingManager.getInstance().unsubscribeToConfigChange(setID, this.configItemChangedObserver) + }) + } + + private initStockInfoFromParams(): void { + const stockInfo = router.getParams() as StockInfo + if (stockInfo) { + this.stockInfo = stockInfo + } + } + + /** + * 刷新所有分组数据 + */ + private refreshSections(): void { + this.commonSections = SettingConfigManager.getInstance().getSettingSections(SettingConfigGroupEnum.COMMON, this.jumpActions) + this.klineSections = SettingConfigManager.getInstance().getSettingSections(SettingConfigGroupEnum.KLINE, this.jumpActions) + this.fenshiSections = SettingConfigManager.getInstance().getSettingSections(SettingConfigGroupEnum.FENSHI, this.jumpActions) + this.refreshItemsDataIfNeeded() + this.dataVersion++ + } + + private refreshItemsDataIfNeeded() { + this.refreshKlineStyleItem() + this.refreshStandardPriceItem() + } + + private refreshKlineStyleItem() { + const klineStyle = QuoteCustomSettingManager.getInstance().getSetting(SettingItemId.KLINE_STYLE_SET, KLineStyle.Default) as number + let klineStyleName = "普通K线" + switch (klineStyle) { + case KLineStyle.Solid: { + klineStyleName = "实心K线" + break + } + case KLineStyle.USA: { + klineStyleName = "美国线" + break + } + case KLineStyle.Tower: { + klineStyleName = "宝塔线" + break + } + case KLineStyle.Close: { + klineStyleName = "收盘价线" + break + } + default: + break + } + + this.configItemShowName('K线样式',klineStyleName) + + let klineScrollName = AppUtil.getContext().resourceManager.getStringSync(this.getOptionText(this.getInteractionType())) + + this.configItemShowName(AppUtil.getContext().resourceManager.getStringSync($r('app.string.kline_interaction_setting_title')),klineScrollName) + + } + + private configItemShowName(itemName:string,subTitleShowName: string) { + const filtered = this.klineSections.filter(item => item.name == itemName ) + if (filtered.length > 0) { + const item = filtered[0] + item.subTitle = subTitleShowName + } + } + + /** + * 获取K线滑动交互类型设置 + * @returns KLineInteractionType 0=切换周期, 1=K线滚动 + */ + private getInteractionType(): KLineInteractionType { + return SettingHelper.getSetting( + QuoteSettingField.INTERACTION_TYPE, + KLineInteractionType.PeriodChange, + SettingDataSource.QUOTE_CLOUD + ) as KLineInteractionType + } + + /** + * 根据交互类型获取显示文字 + */ + private getOptionText(type: KLineInteractionType): Resource { + if (type === KLineInteractionType.PeriodChange) { + return $r('app.string.kline_interaction_period_change') + } + return $r('app.string.kline_interaction_kline_scroll') + } + + private refreshStandardPriceItem() { + const standardPriceDisplayName = + StandardPriceTypeHelper.displayName(QuoteCustomSettingManager.getInstance().getStandardPriceType()) + for (let item of this.commonSections) { + if (item instanceof SettingItemForward && item.name == '涨跌计算基准价设置') { + item.rightText = standardPriceDisplayName + break + } + } + } + + onTitleLocationChanged() { + if (this.tabMargin === 0) { + this.tabMargin = this.getTabMargin(this.currentSwiperIndex) + } + } + + private changeIndicatorMargin(index: number, event: TabsAnimationEvent) { + let nextIndex = index + if (index > 0 && event.currentOffset > 0) { + nextIndex-- + } else if (index < this.titleArray.length && event.currentOffset < 0) { + nextIndex++ + } + + let currentMargin = this.getTabMargin(index) + let nextMargin = this.getTabMargin(nextIndex) + let swipeRatio = Math.abs(event.currentOffset / this.totalTabWidth) + + this.tabMargin = currentMargin + (nextMargin - currentMargin) * swipeRatio + } + + private getTabMargin(index: number): number { + const title = this.titleArray[index] + const location = this.titleLocationArray.get(title) + if (location) { + return location - this.tabContentWidth / 2 + } + return 0 + } + + private startAnimateTo(duration: number, leftMargin: number) { + animateTo({ + duration: duration, // 动画时长 + curve: Curve.Linear, // 动画曲线 + iterations: 1, // 播放次数 + playMode: PlayMode.Normal, // 动画模式 + }, () => { + this.tabMargin = leftMargin + }) + } + + /** + * 加载设置分组数据 + */ + private loadSections(): void { + const manager = SettingConfigManager.getInstance() + + // 先加载JSON配置(如果还没加载的话) + if (!manager.isConfigLoaded()) { + const context = getContext(this) + manager.loadConfigSync(context) + } + + // 创建跳转回调Map + const jumpActions = new Map void>() + + // 指标设置 - 跳转到指标设置页面 + jumpActions.set(SettingItemId.TECH_SET, () => { + router.pushUrl({ url: 'pages/quote/IndicatorManagerPage' }) + }) + + // 常用周期设置 - 跳转到周期设置页面 + jumpActions.set(SettingItemId.CUSTOM_PERIOD_SETTING, () => { + router.pushUrl({ + url: 'pages/quote/CustomPeriodSettingPage', + params: this.stockInfo + }) + }) + + // 图表标注显示 - 跳转到图表标注设置页面 + jumpActions.set(SettingItemId.CHART_LABEL_SET, () => { + // TODO: 跳转到图表标注设置页面 + console.info('跳转到图表标注设置页面') + }) + + // 多周期同列设置 - 跳转到多周期设置页面 + jumpActions.set(SettingItemId.MULTIPLE_PERIOD_SET, () => { + // TODO: 跳转到多周期同列设置页面 + console.info('跳转到多周期同列设置页面') + }) + + // 涨跌基准价设置 - 跳转到涨跌基准价设置页面 + jumpActions.set(SettingItemId.STANDARD_PRICE_SET, () => { + router.pushUrl({ url: 'pages/quote/StandardPriceSettingPage' }) + }) + + // 最新价标注 - 底部弹窗选择 + jumpActions.set(SettingItemId.LATEST_PRICE_LINE_SET, () => { + this.showLatestPriceAnnotationDialog() + }) + + // K线滑动设置 - 跳转到K线滑动设置页面 + jumpActions.set(SettingItemId.KLINE_INTERACTION, () => { + router.pushUrl({ url: 'pages/quote/KLineInteractionSettingPage' }) + }) + + // K线样式 - 跳转到K线样式设置页面 + jumpActions.set(SettingItemId.KLINE_STYLE_SET, () => { + router.pushUrl({ url: 'pages/quote/KlineStyleSettingPage' }) + console.info('跳转到K线样式设置页面') + }) + this.jumpActions = jumpActions + + // 设置需要监听的配置项ID + this.watchedConfigItemIDList = [ + SettingItemId.STANDARD_PRICE_SET, + SettingItemId.KLINE_STYLE_SET, + SettingItemId.KLINE_INTERACTION + ] + + // 加载初始数据 + this.refreshSections() + } + + private showLatestPriceAnnotationDialog(): void { + this.latestPriceAnnotationDialogController?.close() + this.latestPriceAnnotationDialogController = new CustomDialogController({ + builder: LatestPriceAnnotationSettingDialog({}), + alignment: DialogAlignment.Bottom, + customStyle: true, + cornerRadius: 12 + }) + this.latestPriceAnnotationDialogController.open() + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + this.buildPageTitle() + this.buildTabTitle() + Blank() + .width('100%') + .height(.5) + .backgroundColor($r('app.color.elements_others_divider_primary')) + this.buildPageContent() + }.layoutWeight(1) + .backgroundColor($r('app.color.surface_layer1_background')) + } + + @Builder + private buildPageTitle() { + PageTitleBar({ + title: '图表设置', + showBottomDivider: false + }).backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildTabTitle() { + Column() { + Row({ space: 20 }) { + ForEach(this.titleArray, (item: string, index: number) => { + this.buildSingleTabTitle(index) + }) + } + .alignItems(VerticalAlign.Center) + .justifyContent(FlexAlign.Center) + .height(30) + .width('100%') + + Blank() + .width(12) + .height(3) + .backgroundColor(this.tabMargin == 0 ? Color.Transparent : $r('app.color.elements_icon_primary_01')) + .borderRadius(1.5) + .margin({ top: 4, left: this.tabMargin, bottom: 12 }) + .onAreaChange((oldArea: Area, newArea: Area) => { + const width = newArea.width + if (typeof width === 'number') { + this.tabContentWidth = width + } + }) + }.backgroundColor($r('app.color.surface_layer1_foreground')) + .alignItems(HorizontalAlign.Start) + .onAreaChange((oldArea: Area, newArea: Area) => { + const width = newArea.width + if (typeof width === 'number') { + this.totalTabWidth = width + } + }) + + } + + @Builder + private buildSingleTabTitle(index: number) { + Column() { + Text(this.titleArray[index]) + .fontColor(this.currentSwiperIndex === index ? $r('app.color.elements_text_primary_02') : + $r('app.color.elements_text_secondary')) + .fontSize(16) + .fontWeight(this.currentSwiperIndex === index ? FontWeight.Medium : FontWeight.Regular) + .margin({ top: 7 }) + .onAreaChange((oldArea: Area, newArea: Area) => { + const width = newArea.width + const locationX = newArea.globalPosition.x + if (typeof width === 'number' && typeof locationX === 'number') { + const centerX = locationX + width / 2 + if (centerX != 0) { + this.titleLocationArray.set(this.titleArray[index], centerX) + } + } + }) + } + .layoutWeight(1) + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + this.currentSwiperIndex = index; + this.swiperController.changeIndex(index, true) + }) + } + + @Builder + private buildPageContent() { + Swiper(this.swiperController) { + ChartSettingView({ + sections: this.commonSections, dataVersion: this.dataVersion + }) + ChartSettingView({ + sections: this.klineSections, + dataVersion: this.dataVersion, + settingChangeNotifier: (item: BaseSettingItem) => { + if (item instanceof SettingItemSwitch) { + this.doSettingItemSwitchAction(item) + } + } + }) + ChartSettingView({ + sections: this.fenshiSections, dataVersion: this.dataVersion, settingChangeNotifier: (item: BaseSettingItem) => { + if (item instanceof SettingItemSwitch) { + this.doSettingItemSwitchAction(item) + } + } + }) + } + .layoutWeight(1) + .width('100%') + .indicator(false) + .effectMode(EdgeEffect.None) + .loop(false) + .animation({ duration: this.duration, curve: Curve.Linear }) + .onGestureSwipe((index: number, event: TabsAnimationEvent) => { + this.changeIndicatorMargin(index, event) + }) + .onAnimationStart((index: number, target: number) => { + this.startAnimateTo(this.duration, this.getTabMargin(target)) + this.currentSwiperIndex = target + }) + } + + private doSettingItemSwitchAction(item: SettingItemSwitch) { + { + if (item.preferenceKey === SettingItemId.FENSHI_DEAL_POINT_SET) { + // 获取当前开关状态 + const isOn = SettingHelper.getSetting(item.preferenceKey, item.defStatus, item.settingDataSource) as boolean + // 如果开关为开,检查是否首次开启需要展示弹窗 + if (isOn) { + if (!TimelineBSPointDataService.hasDetermined()) { + // 延迟打开弹窗,确保 UI 先更新 + setTimeout(() => { + this.timeLineBSPointDialogController.open() + }, 100) + } + TimelineBSPointDataService.updateTimelineBSPointSwitchStatus(BSPointUserAgreeStatus.Agreed) + } else { + TimelineBSPointDataService.updateTimelineBSPointSwitchStatus(BSPointUserAgreeStatus.Disagree) + } + } else if (item.preferenceKey === SettingItemId.KLINE_DEAL_POINT_SET) { + // K线成交点开关逻辑 + const isOn = SettingHelper.getSetting(item.preferenceKey, item.defStatus, item.settingDataSource) as boolean + if (isOn) { + // 开启时,检查是否首次开启 + if (!KLineBSPointDataService.hasDetermined()) { + // 延迟打开弹窗,确保 UI 先更新 + setTimeout(() => { + this.kLineBSPointDialogController.open() + }, 100) + } else { + KLineBSPointDataService.updateSwitchStatus(BSPointUserAgreeStatus.Agreed, (success: boolean) => { + if (!success) { + // 签约失败,延迟刷新确保状态更新完成 + setTimeout(() => { + this.loadSections() + }, 0) + } + }) + } + } else { + // 关闭开关时 + KLineBSPointDataService.updateSwitchStatus(BSPointUserAgreeStatus.Disagree) + } + } + } + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/CurveBottomToolSortPage.ets b/future_entry_demo/src/main/ets/pages/quote/CurveBottomToolSortPage.ets new file mode 100644 index 0000000..6fe80ea --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/CurveBottomToolSortPage.ets @@ -0,0 +1,18 @@ +import { CurveBottomToolSortView } from 'biz_quote' +import { StatusBar } from '../../widget/StatusBar' + +@Entry +@Component +struct CurveBottomToolSortPage { + build() { + Column() { + StatusBar() + CurveBottomToolSortView() + .layoutWeight(1) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Start) + .alignItems(HorizontalAlign.Start) + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/CustomPeriodSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/CustomPeriodSettingPage.ets new file mode 100644 index 0000000..6464de8 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/CustomPeriodSettingPage.ets @@ -0,0 +1,959 @@ +import { HXLog, PageTitleBar } from 'biz_common' +import { promptAction, router } from '@kit.ArkUI' +import { + CustomPeriodConstants, + CustomPeriodKind, + CustomPeriodManager, + PeriodModel, + StockInfo +} from 'biz_quote' +import { PeriodSettingKeyboard, PeriodSettingLevel } from '@b2c-f/fuhm-softkeyboard' +import { CustomPeriodLevel, makeCustomPeriod } from 'biz_futures_chart' +import { StatusBar } from '../../widget/StatusBar' +import { + ADD_PERIOD_INPUT_ID, + GRID_COLUMNS, + INVALID_INDEX, + PERIOD_BUTTON_HEIGHT, + PERIOD_DELETE_ICON_SIZE, + PERIOD_DELETE_TOUCH_SIZE, + PERIOD_DRAG_LAYOUT_ANIMATION_DURATION, + PERIOD_DRAG_TARGET_STABLE_COUNT, + PERIOD_FIXED_PLACEHOLDER_COUNT, + PERIOD_GRID_COLUMN_GAP, + PERIOD_GRID_HORIZONTAL_PADDING, + PERIOD_GRID_ROW_GAP, + PERIOD_ITEM_HEIGHT, + PERIOD_ITEM_RIGHT_MARGIN, + PERIOD_ITEM_TOP_MARGIN, + PERIOD_SEPARATOR_HEIGHT, + PeriodMoveOffset, +} from './custom_period/CustomPeriodSettingTypes' +import { PeriodSettingIntroductionDialog } from './custom_period/PeriodSettingIntroductionDialog' +import { CustomPeriodSelectionHelper } from './custom_period/CustomPeriodSelectionHelper' + +@Entry +@Component +export struct CustomPeriodSettingPage { + @State currentKind: CustomPeriodKind = CustomPeriodKind.Futures + @State commonPeriods: Array = [] + @State morePeriods: Array = [] + @State selectedLevel: PeriodSettingLevel = PeriodSettingLevel.MINUTE + @State inputValue: string = '' + @State draggingPeriod: string = '' + @State periodMoveOffsets: Array = [] + @State periodGridWidth: number = 0 + private readonly tabs: Resource[] = [ + $r('app.string.custom_period_tab_futures'), + $r('app.string.custom_period_tab_options') + ] + private readonly dragLogTag: string = 'PeriodDrag' + private periodInputController: TextInputController = new TextInputController() + private introductionDialogController: CustomDialogController = new CustomDialogController({ + builder: PeriodSettingIntroductionDialog({ + title: $r('app.string.custom_period_introduction_title'), + message: $r('app.string.custom_period_introduction_tip', CustomPeriodConstants.COMMON_COUNT) + }), + customStyle: true + }) + private dragSourceIndex: number = INVALID_INDEX + private dragOriginalPeriods: Array = [] + private dragLastTargetIndex: number = INVALID_INDEX + private dragPendingTargetIndex: number = INVALID_INDEX + private dragCandidateTargetIndex: number = INVALID_INDEX + private dragCandidateTargetCount: number = 0 + private dragPreviewAnimating: boolean = false + + aboutToAppear(): void { + this.initKindFromParams() + this.reloadPeriods() + } + + onPageHide(): void { + this.closeIntroductionDialog() + } + + aboutToDisappear(): void { + this.closeIntroductionDialog() + this.clearPeriodKeyboardDraft() + } + + private initKindFromParams(): void { + const stockInfo = router.getParams() as StockInfo + if (stockInfo?.stockMarket) { + this.currentKind = CustomPeriodManager.getKindByMarket(stockInfo.stockMarket) + } + } + + private reloadPeriods(): void { + const selected = CustomPeriodSelectionHelper.normalizeSelectedPeriods(this.currentKind, CustomPeriodManager.getAllPeriods(this.currentKind)) + this.commonPeriods = selected.slice(0, CustomPeriodConstants.COMMON_COUNT) + this.morePeriods = selected.slice(CustomPeriodConstants.COMMON_COUNT) + } + + private savePeriods(): void { + this.keepCommonPeriodCount() + const allPeriods = this.commonPeriods.concat(this.morePeriods) + .map((item: PeriodModel, index: number) => CustomPeriodSelectionHelper.clonePeriod(item, index)) + CustomPeriodManager.saveAllPeriods(this.currentKind, allPeriods) + } + + private keepCommonPeriodCount(): void { + this.splitPeriodsToSections(this.commonPeriods.concat(this.morePeriods)) + } + + private splitPeriodsToSections(source: Array): void { + CustomPeriodSelectionHelper.moveFixedTimelineToFirst(source) + source.forEach((item: PeriodModel, index: number) => item.index = index) + this.commonPeriods = source.slice(0, CustomPeriodConstants.COMMON_COUNT) + this.morePeriods = source.slice(CustomPeriodConstants.COMMON_COUNT) + } + + private showToast(message: string): void { + promptAction.showToast({ message: message, duration: 2000 }) + } + + private closeIntroductionDialog(): void { + this.introductionDialogController.close() + } + + private getResourceString(resource: Resource): string { + return getContext(this).resourceManager.getStringSync(resource) + } + + private changeKind(kind: CustomPeriodKind): void { + if (this.currentKind === kind) { + return + } + this.savePeriods() + this.currentKind = kind + this.clearPeriodKeyboardDraft() + this.hidePeriodKeyboard() + this.resetDragState() + this.reloadPeriods() + } + + private clearPeriodKeyboardDraft(): void { + this.inputValue = '' + this.selectedLevel = PeriodSettingLevel.MINUTE + } + + private isPeriodExist(period: number): boolean { + return this.getAllSelectedPeriods().some((item: PeriodModel) => item.kLinePeriod === period) + } + + private getAllSelectedPeriods(): Array { + return this.commonPeriods.concat(this.morePeriods) + } + + private canDeletePeriod(item: PeriodModel, isDragging: boolean): boolean { + return !isDragging && item.isCustomPeriod === CustomPeriodConstants.CUSTOM_FLAG_CUSTOM + } + + private hasKLinePeriodAfterRemove(period: number): boolean { + return this.getAllSelectedPeriods().some((item: PeriodModel) => { + return item.kLinePeriod !== period && !CustomPeriodSelectionHelper.isTimelineLikeModel(item) + }) + } + + private removePeriodFromSelected(period: number): void { + this.commonPeriods = this.commonPeriods.filter((item: PeriodModel) => item.kLinePeriod !== period) + this.morePeriods = this.morePeriods.filter((item: PeriodModel) => item.kLinePeriod !== period) + this.keepCommonPeriodCount() + } + + private deleteCustomPeriod(item: PeriodModel): void { + if (item.isCustomPeriod !== CustomPeriodConstants.CUSTOM_FLAG_CUSTOM || !this.hasKLinePeriodAfterRemove(item.kLinePeriod)) { + return + } + this.removePeriodFromSelected(item.kLinePeriod) + this.savePeriods() + } + + private startDrag(index: number, item: PeriodModel): void { + this.dragOriginalPeriods = this.getAllSelectedPeriods().map((period: PeriodModel, periodIndex: number) => { + return CustomPeriodSelectionHelper.clonePeriod(period, periodIndex) + }) + this.dragSourceIndex = index + this.draggingPeriod = CustomPeriodSelectionHelper.getPeriodKey(item) + this.dragLastTargetIndex = index + HXLog.d(this.dragLogTag, `start kind=${this.currentKind} p=${this.draggingPeriod} from=${index} order=${this.formatPeriodOrder(this.dragOriginalPeriods)}`) + } + + private resetDragState(): void { + this.dragSourceIndex = INVALID_INDEX + this.draggingPeriod = '' + this.dragOriginalPeriods = [] + this.dragLastTargetIndex = INVALID_INDEX + this.dragPendingTargetIndex = INVALID_INDEX + this.dragCandidateTargetIndex = INVALID_INDEX + this.dragCandidateTargetCount = 0 + this.dragPreviewAnimating = false + this.periodMoveOffsets = [] + } + + private handleDrop(insertIndex: number, isSuccess: boolean): void { + const resolvedInsertIndex = this.resolveDropInsertIndex(insertIndex, isSuccess) + if (!isSuccess || this.dragSourceIndex === INVALID_INDEX || resolvedInsertIndex === INVALID_INDEX) { + HXLog.d(this.dragLogTag, `drop-revert ok=${isSuccess} from=${this.dragSourceIndex} to=${insertIndex} cur=${this.formatPeriodOrder(this.getAllSelectedPeriods())}`) + this.revertDragPreview() + this.resetDragState() + return + } + HXLog.d(this.dragLogTag, `drop-commit from=${this.dragSourceIndex} to=${resolvedInsertIndex} raw=${insertIndex} cur=${this.formatPeriodOrder(this.getAllSelectedPeriods())}`) + this.dragPendingTargetIndex = INVALID_INDEX + this.dragPreviewAnimating = false + this.applyDragPreview(resolvedInsertIndex, false, true) + this.savePeriods() + HXLog.d(this.dragLogTag, `saved order=${this.formatPeriodOrder(this.getAllSelectedPeriods())}`) + this.resetDragState() + } + + private resolveDropInsertIndex(insertIndex: number, isSuccess: boolean): number { + if (!isSuccess || this.dragSourceIndex === INVALID_INDEX || insertIndex !== INVALID_INDEX) { + return insertIndex + } + // Grid 偶发在松手时给出“添加周期”之后的 raw insertIndex,映射结果为 -1。 + // 此时如果拖动过程中已有稳定目标/预览目标,应落到最后一个有效目标,而不是回滚到初始顺序。 + if (this.dragPendingTargetIndex !== INVALID_INDEX) { + HXLog.d(this.dragLogTag, `drop-fallback pending=${this.dragPendingTargetIndex}`) + return this.dragPendingTargetIndex + } + if (this.dragLastTargetIndex !== INVALID_INDEX && this.dragLastTargetIndex !== this.dragSourceIndex) { + HXLog.d(this.dragLogTag, `drop-fallback last=${this.dragLastTargetIndex}`) + return this.dragLastTargetIndex + } + return INVALID_INDEX + } + + private revertDragPreview(): void { + if (this.dragOriginalPeriods.length <= 0) { + return + } + HXLog.d(this.dragLogTag, `revert original=${this.formatPeriodOrder(this.dragOriginalPeriods)}`) + const originalPeriods = this.dragOriginalPeriods.map((period: PeriodModel, index: number) => { + return CustomPeriodSelectionHelper.clonePeriod(period, index) + }) + this.animatePeriodLayoutChange(this.getAllSelectedPeriods(), originalPeriods) + } + + private previewDragMove(insertGridIndex: number): void { + if (this.dragSourceIndex === INVALID_INDEX) { + return + } + const targetIndex = this.getDropMergedIndexByGridIndex(insertGridIndex) + if (targetIndex === INVALID_INDEX) { + this.dragPendingTargetIndex = INVALID_INDEX + this.resetDragCandidateTarget() + return + } + if (targetIndex === this.dragLastTargetIndex) { + this.resetDragCandidateTarget() + return + } + if (!this.isDragTargetStable(targetIndex)) { + return + } + if (this.dragPreviewAnimating) { + if (this.dragPendingTargetIndex !== targetIndex) { + this.dragPendingTargetIndex = targetIndex + HXLog.d(this.dragLogTag, `move-pending to=${targetIndex}`) + } + return + } + this.resetDragCandidateTarget() + HXLog.d(this.dragLogTag, `move to=${targetIndex} cur=${this.formatPeriodOrder(this.getAllSelectedPeriods())}`) + this.applyDragPreview(targetIndex, true, false) + } + + private isDragTargetStable(targetIndex: number): boolean { + if (this.dragCandidateTargetIndex !== targetIndex) { + this.dragCandidateTargetIndex = targetIndex + this.dragCandidateTargetCount = 1 + return false + } + this.dragCandidateTargetCount += 1 + const stable = this.dragCandidateTargetCount >= PERIOD_DRAG_TARGET_STABLE_COUNT + return stable + } + + private resetDragCandidateTarget(): void { + this.dragCandidateTargetIndex = INVALID_INDEX + this.dragCandidateTargetCount = 0 + } + + private applyDragPreview(targetIndex: number, animated: boolean, shouldLand: boolean): void { + if (this.dragOriginalPeriods.length <= 0 || this.dragSourceIndex === INVALID_INDEX) { + HXLog.d(this.dragLogTag, `apply-skip empty=${this.dragOriginalPeriods.length <= 0} from=${this.dragSourceIndex}`) + return + } + const currentPeriods = this.getAllSelectedPeriods() + const currentDragIndex = this.getDraggingPeriodIndex(currentPeriods) + if (currentDragIndex === INVALID_INDEX) { + HXLog.d(this.dragLogTag, `apply-skip missing p=${this.draggingPeriod} cur=${this.formatPeriodOrder(currentPeriods)}`) + return + } + const previewPeriods = shouldLand + ? this.buildReorderedPeriods(currentPeriods, currentDragIndex, targetIndex) + : this.buildPreviewReorderedPeriods(currentPeriods, currentDragIndex, targetIndex) + const commitPreview = () => { + this.splitPeriodsToSections(previewPeriods) + this.dragLastTargetIndex = this.getDraggingPeriodIndex(previewPeriods) + } + if (!animated) { + HXLog.d(this.dragLogTag, `apply-land from=${currentDragIndex} to=${targetIndex} next=${this.formatPeriodOrder(previewPeriods)}`) + commitPreview() + return + } + HXLog.d(this.dragLogTag, `apply-preview from=${currentDragIndex} to=${targetIndex} next=${this.formatPeriodOrder(previewPeriods)}`) + this.dragLastTargetIndex = this.getDraggingPeriodIndex(previewPeriods) + this.animatePeriodLayoutChange(currentPeriods, previewPeriods, () => { + const pendingTargetIndex = this.dragPendingTargetIndex + this.dragPendingTargetIndex = INVALID_INDEX + this.resetDragCandidateTarget() + this.dragPreviewAnimating = false + if (pendingTargetIndex !== INVALID_INDEX && pendingTargetIndex !== this.dragLastTargetIndex + && this.dragSourceIndex !== INVALID_INDEX) { + this.applyDragPreview(pendingTargetIndex, true, false) + } + }) + } + + private animatePeriodLayoutChange(beforePeriods: Array, afterPeriods: Array, + onFinish?: () => void): void { + this.dragPreviewAnimating = true + const moveOffsets = this.buildPeriodMoveOffsets(beforePeriods, afterPeriods) + HXLog.d(this.dragLogTag, `anim offsets=${moveOffsets.length} before=${this.formatPeriodOrder(beforePeriods)} after=${this.formatPeriodOrder(afterPeriods)}`) + this.periodMoveOffsets = moveOffsets + this.splitPeriodsToSections(afterPeriods) + if (moveOffsets.length <= 0) { + this.periodMoveOffsets = [] + if (onFinish) { + onFinish() + } + return + } + setTimeout(() => { + animateTo({ + curve: Curve.Smooth, + duration: PERIOD_DRAG_LAYOUT_ANIMATION_DURATION, + onFinish: onFinish + }, () => { + this.periodMoveOffsets = [] + }) + }, 0) + } + + private buildPeriodMoveOffsets(beforePeriods: Array, afterPeriods: Array): Array { + if (this.periodGridWidth <= 0) { + return [] + } + const result: Array = [] + afterPeriods.forEach((item: PeriodModel, afterIndex: number) => { + if (CustomPeriodSelectionHelper.getPeriodKey(item) === this.draggingPeriod) { + return + } + const beforeIndex = beforePeriods.findIndex((period: PeriodModel) => CustomPeriodSelectionHelper.getPeriodKey(period) === CustomPeriodSelectionHelper.getPeriodKey(item)) + if (beforeIndex === INVALID_INDEX || beforeIndex === afterIndex) { + return + } + const beforeGridIndex = this.getGridIndexByMergedIndex(beforeIndex) + const afterGridIndex = this.getGridIndexByMergedIndex(afterIndex) + const offsetX = this.getGridItemXByGridIndex(beforeGridIndex) - this.getGridItemXByGridIndex(afterGridIndex) + const offsetY = this.getGridItemYByGridIndex(beforeGridIndex) - this.getGridItemYByGridIndex(afterGridIndex) + if (Math.abs(offsetX) > 0.5 || Math.abs(offsetY) > 0.5) { + result.push(new PeriodMoveOffset(CustomPeriodSelectionHelper.getPeriodKey(item), offsetX, offsetY)) + } + }) + return result + } + + private getPeriodMoveOffset(item: PeriodModel): PeriodMoveOffset { + const key = CustomPeriodSelectionHelper.getPeriodKey(item) + const offset = this.periodMoveOffsets.find((offsetItem: PeriodMoveOffset) => offsetItem.period === key) + return offset ? offset : new PeriodMoveOffset(key, 0, 0) + } + + private getGridIndexByMergedIndex(mergedIndex: number): number { + if (mergedIndex < this.commonPeriods.length) { + return mergedIndex + } + return mergedIndex + PERIOD_FIXED_PLACEHOLDER_COUNT + } + + private getGridColumnWidth(): number { + if (this.periodGridWidth <= 0) { + return 0 + } + return (this.periodGridWidth - PERIOD_GRID_COLUMN_GAP * (GRID_COLUMNS - 1)) / GRID_COLUMNS + } + + private getGridItemXByGridIndex(gridIndex: number): number { + const column = gridIndex % GRID_COLUMNS + return column * (this.getGridColumnWidth() + PERIOD_GRID_COLUMN_GAP) + } + + private getGridItemYByGridIndex(gridIndex: number): number { + const moreStartGridIndex = this.getMoreStartGridIndex() + if (gridIndex >= moreStartGridIndex) { + const moreRow = Math.floor((gridIndex - moreStartGridIndex) / GRID_COLUMNS) + return this.getSeparatorY() + PERIOD_SEPARATOR_HEIGHT + PERIOD_GRID_ROW_GAP + + moreRow * (PERIOD_ITEM_HEIGHT + PERIOD_GRID_ROW_GAP) + } + const row = Math.floor(gridIndex / GRID_COLUMNS) + return row * (PERIOD_ITEM_HEIGHT + PERIOD_GRID_ROW_GAP) + } + + private buildReorderedPeriods(source: Array, from: number, to: number): Array { + const merged = source.slice() + if (from < 0 || from >= merged.length || to < 0) { + return merged + } + const safeTo = Math.min(to, merged.length - 1) + const item = merged.splice(from, 1)[0] + merged.splice(safeTo, 0, item) + return merged + } + + private buildPreviewReorderedPeriods(source: Array, from: number, to: number): Array { + // 预览态直接按当前拖拽元素的位置执行补位排序;展示层会把被拖拽元素透明, + // 浮层由 pixelMapBuilder 承担,避免出现两个同名标签。 + return this.buildReorderedPeriods(source, from, to) + } + + private getDraggingPeriodIndex(periods: Array): number { + return periods.findIndex((item: PeriodModel) => CustomPeriodSelectionHelper.getPeriodKey(item) === this.draggingPeriod) + } + + private createIndexArray(count: number): Array { + const items: Array = [] + for (let index = 0; index < count; index++) { + items.push(index) + } + return items + } + + private getMoreStartGridIndex(): number { + return this.commonPeriods.length + PERIOD_FIXED_PLACEHOLDER_COUNT + } + + private isFixedPlaceholderGridIndex(gridIndex: number): boolean { + return gridIndex >= this.commonPeriods.length && gridIndex < this.getMoreStartGridIndex() + } + + private isAddGridIndex(gridIndex: number): boolean { + return gridIndex === this.getMoreStartGridIndex() + this.morePeriods.length + } + + private getPeriodByGridIndex(gridIndex: number): PeriodModel | undefined { + if (gridIndex < 0) { + return undefined + } + if (gridIndex < this.commonPeriods.length) { + return this.commonPeriods[gridIndex] + } + const moreIndex = gridIndex - this.getMoreStartGridIndex() + if (moreIndex >= 0 && moreIndex < this.morePeriods.length) { + return this.morePeriods[moreIndex] + } + return undefined + } + + private getMergedIndexByGridIndex(gridIndex: number): number { + if (gridIndex >= 0 && gridIndex < this.commonPeriods.length) { + return gridIndex + } + const moreIndex = gridIndex - this.getMoreStartGridIndex() + if (moreIndex >= 0 && moreIndex < this.morePeriods.length) { + return this.commonPeriods.length + moreIndex + } + return INVALID_INDEX + } + + private getDropMergedIndexByGridIndex(gridIndex: number): number { + const totalPeriodCount = this.commonPeriods.length + this.morePeriods.length + if (totalPeriodCount <= 0 || gridIndex < 0) { + return INVALID_INDEX + } + const gridPeriod = this.getPeriodByGridIndex(gridIndex) + if (gridPeriod && CustomPeriodSelectionHelper.isFixedTimelineModel(gridPeriod)) { + return INVALID_INDEX + } + if (gridIndex < this.commonPeriods.length) { + return gridIndex + } + if (this.isFixedPlaceholderGridIndex(gridIndex)) { + return INVALID_INDEX + } + const moreStartIndex = this.getMoreStartGridIndex() + if (this.isAddGridIndex(gridIndex)) { + return totalPeriodCount - 1 + } + const moreIndex = gridIndex - moreStartIndex + if (moreIndex >= 0 && moreIndex < this.morePeriods.length) { + return this.commonPeriods.length + moreIndex + } + return INVALID_INDEX + } + + private openAddPeriodKeyboard(): void { + setTimeout(() => { + focusControl.requestFocus(ADD_PERIOD_INPUT_ID) + }, 0) + } + + private hidePeriodKeyboard(): void { + this.periodInputController.stopEditing() + this.getUIContext().getFocusController().clearFocus() + } + + private toCustomPeriodLevel(level: PeriodSettingLevel): CustomPeriodLevel { + switch (level) { + case PeriodSettingLevel.DAY: + return CustomPeriodLevel.Day + case PeriodSettingLevel.WEEK: + return CustomPeriodLevel.Week + case PeriodSettingLevel.MONTH: + return CustomPeriodLevel.Month + case PeriodSettingLevel.MINUTE: + default: + return CustomPeriodLevel.Minute + } + } + + private addCustomPeriod(periodNum: number, level: PeriodSettingLevel): boolean { + const customLevel = this.toCustomPeriodLevel(level) + const period = makeCustomPeriod(customLevel, periodNum) + const validation = CustomPeriodManager.validateCustomPeriod(this.currentKind, customLevel, periodNum) + if (!validation.success) { + if (validation.message !== CustomPeriodConstants.VALIDATION_PERIOD_EXISTS) { + this.showToast(validation.message) + } + return false + } + if (this.isPeriodExist(period)) { + return false + } + this.morePeriods.push(CustomPeriodConstants.createPeriodModel(period, this.commonPeriods.length + this.morePeriods.length, true)) + this.clearPeriodKeyboardDraft() + this.savePeriods() + this.hidePeriodKeyboard() + this.showToast(this.getResourceString($r('app.string.custom_period_added_to_more_toast'))) + return true + } + + private formatPeriodOrder(periods: Array): string { + return `[${periods.map((item: PeriodModel) => CustomPeriodSelectionHelper.getPeriodKey(item)).join(',')}]` + } + + private getCommonGridRows(): number { + return Math.ceil(this.commonPeriods.length / GRID_COLUMNS) + } + + private getUnifiedGridHeight(): number { + const commonRows = this.getCommonGridRows() + const moreRows = Math.max(1, Math.ceil((this.morePeriods.length + 1) / GRID_COLUMNS)) + const itemRows = commonRows + moreRows + return itemRows * PERIOD_ITEM_HEIGHT + PERIOD_SEPARATOR_HEIGHT + itemRows * PERIOD_GRID_ROW_GAP + } + + private getSeparatorY(): number { + const commonRows = this.getCommonGridRows() + return commonRows * PERIOD_ITEM_HEIGHT + commonRows * PERIOD_GRID_ROW_GAP + } + + private getKindByIndex(index: number): CustomPeriodKind { + return index === 0 ? CustomPeriodKind.Futures : CustomPeriodKind.Options + } + + private isTabSelected(index: number): boolean { + return this.currentKind === this.getKindByIndex(index) + } + + build() { + Stack() { + Column() { + StatusBar() + PageTitleBar({ + title: this.getResourceString($r('app.string.custom_period_setting_title')), + showBottomDivider: true + }) + this.buildKindTabs() + Scroll() { + Column() { + this.buildCommonSection() + this.createPeriodGrid() + } + .padding({ bottom: 24 }) + .width('100%') + .constraintSize({ minHeight: '100%' }) + .justifyContent(FlexAlign.Start) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + .layoutWeight(1) + .width('100%') + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.None) + .align(Alignment.Top) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + .height('100%') + .width('100%') + .justifyContent(FlexAlign.Start) + .alignItems(HorizontalAlign.Start) + .backgroundColor($r('app.color.surface_layer1_background')) + + this.buildPeriodKeyboardAnchor() + } + .height('100%') + .width('100%') + } + + @Builder + private buildKindTabs() { + Row({ space: 20 }) { + ForEach(this.tabs, (title: Resource, index: number) => { + this.buildSingleTabTitle(index) + }) + } + .alignItems(VerticalAlign.Center) + .justifyContent(FlexAlign.Center) + .height(40) + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildSingleTabTitle(index: number) { + Column() { + Text(this.tabs[index]) + .fontColor($r('app.color.elements_text_primary_02')) + .fontSize(16) + .fontWeight(this.isTabSelected(index) ? FontWeight.Medium : FontWeight.Regular) + .margin({ top: 7 }) + Blank() + .width(12) + .height(3) + .backgroundColor(this.isTabSelected(index) ? $r('app.color.elements_icon_primary_01') : Color.Transparent) + .borderRadius(1.5) + .margin({ top: 4 }) + } + .layoutWeight(1) + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + this.changeKind(this.getKindByIndex(index)) + }) + } + + @Builder + private buildCommonSection() { + Column() { + Row() { + Text($r('app.string.custom_period_common_title')) + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + .fontWeight(FontWeight.Medium) + .margin({ left: 10, top: 10, bottom: 10, right: 4 }) + this.buildIntroductionIcon() + Blank() + Text($r('app.string.custom_period_drag_sort_tip')) + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + .fontWeight(FontWeight.Normal) + .margin({ top: 10, bottom: 10, right: 10 }) + } + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + + } + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildIntroductionIcon() { + Stack({ alignContent: Alignment.Center }) { + Text('?') + .fontSize(11) + .fontColor($r('app.color.elements_text_tertiary')) + .textAlign(TextAlign.Center) + } + .width(16) + .height(16) + .borderRadius(8) + .border({ width: 0.8, color: $r('app.color.elements_text_tertiary') }) + .margin({ left: 2, top: 10, bottom: 10 }) + .onClick(() => { + this.introductionDialogController.open() + }) + } + + @Builder + private createPeriodGrid() { + Stack() { + Grid() { + ForEach(this.commonPeriods, (item: PeriodModel, index: number) => { + GridItem() { + this.createPeriodItemView(item, false) + } + }, (item: PeriodModel) => CustomPeriodSelectionHelper.getPeriodKey(item)) + + ForEach(this.createIndexArray(PERIOD_FIXED_PLACEHOLDER_COUNT), (item: number) => { + GridItem() { + this.createFixedPlaceholderItemView(item) + } + .draggable(false) + }, (item: number) => `fixed-placeholder-${item}`) + + ForEach(this.morePeriods, (item: PeriodModel, index: number) => { + GridItem() { + this.createPeriodItemView(item, false) + } + }, (item: PeriodModel) => CustomPeriodSelectionHelper.getPeriodKey(item)) + + GridItem() { + this.createAddPeriodItemView() + } + .draggable(false) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Up) { + this.openAddPeriodKeyboard() + } + }) + } + .width('100%') + .height('100%') + .scrollBar(BarState.Off) + .columnsTemplate('1fr 1fr 1fr') + .columnsGap(PERIOD_GRID_COLUMN_GAP) + .rowsGap(PERIOD_GRID_ROW_GAP) + .clip(false) + .editMode(true) + // 中间态排序由 applyDragPreview + animateTo 接管;Grid 原生 supportAnimation 会固定追踪 dragStart 的 itemIndex, + // 在源位置已被补位后仍误触发系统动效,因此这里关闭原生补位动画。 + .supportAnimation(false) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .onAreaChange((_oldArea: Area, newArea: Area) => { + const width = newArea.width + if (typeof width === 'number' && width > 0) { + this.periodGridWidth = width + } + }) + .onItemDragStart((_event: ItemDragInfo, itemIndex: number) => { + if (this.isAddGridIndex(itemIndex)) { + HXLog.d(this.dragLogTag, `start-block add grid=${itemIndex}`) + return undefined + } + const item = this.getPeriodByGridIndex(itemIndex) + if (!item || CustomPeriodSelectionHelper.isFixedTimelineModel(item)) { + HXLog.d(this.dragLogTag, `start-block fixed/empty grid=${itemIndex} p=${item ? item.kLinePeriod : 'none'}`) + return undefined + } + const mergedIndex = this.getMergedIndexByGridIndex(itemIndex) + if (mergedIndex === INVALID_INDEX) { + HXLog.d(this.dragLogTag, `start-block invalid grid=${itemIndex}`) + return undefined + } + this.startDrag(mergedIndex, item) + return this.pixelMapBuilder(item) + }) + .onItemDragMove((_event: ItemDragInfo, _itemIndex: number, insertIndex: number) => { + this.previewDragMove(insertIndex) + }) + .onItemDrop((_event: ItemDragInfo, _itemIndex: number, insertIndex: number, isSuccess: boolean) => { + const targetMergedIndex = this.getDropMergedIndexByGridIndex(insertIndex) + HXLog.d(this.dragLogTag, `drop raw=${insertIndex}->${targetMergedIndex} ok=${isSuccess}`) + this.handleDrop(targetMergedIndex, isSuccess) + }) + + this.buildMoreSeparator() + } + .width('100%') + .height(this.getUnifiedGridHeight()) + .padding({ left: PERIOD_GRID_HORIZONTAL_PADDING, right: 10 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildMoreSeparator() { + Row() { + Divider() + .layoutWeight(1) + .color($r('app.color.elements_others_divider_primary')) + .strokeWidth(0.5) + Text($r('app.string.custom_period_more_separator_tip')) + .fontSize(12) + .fontColor($r('app.color.elements_text_tertiary')) + .margin({ left: 8, right: 8 }) + Divider() + .layoutWeight(1) + .color($r('app.color.elements_others_divider_primary')) + .strokeWidth(0.5) + } + .height(PERIOD_SEPARATOR_HEIGHT) + .width('100%') + .alignItems(VerticalAlign.Center) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .position({ x: 0, y: this.getSeparatorY() }) + .zIndex(2) + .hitTestBehavior(HitTestMode.Transparent) + } + + @Builder + private createFixedPlaceholderItemView(placeholderIndex: number) { + Column() { + } + .height(placeholderIndex === 0 ? PERIOD_ITEM_HEIGHT : PERIOD_SEPARATOR_HEIGHT) + .width('100%') + } + + @Builder + private createPeriodItemView(item: PeriodModel, isDragging: boolean) { + Stack({ alignContent: Alignment.TopEnd }) { + Column() { + Blank() + .height(PERIOD_ITEM_TOP_MARGIN) + Row() { + Stack({ alignContent: Alignment.Center }) { + Text(item.name) + .fontSize(14) + .fontColor(CustomPeriodSelectionHelper.isFixedTimelineModel(item) ? $r('app.color.elements_text_tertiary') : $r('app.color.elements_text_secondary')) + .width('100%') + .height('100%') + .textAlign(TextAlign.Center) + } + .layoutWeight(1) + .height(PERIOD_BUTTON_HEIGHT) + .borderRadius(4) + .backgroundColor(CustomPeriodSelectionHelper.isFixedTimelineModel(item) ? $r('app.color.surface_layer2_background') : $r('app.color.elements_button_bg_tertiary')) + + Blank() + .width(PERIOD_ITEM_RIGHT_MARGIN) + } + .height(PERIOD_BUTTON_HEIGHT) + .width('100%') + } + + if (this.canDeletePeriod(item, isDragging)) { + this.buildDeletePeriodIcon(item) + } + } + .height(PERIOD_ITEM_HEIGHT) + .width(isDragging ? '33.33%' : '100%') + .scale({ + x: this.draggingPeriod === CustomPeriodSelectionHelper.getPeriodKey(item) ? 0.9 : 1, + y: this.draggingPeriod === CustomPeriodSelectionHelper.getPeriodKey(item) ? 0.9 : 1 + }) + .opacity(!isDragging && this.draggingPeriod === CustomPeriodSelectionHelper.getPeriodKey(item) ? 0 : 1) + .translate(isDragging ? { x: 0, y: 0 } : { + x: this.getPeriodMoveOffset(item).x, + y: this.getPeriodMoveOffset(item).y + }) + .draggable(false) + .onClick(() => {}) + } + + @Builder + private buildDeletePeriodIcon(item: PeriodModel) { + Row() { + Image($r('app.media.theme_icon_item_del')) + .width(PERIOD_DELETE_ICON_SIZE) + .height(PERIOD_DELETE_ICON_SIZE) + .objectFit(ImageFit.Contain) + } + .width(PERIOD_DELETE_TOUCH_SIZE) + .height(PERIOD_DELETE_TOUCH_SIZE) + .justifyContent(FlexAlign.End) + .alignItems(VerticalAlign.Top) + .align(Alignment.TopEnd) + .margin({ + top: PERIOD_ITEM_TOP_MARGIN - PERIOD_DELETE_ICON_SIZE / 2, + right: PERIOD_ITEM_RIGHT_MARGIN - PERIOD_DELETE_ICON_SIZE / 2 + }) + .zIndex(1) + .onClick(() => { + this.deleteCustomPeriod(item) + }) + } + + @Builder + private createAddPeriodItemView() { + Column() { + Blank() + .height(PERIOD_ITEM_TOP_MARGIN) + Row() { + Stack({ alignContent: Alignment.Center }) { + Text($r('app.string.custom_period_add_button')) + .fontSize(14) + .fontColor($r('app.color.new_color_alpha84')) + .width('100%') + .height('100%') + .textAlign(TextAlign.Center) + } + .layoutWeight(1) + .height(PERIOD_BUTTON_HEIGHT) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(4) + .border({ width: 0.5, color: $r('app.color.period_setting_add_border') }) + + Blank() + .width(PERIOD_ITEM_RIGHT_MARGIN) + } + .height(PERIOD_BUTTON_HEIGHT) + .width('100%') + } + .height(PERIOD_ITEM_HEIGHT) + .width('100%') + .draggable(false) + } + + @Builder + private pixelMapBuilder(item: PeriodModel) { + Column() { + this.createPeriodItemView(item, true) + } + .zIndex(1) + .scale({ x: 1.05, y: 1.05 }) + .translate({ x: 0, y: 0 }) + } + + @Builder + private buildPeriodKeyboardAnchor() { + TextInput({ + text: $$this.inputValue, + controller: this.periodInputController, + placeholder: '' + }) + .id(ADD_PERIOD_INPUT_ID) + .type(InputType.Number) + .width(1) + .height(1) + .opacity(0.01) + .position({ x: 0, y: 0 }) + .enableKeyboardOnFocus(true) + .customKeyboard(this.periodKeyboardBuilder) + .onChange((value: string) => { + this.inputValue = value + }) + } + + @Builder + private periodKeyboardBuilder() { + PeriodSettingKeyboard({ + inputValue: $inputValue, + selectedLevel: $selectedLevel, + controller: this.periodInputController, + showHideIcon: true, + onHideKeyboard: (): void => { + this.hidePeriodKeyboard() + }, + onPeriodAdd: (period: number, level: PeriodSettingLevel): boolean => { + return this.addCustomPeriod(period, level) + } + }) + .width('100%') + } + +} diff --git a/future_entry_demo/src/main/ets/pages/quote/IndicatorManagerPage.ets b/future_entry_demo/src/main/ets/pages/quote/IndicatorManagerPage.ets new file mode 100644 index 0000000..b6cf3b0 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/IndicatorManagerPage.ets @@ -0,0 +1,287 @@ +/** + * 指标管理页面 + * 复刻iOS LJTechManagerController + * 顶部三个Tab:K线副图、K线主图、分时副图 + * 底部:自编指标和广场指标入口 + * + * Created by LY on 2026/01/04. + * Updated by LY on 2026/04/27 - 添加底部自编指标和广场指标入口 + * Copyright © 2026 hexin. All rights reserved. + */ + +import { PageTitleBar, ScreenUtil } from 'biz_common' +import { StatusBar } from '../../widget/StatusBar' +import { IndicatorList, IndicatorType, IndicatorSettingManager } from 'biz_quote' +import { router } from '@kit.ArkUI' +import { util } from '@kit.ArkTS' +import { HXUserService } from 'biz_hxservice' + +@Entry +@Component +export struct IndicatorManagerPage { + @Provide pageShow: boolean = false + @State currentIndex: number = 0 + + private tabTitles: string[] = ['K线副图', 'K线主图', '分时副图'] + private tabController: TabsController = new TabsController() + + onPageShow(): void { + // 从搜索页返回时同步选中的Tab索引 + const params = router.getParams() as Record + if(!params){ + this.pageShow = true + return + } + if (!params.fromSearchPage || params.fromSearchPage !== 1) { + this.pageShow = true + return + } + + if (params.currentIndex !== undefined && params.currentIndex !== this.currentIndex) { + this.currentIndex = params.currentIndex + this.tabController.changeIndex(this.currentIndex) + } + this.pageShow = true + } + + onPageHide(): void { + this.pageShow = false + } + + aboutToAppear(): void { + // 获取传入的当前Tab索引 + const params = router.getParams() as Record + if (params?.currentIndex !== undefined) { + this.currentIndex = params.currentIndex + IndicatorSettingManager.selectedTabIndex = this.currentIndex + } + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + + // 内容区域 + Column() { + PageTitleBar({ + title: '指标设置', + titleRightBuilder: (): void => this.buildSearchButton() + }) + + // Tab 栏 + this.buildTabBar() + + // Tab 内容(留出底部空间) + Tabs({ barPosition: BarPosition.Start, controller: this.tabController, index: this.currentIndex }) { + // K线副图 + TabContent() { + this.buildIndicatorListWrapper(IndicatorType.KLINE_VICE, 0) + } + .backgroundColor($r('app.color.surface_layer1_background')) + + // K线主图 + TabContent() { + this.buildIndicatorListWrapper(IndicatorType.KLINE_MAIN, 1) + } + .backgroundColor($r('app.color.surface_layer1_background')) + + // 分时副图 + TabContent() { + this.buildIndicatorListWrapper(IndicatorType.TIME_LINE_VICE, 2) + } + .backgroundColor($r('app.color.surface_layer1_background')) + } + .barWidth(0) + .barHeight(0) + .scrollable(true) + .layoutWeight(1) + .onChange((index: number) => { + this.currentIndex = index + IndicatorSettingManager.selectedTabIndex = index + }) + } + .width('100%') + .layoutWeight(1) + + // 底部功能栏:自编指标 + 指标广场(固定在底部,不随列表滚动) + this.buildBottomView() + } + } + + /** + * 指标列表包装器:为列表底部留出空间 + */ + @Builder + buildIndicatorListWrapper(indicatorType: IndicatorType, pageIndex: number) { + Stack({ alignContent: Alignment.Bottom }) { + IndicatorList({ indicatorType: indicatorType, currentTabIndex: this.currentIndex, pageIndex: pageIndex }) + } + .width('100%') + .height('100%') + } + + /** + * 底部视图:自编指标和广场指标入口 + * 复刻iOS LJIndicatorBottomView + * 固定在底部,不随列表滚动 + */ + @Builder + buildBottomView() { + Column() { + Row() { + // 左侧:自编指标 + this.buildBottomButton('自编指标', $r('app.media.hq_indicator_edit'), () => this.gotoEditIndicator()) + + // 中间分割线 + Column() + .width(0.5) + .height(24) + .backgroundColor($r('app.color.new_color_alpha8')) + + // 右侧:指标广场 + this.buildBottomButton('指标广场', $r('app.media.indicator_square_icon'), () => this.gotoSquareIndicator()) + } + .width('100%') + .height(52) + .backgroundColor($r('app.color.color_ffffff_1c1c1c')) + .padding({ left: 16, right: 16 }) + } + .width('100%') + .backgroundColor($r('app.color.color_ffffff_1c1c1c')) + .padding({ bottom: 34 }) // 底部安全区域 + } + + /** + * 底部按钮 Builder + * @param text 按钮文字 + * @param icon 图标资源 + * @param onClick 点击回调 + */ + @Builder + buildBottomButton(text: string, icon: Resource, onClick: () => void) { + Row() { + Image(icon) + .width(20) + .height(20) + .objectFit(ImageFit.Contain) + + Text(text) + .fontSize(16) + .fontColor($r('app.color.new_color_alpha84')) + .margin({ left: 4 }) + } + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + .onClick(onClick) + } + + /** + * 跳转到自编指标编辑页面(WebView) + * 复刻自 IndicatorSwitchDialog.openCustomEntrancePage + */ + private gotoEditIndicator(): void { + const userService = HXUserService.getInstance() + if (!userService.isLoggedIn()) { + router.pushUrl({ + url: 'pages/login/LoginPage', + params: { + flexSwitchFlag: true, + sourcePage: 'pages/quote/IndicatorManagerPage' + } + }) + return + } + + const context = getContext(this) + const baseUrl = context.resourceManager.getStringSync($r('app.string.indicator_edit_web_url')) + if (!baseUrl || baseUrl.length === 0) { + console.error('[IndicatorManagerPage] indicator_edit_web_url is empty') + return + } + + const uuid = util.generateRandomUUID() + const separator = baseUrl.indexOf('?') >= 0 ? '&' : '?' + const targetUrl = `${baseUrl}${separator}type=add&uuid=${uuid}` + + router.pushUrl({ + url: 'pages/WebViewComponentPage', + params: { + url: targetUrl, + useCookie: true, + isLoginRefreshWeb: true, + 'ishiddenbar': 1 + } + }) + } + + /** + * 跳转到指标广场页面(WebView) + * 复刻自 IndicatorSwitchDialog.openIndicatorSquarePage + */ + private gotoSquareIndicator(): void { + const context = getContext(this) + const baseUrl = context.resourceManager.getStringSync($r('app.string.indicator_square_web_url')) + + router.pushUrl({ + url: 'pages/WebViewComponentPage', + params: { + url: baseUrl + } + }) + } + + @Builder + private buildSearchButton() { + Column() { + Image($r('app.media.icon_search_24')) + .fillColor($r('app.color.color_222222_dddddd')) + .width(24) + .height(24) + } + .size({ width: 36, height: '100%' }) + .margin({ right: 8 }) + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + router.pushUrl({ + url: 'pages/quote/IndicatorSearchPage', + params: { + currentIndex: this.currentIndex + } + }) + }) + } + + + @Builder + private buildTabBar() { + Row() { + ForEach(this.tabTitles, (title: string, index: number) => { + Column() { + Text(title) + .fontSize(16) + .fontWeight(this.currentIndex === index ? FontWeight.Medium : FontWeight.Regular) + .fontColor(this.currentIndex === index ? $r('app.color.elements_text_primary_02') : $r('app.color.elements_text_secondary')) + .height(40) + .textAlign(TextAlign.Center) + + // 底部指示器 + Row() + .width(12) + .height(3) + .borderRadius(1.5) + .backgroundColor(this.currentIndex === index ? $r('app.color.elements_text_primary_01') : 'transparent') + } + .layoutWeight(1) + .justifyContent(FlexAlign.End) + .onClick(() => { + this.currentIndex = index + this.tabController.changeIndex(index) + }) + }) + } + .width('100%') + .height(45) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/IndicatorSearchPage.ets b/future_entry_demo/src/main/ets/pages/quote/IndicatorSearchPage.ets new file mode 100644 index 0000000..84d1c3b --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/IndicatorSearchPage.ets @@ -0,0 +1,187 @@ +/** + * 指标搜索页面 + * 复刻iOS LJIndicatorSearchViewController + * + * 功能: + * - 搜索栏支持中文名称和拼音模糊匹配 + * - 三个Tab:K线副图、K线主图、分时副图 + * - 搜索结果分两个Section:常用指标 + 待选指标 + * + * Created by LY on 2026/01/13. + * Copyright © 2026 hexin. All rights reserved. + */ + +import { router } from '@kit.ArkUI' +import { StatusBar } from '../../widget/StatusBar' +import { IndicatorSearchList, IndicatorType, IndicatorSettingManager } from 'biz_quote' + +/** 搜索栏高度 */ +const SEARCH_BAR_HEIGHT = 32 +/** 导航栏高度 */ +const NAV_BAR_HEIGHT = 44 +/** Tab栏高度 */ +const TAB_BAR_HEIGHT = 40 + +@Entry +@Component +export struct IndicatorSearchPage { + @State currentIndex: number = 0 + @State searchKeyword: string = '' + + private tabTitles: string[] = ['K线副图', 'K线主图', '分时副图'] + private tabController: TabsController = new TabsController() + private searchController: TextInputController = new TextInputController() + + aboutToAppear(): void { + // 获取传入的当前Tab索引 + const params = router.getParams() as Record + if (params?.currentIndex !== undefined) { + this.currentIndex = params.currentIndex + } + } + + onBackPress(): boolean | void { + router.back({ + url: 'pages/quote/IndicatorManagerPage', + params: { currentIndex: this.currentIndex, fromSearchPage: 1 } + } as router.RouterOptions) + return true + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + + // 自定义导航栏(返回按钮 + 搜索栏) + this.buildNavBar() + + // Tab 栏 + this.buildTabBar() + + // Tab 内容(搜索结果列表) + Tabs({ barPosition: BarPosition.Start, controller: this.tabController, index: this.currentIndex }) { + // K线副图 + TabContent() { + IndicatorSearchList({ + indicatorType: IndicatorType.KLINE_VICE, + searchKeyword: this.searchKeyword + }) + } + + // K线主图 + TabContent() { + IndicatorSearchList({ + indicatorType: IndicatorType.KLINE_MAIN, + searchKeyword: this.searchKeyword + }) + } + + // 分时副图 + TabContent() { + IndicatorSearchList({ + indicatorType: IndicatorType.TIME_LINE_VICE, + searchKeyword: this.searchKeyword + }) + } + } + .barWidth(0) + .barHeight(0) + .scrollable(true) + .layoutWeight(1) + .onChange((index: number) => { + this.currentIndex = index + IndicatorSettingManager.selectedTabIndex = index + }) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.color_ffffff_1c1c1c')) + } + + @Builder + private buildNavBar() { + Row() { + // 返回按钮 + Column() { + Image($r('app.media.navigate_back_icon')) + .fillColor($r('app.color.color_222222_dddddd')) + .width(24) + .height(24) + } + .width(44) + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .onClick(() => { + router.back({ + url: 'pages/quote/IndicatorManagerPage', + params: { currentIndex: this.currentIndex, fromSearchPage: 1 } + } as router.RouterOptions) + }) + + + // 搜索栏 + Row() { + Image($r('app.media.icon_search_24')) + .width(16) + .height(16) + .fillColor($r('app.color.color_80222222_80dddddd')) + .margin({ left: 8 }) + + TextInput({ placeholder: '请输入指标名称', controller: this.searchController }) + .placeholderColor($r('app.color.new_color_alpha40')) + .placeholderFont({ size: 14 }) + .fontSize(14) + .fontColor($r('app.color.new_color_alpha84')) + .layoutWeight(1) + .height('100%') + .backgroundColor(Color.Transparent) + .padding({ left: 4, right: 8 }) + .onChange((value: string) => { + this.searchKeyword = value + }) + .onSubmit(() => { + // 搜索提交 + }) + } + .height(SEARCH_BAR_HEIGHT) + .layoutWeight(1) + .backgroundColor($r('app.color.color_eeeeee_212125')) + .borderRadius(4) + .margin({ right: 16 }) + } + .width('100%') + .height(NAV_BAR_HEIGHT) + } + + @Builder + private buildTabBar() { + Row() { + ForEach(this.tabTitles, (title: string, index: number) => { + Column() { + Text(title) + .fontSize(16) + .fontWeight(this.currentIndex === index ? FontWeight.Medium : FontWeight.Regular) + .fontColor(this.currentIndex === index ? $r('app.color.new_color_alpha84') : $r('app.color.new_color_alpha60')) + .height(TAB_BAR_HEIGHT - 5) + .textAlign(TextAlign.Center) + + // 底部指示器 + Row() + .width(12) + .height(3) + .borderRadius(1.5) + .backgroundColor(this.currentIndex === index ? $r('app.color.elements_text_primary_01') : 'transparent') + } + .layoutWeight(1) + .justifyContent(FlexAlign.End) + .onClick(() => { + this.currentIndex = index + this.tabController.changeIndex(index) + }) + }) + } + .width('100%') + .height(TAB_BAR_HEIGHT) + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/KLineInteractionSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/KLineInteractionSettingPage.ets new file mode 100644 index 0000000..0b291eb --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/KLineInteractionSettingPage.ets @@ -0,0 +1,146 @@ +/** + * K线滑动设置页面 + * 提供"切换周期"和"移动K线"两种滑动模式选择 + * + * Created by zhangshan2 + * Copyright © 2026 hexin. All rights reserved. + */ + +import { PageTitleBar } from 'biz_common' +import { SettingHelper, SettingDataSource } from 'biz_quote' +import { QuoteSettingField } from 'biz_quote/src/main/ets/data/setting/QuoteSettingConstants' +import { KLineInteractionType } from 'biz_quote/src/main/ets/data/setting/QuoteCustomSettingModel' +import { StatusBar } from '../../widget/StatusBar' +import { UnifyUrlManager } from '@b2c-f/url-modify-config' +import { WebViewComponent } from 'biz_webview' + +@Entry +@Component +export struct KLineInteractionSettingPage { + @State selectedIndex: KLineInteractionType = KLineInteractionType.KLineScroll + @State previewUrl: string = '' + + aboutToAppear(): void { + // 读取当前设置值 + const currentValue = SettingHelper.getSetting( + QuoteSettingField.INTERACTION_TYPE, + KLineInteractionType.KLineScroll, + SettingDataSource.QUOTE_CLOUD + ) as number + this.selectedIndex = currentValue + this.previewUrl = this.getPreviewUrl(this.selectedIndex) + } + + /** + * 根据交互类型获取预览URL + */ + private getPreviewUrl(type: KLineInteractionType): string { + if (type === KLineInteractionType.PeriodChange) { + return UnifyUrlManager.getRealUrl('kline_interaction_period_change_url') + } + return UnifyUrlManager.getRealUrl('kline_interaction_kline_scroll_url') + } + + /** + * 根据交互类型获取显示文字 + */ + private getOptionText(type: KLineInteractionType): Resource { + if (type === KLineInteractionType.PeriodChange) { + return $r('app.string.kline_interaction_period_change') + } + return $r('app.string.kline_interaction_kline_scroll') + } + + /** + * 处理选项切换 + */ + private onTabSelect(type: KLineInteractionType): void { + if (this.selectedIndex === type) { + return + } + this.selectedIndex = type + this.previewUrl = this.getPreviewUrl(type) + // 保存到云端 + SettingHelper.updateSetting( + QuoteSettingField.INTERACTION_TYPE, + type, + SettingDataSource.QUOTE_CLOUD + ) + } + + build() { + Column() { + StatusBar() + PageTitleBar({ + title: $r('app.string.kline_interaction_setting_title'), + showBottomDivider: false + }) + this.buildOptionsArea() + Blank() + .width('100%') + .height(0.5) + .backgroundColor($r('app.color.elements_others_divider_primary')) + this.buildPreviewArea() + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_background')) + } + + @Builder + private buildOptionsArea() { + Column() { + this.buildOptionItem(KLineInteractionType.PeriodChange) + Blank() + .width('100%') + .height(0.5) + .backgroundColor($r('app.color.elements_others_divider_primary')) + this.buildOptionItem(KLineInteractionType.KLineScroll) + } + .width('100%') + .padding({ left: 16, right: 16, top: 8, bottom: 8 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + private buildOptionItem(type: KLineInteractionType) { + Row() { + Text(this.getOptionText(type)) + .fontSize(16) + .fontColor($r('app.color.elements_text_primary_02')) + .layoutWeight(1) + Image($r('app.media.icon_checked_24')) + .fillColor($r('app.color.elements_icon_primary_01')) + .width(22) + .height(22).visibility(this.selectedIndex === type ? Visibility.Visible:Visibility.Hidden) + } + .width('100%') + .height(48) + .onClick(() => { + this.onTabSelect(type) + }) + } + + @Builder + private buildPreviewArea() { + Column() { + Text($r('app.string.kline_interaction_preview_title')) + .fontSize(14) + .fontColor($r('app.color.elements_text_secondary')) + .width('100%') + .padding({ left: 16, top: 12, bottom: 8 }) + + WebViewComponent({ + urlString: this.previewUrl, + onShow: true, + useCookie: true, + isLoginRefreshWeb: true, + refreshOnWidthChange: true + }).padding({ left: 16, right: 16 }) + .width('100%') + .layoutWeight(1) + }.backgroundColor($r('app.color.surface_layer1_background')) + .width('100%') + .layoutWeight(1) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/quote/KlineStyleSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/KlineStyleSettingPage.ets new file mode 100644 index 0000000..699cb5e --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/KlineStyleSettingPage.ets @@ -0,0 +1,230 @@ +/** + * K线样式设置页面 + * author: tanglinhong + * date: 2026/04/23 + * version: 1.0 + */ + +import { QuoteCustomSettingManager, SettingConfigManager, SettingItemId } from 'biz_quote' +import { KLineStyle } from 'biz_quote' +import { HXLog, PageTitleBar, StatusBar } from 'biz_common' + +/** + * K线样式数据模型 + */ +@Component +export struct KlineStyleModelItem { + @ObjectLink itemData: KlineStyleItem + onItemClick: (item: KlineStyleItem) => void = () => {} + + build() { + Row() { + Image($r(this.itemData.klineStyleIconResName)) + .width(20) + .height(20) + .margin({ left: 16 }) + + Text(this.itemData.klineStyleName) + .fontSize(15) + .fontColor($r('app.color.new_color_alpha84')) + .margin({ left: 4 }) + + Text(this.itemData.klineStyleSubName) + .fontSize(14) + .fontColor($r('app.color.new_color_alpha60')) + .margin({ left: 8 }) + + Blank() + + Image($r('app.media.icon_checked_24')) + .fillColor($r('app.color.elements_icon_primary_01')) + .width(18) + .height(18) + .margin({ right: 16 }) + .visibility(this.itemData.isSelected ? Visibility.Visible : Visibility.Hidden) + } + .width('100%') + .height(50) + .onClick(() => { + this.onItemClick(this.itemData) + }) + } +} + +/** + * K线样式数据项 + */ +@Observed +class KlineStyleItem { + klineStyleType: number = 0 + klineStyleName: string = '' + klineStyleSubName: string = '' + klineStyleIconResName: string = '' + klineStyleBmpResName: string = '' + cbas: string = '' + isSelected: boolean = false + + constructor(type: number, name: string, subName: string, iconRes: string, bmpRes: string, cbas: string) { + this.klineStyleType = type + this.klineStyleName = name + this.klineStyleSubName = subName + this.klineStyleIconResName = iconRes + this.klineStyleBmpResName = bmpRes + this.cbas = cbas + } +} + +@Entry +@Component +export struct KlineStyleSettingPage { + + @State klineStyleList: KlineStyleItem[] = [] + @State selectedKlineStyle: KLineStyle = KLineStyle.Default + @State selectedPreviewRes: string = 'app.media.bmp_candle_rect' + + // K线样式配置数据(与Android端all_kline_style.json一致) + private klineStyleConfig: KlineStyleItem[] = [ + new KlineStyleItem(KLineStyle.Default, '普通K线', '', 'app.media.ic_candle_rect', 'app.media.bmp_candle_rect', + 'hollow'), + new KlineStyleItem(KLineStyle.Solid, '实心K线', '', 'app.media.ic_solid_candle_rect', + 'app.media.bmp_solid_candle_rect', 'solid'), + new KlineStyleItem(KLineStyle.USA, '美国线', 'BAR', 'app.media.ic_american', 'app.media.bmp_american', 'bar'), + new KlineStyleItem(KLineStyle.Tower, '宝塔线', 'TOWER', 'app.media.ic_tower', 'app.media.bmp_tower', 'tower'), + new KlineStyleItem(KLineStyle.Close, '收盘价线', 'CLOSE', 'app.media.ic_close', 'app.media.bmp_close', 'close') + ] + + aboutToAppear(): void { + this.loadKlineStyles() + this.loadCurrentSetting() + } + + @Builder + private buildPageTitle() { + PageTitleBar({ + title: 'K线样式', + showBottomDivider: true + }).backgroundColor($r('app.color.surface_layer1_foreground')) + } + + /** + * 加载K线样式列表 + */ + private loadKlineStyles(): void { + this.klineStyleList = this.klineStyleConfig.map(config => new KlineStyleItem( + config.klineStyleType, + config.klineStyleName, + config.klineStyleSubName, + config.klineStyleIconResName, + config.klineStyleBmpResName, + config.cbas + )) + } + + /** + * 加载当前设置 + */ + private loadCurrentSetting(): void { + const currentStyle = QuoteCustomSettingManager.getInstance().getSetting(SettingItemId.KLINE_STYLE_SET, KLineStyle.Default) as KLineStyle + this.selectedKlineStyle = currentStyle + this.updateSelection(currentStyle) + } + + /** + * 更新选中状态 + */ + private updateSelection(klineStyleType: KLineStyle): void { + this.klineStyleList.forEach(item => { + item.isSelected = item.klineStyleType === klineStyleType + if (item.isSelected) { + this.selectedPreviewRes = item.klineStyleBmpResName + } + }) + } + + /** + * K线样式项点击 + */ + private onKlineStyleClick(item: KlineStyleItem): void { + // 更新选中状态 + this.klineStyleList.forEach(klineItem => { + klineItem.isSelected = klineItem.klineStyleType === item.klineStyleType + if (klineItem.isSelected) { + this.selectedPreviewRes = klineItem.klineStyleBmpResName + } + }) + // 保存设置 + this.selectedKlineStyle = item.klineStyleType as KLineStyle + QuoteCustomSettingManager.getInstance().updateSetting('klineStyle', item.klineStyleType) + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + this.buildPageTitle() + Scroll() { + Column(){ + // K线样式列表 + List() { + ForEach(this.klineStyleList, (item: KlineStyleItem, index: number) => { + ListItem() { + Column(){ + KlineStyleModelItem({ + itemData: item, + onItemClick: (clickedItem: KlineStyleItem) => { + this.onKlineStyleClick(clickedItem) + } + }) + //列表整体的最后一行或者下一个是预览图时,均不展示分割线 + if (index != this.klineStyleList.length - 1) { + Divider() + .width('100%') + .vertical(false) + .strokeWidth(.5) + .color($r('app.color.elements_others_divider_primary')) + .margin({ left: 16, right: 16 }) + } + } + } + }, (item: KlineStyleItem) => item.klineStyleType.toString()) + } + .enableScrollInteraction(false) + .scrollBar(BarState.Off) + .width('100%') + .height(50 * this.klineStyleList.length) + + // 中间间隔 + Column() + .width('100%') + .height(8) + .backgroundColor($r('app.color.surface_layer1_background')) + + // 预览区域 + Text('效果预览') + .fontSize(13) + .fontColor($r('app.color.new_color_alpha40')) + .width('100%') + .padding({ left: 16, top: 16 }) + RelativeContainer() { + Image($r(this.selectedPreviewRes)) + .alignRules({ middle: { anchor: "__container__", align: HorizontalAlign.Center } }) + .objectFit(ImageFit.Contain) + } + .width('100%') + .height(278) + .padding({ + top: 16, + left: 16, + right: 16, + }) + } + } + .width('100%') + .height('100%') + .align(Alignment.Top) + .scrollBar(BarState.Off) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/quote/LargeOrderSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/LargeOrderSettingPage.ets new file mode 100644 index 0000000..f48d8eb --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/LargeOrderSettingPage.ets @@ -0,0 +1,25 @@ +/** + * 大单筛选设置页(路由页面) + * 职责: 承载 biz_quote 的 LargeOrderSettingView,从路由参数取合约信息透传。 + * 路由: pages/quote/LargeOrderSettingPage,参数 stockCode / stockMarket / stockName + */ +import { LargeOrderSettingView } from 'biz_quote' +import { router } from '@kit.ArkUI' + +@Entry +@Component +export struct LargeOrderSettingPage { + private params: Record = router.getParams() as Record + + build() { + Flex({ direction: FlexDirection.Column }) { + LargeOrderSettingView({ + stockCode: this.params['stockCode'] ? this.params['stockCode'] : '', + stockName: this.params['stockName'] ? this.params['stockName'] : '' + }) + } + .backgroundColor($r('app.color.large_order_setting_background')) + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/SettingIntroducePage.ets b/future_entry_demo/src/main/ets/pages/quote/SettingIntroducePage.ets new file mode 100644 index 0000000..ea70d93 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/SettingIntroducePage.ets @@ -0,0 +1,55 @@ +/** + * 设置项说明页面 + * 用于展示设置项的详细说明 + * + * Created by LY on 2026/01/04. + * Copyright © 2026 hexin. All rights reserved. + */ + +import { PageTitleBar } from 'biz_common' +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' + +interface IntroduceParams { + title: string + content: string +} + +@Entry +@Component +export struct SettingIntroducePage { + @State title: string = '' + @State content: string = '' + + aboutToAppear(): void { + const params = router.getParams() as IntroduceParams + if (params) { + this.title = params.title ?? '' + this.content = params.content ?? '' + } + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + Column() { + PageTitleBar({ + title: this.title + }) + + Column() { + Text(this.content) + .fontSize(15) + .fontColor('#333333') + .lineHeight(24) + } + .width('100%') + .padding(16) + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .height('100%') + .backgroundColor('#ffffff') + } + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/StandardPriceSettingPage.ets b/future_entry_demo/src/main/ets/pages/quote/StandardPriceSettingPage.ets new file mode 100644 index 0000000..f782e3f --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/StandardPriceSettingPage.ets @@ -0,0 +1,18 @@ +import { PageTitleBar } from 'biz_common' +import { StandardPriceSettingView } from 'biz_quote' +import { StatusBar } from '../../widget/StatusBar' + +@Entry +@Component +export struct StandardPriceSettingPage { + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + PageTitleBar({ title: '涨跌计算基准价设置' }) + StandardPriceSettingView() + } + .backgroundColor($r('app.color.surface_layer1_background')) + .width('100%') + .height('100%') + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSelectionHelper.ets b/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSelectionHelper.ets new file mode 100644 index 0000000..d305c4b --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSelectionHelper.ets @@ -0,0 +1,54 @@ +import { CustomPeriodConstants, CustomPeriodKind, PeriodIdentity, PeriodModel } from 'biz_quote' + +export class CustomPeriodSelectionHelper { + static normalizeSelectedPeriods(kind: CustomPeriodKind, source: Array): Array { + const result: Array = [] + const seen = new Set() + source.forEach((item: PeriodModel) => { + const key = CustomPeriodSelectionHelper.getPeriodKey(item) + if (!seen.has(key) && PeriodIdentity.isPeriodModelAllowedForKind(kind, item)) { + seen.add(key) + result.push(CustomPeriodSelectionHelper.clonePeriod(item, result.length)) + } + }) + CustomPeriodConstants.getRequiredSystemPeriods(kind).forEach((item: PeriodModel) => { + const key = CustomPeriodSelectionHelper.getPeriodKey(item) + if (!seen.has(key) && PeriodIdentity.isPeriodModelAllowedForKind(kind, item)) { + seen.add(key) + result.push(CustomPeriodSelectionHelper.clonePeriod(item, result.length)) + } + }) + CustomPeriodSelectionHelper.moveFixedTimelineToFirst(result) + result.forEach((item: PeriodModel, index: number) => item.index = index) + return result + } + + static moveFixedTimelineToFirst(periods: Array): void { + const fixedIndex = periods.findIndex((item: PeriodModel) => CustomPeriodSelectionHelper.isFixedTimelineModel(item)) + if (fixedIndex > 0) { + periods.splice(0, 0, periods.splice(fixedIndex, 1)[0]) + } + } + + static clonePeriod(source: PeriodModel, index: number): PeriodModel { + const model = new PeriodModel() + model.name = source.name + model.tag = source.tag + model.index = index + model.kLinePeriod = source.kLinePeriod + model.isCustomPeriod = source.isCustomPeriod + return model + } + + static getPeriodKey(item: PeriodModel): string { + return PeriodIdentity.getPeriodKey(item) + } + + static isFixedTimelineModel(item: PeriodModel): boolean { + return PeriodIdentity.isNormalTimeLineModel(item) + } + + static isTimelineLikeModel(item: PeriodModel): boolean { + return PeriodIdentity.isTimelineLikeModel(item) + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSettingTypes.ets b/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSettingTypes.ets new file mode 100644 index 0000000..e04d14d --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/custom_period/CustomPeriodSettingTypes.ets @@ -0,0 +1,28 @@ +export const GRID_COLUMNS: number = 3 +export const PERIOD_BUTTON_HEIGHT: number = 36 +export const PERIOD_DELETE_ICON_SIZE: number = 14 +export const PERIOD_DELETE_TOUCH_SIZE: number = 24 +export const PERIOD_ITEM_TOP_MARGIN: number = 8 +export const PERIOD_ITEM_RIGHT_MARGIN: number = 8 +export const PERIOD_ITEM_HEIGHT: number = PERIOD_BUTTON_HEIGHT + PERIOD_ITEM_TOP_MARGIN +export const PERIOD_GRID_ROW_GAP: number = 10 +export const PERIOD_GRID_COLUMN_GAP: number = 12 +export const PERIOD_GRID_HORIZONTAL_PADDING: number = 20 +export const PERIOD_SEPARATOR_HEIGHT: number = 44 +export const PERIOD_FIXED_PLACEHOLDER_COUNT: number = GRID_COLUMNS + 1 +export const PERIOD_DRAG_LAYOUT_ANIMATION_DURATION: number = 180 +export const PERIOD_DRAG_TARGET_STABLE_COUNT: number = 2 +export const INVALID_INDEX: number = -1 +export const ADD_PERIOD_INPUT_ID: string = 'custom_period_setting_keyboard_input' + +export class PeriodMoveOffset { + period: string = '' + x: number = 0 + y: number = 0 + + constructor(period: string, x: number, y: number) { + this.period = period + this.x = x + this.y = y + } +} diff --git a/future_entry_demo/src/main/ets/pages/quote/custom_period/PeriodSettingIntroductionDialog.ets b/future_entry_demo/src/main/ets/pages/quote/custom_period/PeriodSettingIntroductionDialog.ets new file mode 100644 index 0000000..f75734c --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/quote/custom_period/PeriodSettingIntroductionDialog.ets @@ -0,0 +1,47 @@ +@CustomDialog +export struct PeriodSettingIntroductionDialog { + controller: CustomDialogController + @Prop title: string | Resource = '' + @Prop message: string | Resource = '' + + build() { + Column() { + Text(this.title) + .fontSize(18) + .fontColor($r('app.color.elements_text_primary_02')) + .fontWeight(FontWeight.Medium) + .textAlign(TextAlign.Center) + .width('100%') + .margin({ bottom: 14 }) + + Image($r('app.media.theme_img_period_introduction')) + .width(233) + .height(144) + .objectFit(ImageFit.Contain) + .margin({ bottom: 8 }) + + Text(this.message) + .fontSize(15) + .fontColor($r('app.color.elements_text_secondary')) + .lineHeight(22) + .textAlign(TextAlign.Start) + .width(233) + .margin({ bottom: 20 }) + + Button($r('app.string.custom_period_introduction_known_button'), { type: ButtonType.Normal, stateEffect: true }) + .height(40) + .width('100%') + .backgroundColor($r('app.color.elements_icon_primary_01')) + .fontColor(Color.White) + .fontSize(16) + .borderRadius(8) + .onClick(() => { + this.controller.close() + }) + } + .padding({ left: 16, right: 16, top: 20, bottom: 16 }) + .width('74%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(12) + } +} diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfCodePage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfCodePage.ets new file mode 100644 index 0000000..1311aa5 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfCodePage.ets @@ -0,0 +1,249 @@ +import { AIGroupContentView, AIGroupListView, SelectedIdOfWatchlistGroup, SelfCodeGroupView, SelfCodeTableView, SelfCodeTopTabView } from 'biz_selfcode' +import { HeaderItem } from '@b2b/hq_table' +import { loadCustomGroups } from 'service_watchlist' +import { HQContractMapItem, IndexScrollBar } from 'biz_market' +import { EmitterConstants, HeadSettingManager, IndexSettingManager, RequestController } from 'biz_common' +import { emitter } from '@kit.BasicServicesKit' +import { setCurrentFrameId } from 'biz_quote/src/main/ets/util/FrameId' +import image from '@ohos.multimedia.image'; +import drawing from '@ohos.graphics.drawing'; +import { StrUtil } from '@pura/harmony-utils' +import { AIGroupManager, AIStrategy, AI_GROUP_UPDATE_EVENT } from 'service_aigroup' +import { TableConstants } from '@b2c/lib_baseui' +import { delay } from '@wolfx/lodash' + +@Component +export struct SelfCodePage { + @Prop @Watch('onShowStatusChange') onForeground: boolean + @State tableHeaderInit: boolean = false + @State requestNum: number = 0 + @State topTabIndex: number = 0 // 0: 自选分组, 1: AI分组 + @State indexArray: HQContractMapItem[] | null = null + @Provide frameId: number = 2201 + @State selfCodeHeaderConfig: Array = [] + @State indexVisible: boolean = true + // 控制顶层Tab栏是否显示 + @State showTopTab: boolean = true + // AI 分组当前选中的策略 + @State currentStrategy: AIStrategy | undefined = undefined + private requestController = new RequestController() + private netRelinkCallBack = () => { + // 延迟 500ms 等子组件 @Prop requestOrRemove 同步完成后再触发 IndexScrollBar 的 onRequest, + // 否则 onShowStatusChange 同步链上读到的还是旧的 requestOrRemove,请求会被跳过。 + delay(() => { + if (this.onForeground) { + this.sendRequest() + } + }, 500) + } + // 分组数据变化回调 + private onGroupDataChange = () => { + this.checkTopTabVisibility() + } + + aboutToAppear(): void { + this.initHeader() + this.indexVisible = IndexSettingManager.getInstance().isSelfStockIndexVisible() + if (this.indexVisible) { + this.indexArray = IndexSettingManager.getInstance().getSelfStockIndexConfig() + } + // 检查顶层Tab栏是否显示 + this.checkTopTabVisibility() + // 监听自选分组变化 + emitter.on({ eventId: EmitterConstants.SELF_STOCK_GROUP_CHANGE }, this.onGroupDataChange) + // 监听AI分组变化 + emitter.on({ eventId: AI_GROUP_UPDATE_EVENT }, this.onGroupDataChange) + } + + aboutToDisappear(): void { + emitter.off(EmitterConstants.SELF_STOCK_GROUP_CHANGE, this.onGroupDataChange) + emitter.off(AI_GROUP_UPDATE_EVENT, this.onGroupDataChange) + } + + /** + * 检查顶层Tab栏是否显示 + * 显示条件:自选分组数 > 0 OR AI分组数 > 0 + * 隐藏条件:自选分组数 == 0 AND AI分组数 == 0 + */ + private checkTopTabVisibility(): void { + const customGroups = loadCustomGroups(true) + const aiStrategies = AIGroupManager.getUgcAddedStrategies() + const shouldShow = customGroups.length > 0 || aiStrategies.length > 0 + + if (this.showTopTab !== shouldShow) { + this.showTopTab = shouldShow + // 如果从显示切换到隐藏,重置到默认分组 + if (!shouldShow) { + this.topTabIndex = 0 + SelectedIdOfWatchlistGroup.setDefaultId() + } + } + } + + private initHeader() { + HeadSettingManager.getInstance().getFuturesDisplaySettingConfig(true).then((result) => { + const config = result?.hqData ?? [] + if (this.isHeaderChanged(this.selfCodeHeaderConfig,config)) { + this.selfCodeHeaderConfig = [...config] + this.tableHeaderInit = true + this.sendRequest() + } + }) + } + + private isHeaderChanged(oldHeaderItem:HeaderItem[],newHeaderItem:HeaderItem[]){ + if (oldHeaderItem.length <=0 || newHeaderItem.length <= 0 || oldHeaderItem.length != newHeaderItem.length) { + return true + } + const oldItemJson = JSON.stringify(oldHeaderItem) + const newItemJson = JSON.stringify(newHeaderItem) + if (StrUtil.isEmpty(oldItemJson) || StrUtil.isEmpty(newItemJson)) { + return true + } + return oldItemJson != newItemJson + } + + onShowStatusChange() { + if (this.onForeground) { + this.initHeader() + this.indexVisible = IndexSettingManager.getInstance().isSelfStockIndexVisible() + if (this.indexVisible) { + const newIndexConfig = IndexSettingManager.getInstance().getSelfStockIndexConfig() + if (this.isIndexArrayChanged(newIndexConfig)) { + this.indexArray = newIndexConfig + } + } + // 检查顶层Tab栏是否显示 + this.checkTopTabVisibility() + // 切换前后台时触发重新请求 + emitter.on({ + eventId: EmitterConstants.NETWORK_TCP_RECONNECT + }, this.netRelinkCallBack); + this.netRelinkCallBack() + setCurrentFrameId(this.frameId) + } else { + emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack) + } + } + + /** + * 判断indexArray和最新数据有没有差异 + * @returns + */ + isIndexArrayChanged(newIndexArray: HQContractMapItem[]): boolean { + if (!this.indexArray || this.indexArray.length !== newIndexArray.length) { + return true + } + for (let i = 0; i < newIndexArray.length; i++) { + const newHqItem = newIndexArray[i] + const currentHqItem = this.indexArray[i] + // 逐个比较字段 + if (newHqItem.market !== currentHqItem.market || + newHqItem.contract !== currentHqItem.contract || + newHqItem.sorts !== currentHqItem.sorts || + newHqItem.contract_name !== currentHqItem.contract_name || + newHqItem.contract_type !== currentHqItem.contract_type) { + return true + } + } + return false + } + + private sendRequest() { + if (this.onForeground && this.selfCodeHeaderConfig.length > 0) { + this.requestNum++ + this.requestController.request() + } + } + + build() { + Column() { + // 顶层Tab切换:自选分组 / AI分组 + if (this.showTopTab) { + SelfCodeTopTabView({ + currentIndex: this.topTabIndex, + onTabChange: (index: number) => { + this.topTabIndex = index + // 切换 tab 时,重置为默认分组 + if (index === 0) { + SelectedIdOfWatchlistGroup.setDefaultId() + } + } + }) + } + + if (this.topTabIndex === 0) { + // 自选分组内容 + SelfCodeGroupView({ + onForeground: this.onForeground, + onSelectGroup: (index: number) => { + this.onSelectGroup(index) + } + }) + } else { + // AI 分组列表 + AIGroupListView({ + onForeground: this.onForeground, + onStrategyChange: (strategy: AIStrategy | undefined) => { + this.currentStrategy = strategy + } + }) + } + + // 指数栏(自选分组和AI分组共用) + IndexScrollBar({ + indexArray: this.indexArray, + requestController: this.requestController, + requestOrRemove: this.onForeground, + isVisible: this.indexVisible, + showSettingEntry: true + }) + .margin({ top: this.indexVisible ? 8 : 0, bottom: this.indexVisible ? 8 : 0 }) + + Blank() + .height(7) + .width('100%') + .backgroundColor($r('app.color.surface_layer1_background')) + + if (this.topTabIndex === 0) { + // 自选分组表格 + if (this.tableHeaderInit) { + SelfCodeTableView({ + headerConfig: this.selfCodeHeaderConfig, + requestOrRemove: this.onForeground, + requestNum: this.requestNum + }) + .layoutWeight(1) + } + } else { + // AI分组内容 + AIGroupContentView({ + onForeground: this.onForeground, + currentStrategy: this.currentStrategy, + onViewMore: () => { + // TODO: 跳转到AI分组更多页面 + } + }) + .layoutWeight(1) + } + } + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + private onSelectGroup(index: number) { + // 只有在自选分组 tab 时才处理,避免影响 AI 分组 + if (this.topTabIndex !== 0) { + return + } + let customGroups = loadCustomGroups(true) + if (index <= customGroups.length && index >= 0) { + if (index === 0) { + SelectedIdOfWatchlistGroup.setDefaultId() + } else if (customGroups.length > 0) { + SelectedIdOfWatchlistGroup.setId(customGroups[index-1].id) + } + emitter.emit({ eventId: EmitterConstants.TABLE_INDEX_CHANGE }) + this.requestNum++ + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfCodeToMainContractPage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfCodeToMainContractPage.ets new file mode 100644 index 0000000..f75619a --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfCodeToMainContractPage.ets @@ -0,0 +1,39 @@ +import { BasicStockInfo, GlobalContext, PageTitleBar } from 'biz_common' +import { ConvertToMainContractView } from 'biz_selfcode' +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' + +@Entry +@Component +export struct SelfCodeToMainContractPage { + sourceStockInfo: BasicStockInfo[] = [] + groupIndex: number = 0 + + aboutToAppear(): void { + let params = router.getParams() as Record + let stocks = params['stocks'] as BasicStockInfo[] + let groupIndex = params['groupIndex'] as number + if (stocks) { + this.sourceStockInfo = stocks + } + if (typeof groupIndex === 'number') { + this.groupIndex = groupIndex + } + } + + build() { + Column() { + StatusBar() + PageTitleBar({title: '一键换主力'}) + ConvertToMainContractView({ + stocks: this.sourceStockInfo, + groupIndex: this.groupIndex + }) + .layoutWeight(1) + } + .backgroundColor($r('app.color.surface_layer1_foreground')) + .width('100%') + .height('100%') + .padding({bottom: GlobalContext.navigationBarHeightVp}) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupAddStockPage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupAddStockPage.ets new file mode 100644 index 0000000..5d2c0e6 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupAddStockPage.ets @@ -0,0 +1,36 @@ +import { GlobalContext } from 'biz_common' +import { SelfGroupAddStock } from 'biz_selfcode' +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' + +@Entry +@Component +struct SelfGroupAddStockPage { + @State pageBack:boolean = false + @State groupId:string ='0_36' + @State isSelfCodeEmpty:boolean = true + onBackPress(): boolean | void { + this.pageBack = true; + } + + aboutToAppear(): void { + let routerParam = router.getParams() as addRouterParam + if(routerParam !== null) { + this.isSelfCodeEmpty = routerParam.isSelfCodeEmpty as boolean + } + } + + build() { + Column() { + StatusBar() + SelfGroupAddStock({pageBack:this.pageBack,isSelfCodeEmpty:this.isSelfCodeEmpty}) + Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor("#fff0f0f0") + } + .width('100%') + .height('100%') + } +} + +export interface addRouterParam { + isSelfCodeEmpty:boolean +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupRenamePage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupRenamePage.ets new file mode 100644 index 0000000..9b9c413 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupRenamePage.ets @@ -0,0 +1,24 @@ +import { GlobalContext } from 'biz_common'; +import { SelfGroupRename } from 'biz_selfcode'; +import { StatusBar } from '../../widget/StatusBar'; + +@Entry +@Component +struct SelfGroupRenamePage { + @State pageBack:boolean = false + onBackPress(): boolean | void { + this.pageBack = true; + } + build() { + + Column() { + StatusBar() + SelfGroupRename({pageBack:this.pageBack}) + .layoutWeight(1) + Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor("#fff0f0f0") + } + .width('100%') + .height('100%') + + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupSettingsPage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupSettingsPage.ets new file mode 100644 index 0000000..69eb8c4 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfGroupSettingsPage.ets @@ -0,0 +1,23 @@ +import { GlobalContext } from 'biz_common'; +import { SelfGroupSettings } from 'biz_selfcode'; +import { StatusBar } from '../../widget/StatusBar'; + +@Entry +@Component +struct SelfGroupSettingsPage { + @State pageBack:boolean = false + onBackPress(): boolean | void { + this.pageBack = true; + } + + build() { + Column() { + StatusBar() + SelfGroupSettings({pageBack:this.pageBack}) + .layoutWeight(1) + Blank().height(px2vp(GlobalContext.navigationBarHeight)).flexShrink(0).backgroundColor("#fff0f0f0") + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfStockIndexSettingPage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfStockIndexSettingPage.ets new file mode 100644 index 0000000..8ab951d --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfStockIndexSettingPage.ets @@ -0,0 +1,21 @@ +import { PageTitleBar } from "biz_common"; +import { SelfStockIndexComponent } from "biz_market"; +import { StatusBar } from "../../widget/StatusBar"; + +/** + * author : zengyonghao@myhexin.com + * time : 2025/10/9 20:18 + * desc : + * version: 1.0 + */ +@Entry +@Component +export struct SelfStockIndexSettingPage { + build() { + Column() { + StatusBar() + PageTitleBar({ title: '自选指数设置' }) + SelfStockIndexComponent() + }.width('100%').height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/selfcode/SelfStockSettingPage.ets b/future_entry_demo/src/main/ets/pages/selfcode/SelfStockSettingPage.ets new file mode 100644 index 0000000..b010946 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/selfcode/SelfStockSettingPage.ets @@ -0,0 +1,122 @@ +import { GlobalContext, HQPageConstants, PageTitleBar } from 'biz_common' +import { SelfStockGroupSetting, SelfStockSetting } from 'biz_selfcode' +import { StatusBar } from '../../widget/StatusBar' +import { router } from '@kit.ArkUI' +import { TableHeadSettingPage } from 'biz_market/src/main/ets/components/setting/TableHeadSettingContainer' + +@Entry +@Component +export struct SelfStockSettingPage { + @State isPageForeground: boolean = false + @State @Watch('onPageIndexChange') pageIndex: number = 0 + @State groupTabIndex: number = 0 + private swiperController: SwiperController = new SwiperController() + + onPageShow(): void { + this.isPageForeground = true + } + + onPageHide(): void { + this.isPageForeground = false + } + + onPageIndexChange() { + this.swiperController.changeIndex(this.pageIndex) + } + + aboutToAppear(): void { + let params = router.getParams() as Record + if (params) { + let index = params['index'] as number + if (index) { + this.pageIndex = index + } + let groupTab = params['groupTabIndex'] as number + if (groupTab !== undefined) { + this.groupTabIndex = groupTab + } + } + } + + build() { + Flex({ + direction: FlexDirection.Column, + justifyContent: FlexAlign.Start, + alignItems: ItemAlign.Center, + wrap: FlexWrap.NoWrap + }) { + StatusBar() + this.buildPageTitle() + + Swiper(this.swiperController) { + SelfStockSetting({ + isPageForeground: this.isPageForeground, + isShow:this.pageIndex === HQPageConstants.SETTING_PAGE_INDEX_SELF_STOCK + }) + SelfStockGroupSetting({ + onForeground: this.isPageForeground && this.pageIndex === HQPageConstants.SETTING_PAGE_INDEX_GROUP, + initTabIndex: this.groupTabIndex + }) + TableHeadSettingPage({ + onForeground: this.isPageForeground && this.pageIndex === HQPageConstants.SETTING_PAGE_INDEX_HEAD_SETTING + }) + } + .index(this.pageIndex) + .indicator(false) + .disableSwipe(true) + } + .backgroundColor($r('app.color.surface_layer1_foreground')) + .padding({ bottom: px2vp(GlobalContext.navigationBarHeight) }) + .height('100%') + .width('100%') + } + + @Builder + buildPageTitle() { + Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { + Column() { + Image($r("app.media.icon_arrow_back_24")) + .fillColor($r('app.color.elements_icon_primary_02')) + .width(24) + .height(24) + } + .size({ + width: 36, + height: '100%' + }) + .margin({ + left: 8 + }) + .onClick(() => router.back()) + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + + Row({ space: 20 }) { + this.buildSinglePageTitle('自选设置', HQPageConstants.SETTING_PAGE_INDEX_SELF_STOCK) + this.buildSinglePageTitle('分组管理', HQPageConstants.SETTING_PAGE_INDEX_GROUP) + this.buildSinglePageTitle('表头设置', HQPageConstants.SETTING_PAGE_INDEX_HEAD_SETTING) + } + .alignItems(VerticalAlign.Center) + + Column() { + } + .size({ + width: 36, + height: '100%' + }) + }.width('100%') + .height(44) + .backgroundColor($r('app.color.surface_layer1_foreground')) + } + + @Builder + buildSinglePageTitle(name: string, index: number) { + Text(name) + .fontColor(this.pageIndex === index ? $r('app.color.elements_text_primary_02') : $r('app.color.elements_text_primary_02')) + .fontSize(this.pageIndex === index ? 20 : 16) + .fontWeight(this.pageIndex === index ? FontWeight.Medium : FontWeight.Regular) + .onClick(() => { + this.pageIndex = index + }) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/setting/HQTableSettingPage.ets b/future_entry_demo/src/main/ets/pages/setting/HQTableSettingPage.ets new file mode 100644 index 0000000..c92955b --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/setting/HQTableSettingPage.ets @@ -0,0 +1,30 @@ +/** + * author : zengyonghao@myhexin.com + * time : 2025/9/28 15:53 + * desc : + * version: 1.0 + */ +import { PageTitleBar } from 'biz_common'; +import { HQTableSettingComponent } from 'biz_market'; +import { StatusBar } from '../../widget/StatusBar'; + +@Entry +@Component +struct HQTableSettingPage { + onPageShow(): void { + + } + + onPageHide(): void { + + } + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + PageTitleBar({ title: '行情列表设置' }) + HQTableSettingComponent().width('100%').height('100%') + } + .backgroundColor($r('app.color.surface_layer1_background')) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/setting/SystemSettingPage.ets b/future_entry_demo/src/main/ets/pages/setting/SystemSettingPage.ets new file mode 100644 index 0000000..4f24d94 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/setting/SystemSettingPage.ets @@ -0,0 +1,408 @@ +import { ClipUtils, GlobalContext, PreferenceService, PRIVACY_PAGE_URL, USER_AGREEMENT_URL } from 'biz_common'; +import { WebTitleView } from 'biz_login/src/main/ets/view/WebTitleView'; +import { LevelMode, promptAction, window } from '@kit.ArkUI'; +import { common } from '@kit.AbilityKit'; +import { AppUtil, StrUtil } from '@pura/harmony-utils'; +import { CodeTableManager } from 'lib_codetablemanager'; +import { DebugFloatingBall } from '../debug/DebugFloatingBall'; +import BuildProfile from 'biz_common/BuildProfile'; +import router from '@ohos.router' + +interface ISystemSettingInfo { + text: string | Resource, + subText: string | Resource, + clickUrl: string, + pageUrl : string, + fundId?: string, + switchType?: string +} + +const SYSTEM_SETTING_FUNCTION_ID_CLEAR_CACHE = "System_setting_function_id_clear_cache" +const SYSTEM_SETTING_FUNCTION_ID_UPDATE_CODE_DB = "System_setting_function_id_update_db" +const CLIPBOARD_SWITCH_STATE = "ClipBoard_Switch_State" + +const mapSystemSettingInfo: ISystemSettingInfo[] = [ + { + text: $r('app.string.usercenter_system_setting_kline_setting_label'), + subText: '', + pageUrl : 'pages/quote/IndicatorManagerPage', + clickUrl: '' + }, + // { + // text: $r('app.string.usercenter_system_setting_switch_server_label'), + // pageUrl : '', + // clickUrl: '', + // }, + { + text: $r('app.string.usercenter_system_setting_privacy_label'), + subText: $r('app.string.withdraw_agree_sub_title'), + pageUrl : 'pages/WebViewComponentPage', + clickUrl: PRIVACY_PAGE_URL, + }, + { + text: $r('app.string.usercenter_system_setting_user_label'), + subText: '', + pageUrl : 'pages/WebViewComponentPage', + clickUrl: USER_AGREEMENT_URL, + }, + { + text: $r('app.string.usercenter_system_setting_clear_cache'), + subText: '', + pageUrl : '', + clickUrl: '', + fundId:SYSTEM_SETTING_FUNCTION_ID_CLEAR_CACHE + }, + { + text: $r('app.string.clipboard_permission_text'), + subText: '', + pageUrl: '', + clickUrl: '', + switchType: CLIPBOARD_SWITCH_STATE + }, + { + text: $r('app.string.usercenter_system_setting_updateCode'), + subText: '', + pageUrl : '', + clickUrl: '', + fundId:SYSTEM_SETTING_FUNCTION_ID_UPDATE_CODE_DB + } +]; + +@Entry +@Component +struct SystemSettingPage { + + @State isClipboardEnabled: boolean = ClipUtils.getInstance().getClipBoardSwitchState() + + withdrawAgreeDialogController: CustomDialogController = new CustomDialogController({ + builder: CommonDialog({ + title: $r('app.string.withdraw_agree_title'), + content: $r('app.string.withdraw_agree_content'), + cancelText: $r('app.string.withdraw_agree_confirm'), + cancel: () => { + PreferenceService.putVal('Privacy', 'isUserAgree', false, true); + (getContext(this) as common.UIAbilityContext).terminateSelf(() => {}) + }, + confirmText: $r('app.string.withdraw_agree_cancel'), + confirm: () => { + + } + }) + }) + clipBoardSwitchTipDialog: CustomDialogController = new CustomDialogController({ + builder: ClipBoardSwitchTipDialog({ + cancel: () => { + this.isClipboardEnabled = true + ClipUtils.getInstance().setClipBoardSwitchState(this.isClipboardEnabled) + } + }), + levelMode: LevelMode.EMBEDDED, + autoCancel: false, + maskColor: $r('app.color.color_800000'), + width: 280, + cornerRadius: 12 + }) + + progressDialogController: CustomDialogController = new CustomDialogController({ + builder: CommonProgressDialog({ + content: "码表更新中..." + }), + autoCancel:false, + width:'70%', + height:60, + onWillDismiss:(dismissReason)=>{ + if (dismissReason.reason === DismissReason.PRESS_BACK) { + return true + } + return false + }, + backgroundColor:Color.Transparent + + }) + + build() { + Column() { + Row().width('100%').height(px2vp(GlobalContext.statusBarHeight)).backgroundColor($r('app.color.surface_layer1_foreground')).flexShrink(0) + WebTitleView({ title: $r('app.string.usercenter_system_setting_title') }) + .padding({ left: 10, right: 10 }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .gesture( + LongPressGesture({duration:3000}) + .onAction(() => { + if (BuildProfile.BUILD_MODE_NAME.toString() != "release") { + DebugFloatingBall.canShow = true + let windowStage = AppStorage.get('EntryAbilityWindowStage') as window.WindowStage + DebugFloatingBall.createDebugFloatingBall(windowStage) + } + router.pushNamedRoute({name:"DebugPage"}) + }) + ) + Column() { + ForEach(mapSystemSettingInfo, (systemSettingInfo: ISystemSettingInfo, idx: number) => { + Row() { + Text(systemSettingInfo.text) + .fontSize(16) + .layoutWeight(1) + Text(systemSettingInfo.subText) + .fontSize(14) + .fontColor($r('app.color.text_light_color')) + .padding(4) + .onClick(() => { + if (idx == 1) { + this.withdrawAgreeDialogController.open() + } + }) + if (systemSettingInfo.switchType === CLIPBOARD_SWITCH_STATE) { + this.buildClipBoardSwitch() + } else { + Image($r('app.media.icon_arrow_forward_16')).size({ + height: 16, + width: 16 + }) + .fillColor($r('app.color.elements_icon_primary_02')) + } + } + .onClick(() => { + this.onItemClick(systemSettingInfo) + }) + .size({ + width: 'calc(100% - 32vp)', + height: 52 + }) + .borderWidth({ bottom: idx === mapSystemSettingInfo.length - 1 ? 0 : 0.5 }) + .borderColor($r('app.color.elements_others_divider_primary')) + }) + } + .size({ + width: 'calc(100% - 32vp)' + }) + .margin({ + top: 12, + bottom: 12 + }) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(5) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.surface_layer1_background')) + } + + + onItemClick(item:ISystemSettingInfo){ + if (StrUtil.isNotEmpty(item.fundId)) { + switch (item.fundId){ + case SYSTEM_SETTING_FUNCTION_ID_CLEAR_CACHE: + promptAction.showToast({ + message: "清除数据会重启,清除缓存中...", + duration:5000 + }) + AppUtil.getContext().getApplicationContext().clearUpApplicationData(()=>{ + }); + break + case SYSTEM_SETTING_FUNCTION_ID_UPDATE_CODE_DB: + this.progressDialogController.open() + CodeTableManager.get().tryToInitFullDataFromRemote(AppUtil.getContext(),true,(updateResult:boolean) =>{ + this.progressDialogController.close() + promptAction.showToast({message:updateResult?"更新码表成功":"更新码表失败"}) + }) + break; + default : + break + } + }else { + router.pushUrl({ + url: item.pageUrl, + params: { + url: item.clickUrl, + title: item.text, + updateH5Title: '0' + } + }) + } + + } + + @Builder + buildClipBoardSwitch() { + Toggle({ type: ToggleType.Switch, isOn: this.isClipboardEnabled }) + .size({ width: 50, height: 30 }) + .selectedColor($r('app.color.elements_button_bg_primary')) + .onChange((isOn: boolean) => { + this.isClipboardEnabled = isOn; + if (!isOn) { + this.clipBoardSwitchTipDialog.open() + } + ClipUtils.getInstance().setClipBoardSwitchState(isOn) + }) + } +} + +@CustomDialog +struct CommonDialog { + title?: string | Resource + content?: string | Resource + cancelText?: string | Resource + cancel?: () => void + confirmText?: string | Resource + confirm?: () => void + controller: CustomDialogController + scroller: Scroller = new Scroller() + + build() { + Column() { + Text(this.title) + .fontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .fontWeight(FontWeight.Bold) + .align(Alignment.Center) + .margin({ + top: 20 + }) + Scroll(this.scroller) { + + Column() { + + Text(this.content) + .fontSize( 17) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width('100%') + .margin({ + top: 20 + }) + }.width('100%') + }.width('100%').layoutWeight(1) + + Divider().vertical(false).width('100%').height(1).margin({ top: 5 }).color($r('app.color.elements_others_divider_primary')) + Row() { + if (this.cancelText != undefined && this.cancelText != "") { + Text(this.cancelText) + .fontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width('50%') + .height('100%') + .onClick(() => { + this.controller.close() + if (this.cancel) { + this.cancel() + } + }) + Divider().vertical(true).width(1).height('100%').color($r('app.color.elements_others_divider_primary')) + } + + Text(this.confirmText) + .fontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width('50%') + .height('100%') + .onClick(() => { + this.controller.close() + if (this.confirm) { + this.confirm() + } + }) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height(44) + } + .margin({ + left: 30, + right: 30 + }) + } + +} +@CustomDialog +struct CommonProgressDialog { + content?: string | Resource + controller: CustomDialogController + build() { + this.progressLoadingBuilder(this.content) + } + @Builder + progressLoadingBuilder(loadingTip?:string|Resource) { + Row() { + LoadingProgress() + .width(30).height(30) + + Text(loadingTip) + .fontSize(14) + .fontColor($r('app.color.elements_text_primary_02')) + .margin({ left: 10 }) + } + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Center) + .backgroundColor($r('app.color.surface_layer1_foreground')) + .width('100%') + .height(60) + } +} + +@CustomDialog +struct ClipBoardSwitchTipDialog { + cancel?: () => void + controller: CustomDialogController + + build() { + Column() { + Text($r('app.string.clipboard_permission_title')) + .fontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .fontWeight(FontWeight.Bold) + .align(Alignment.Center) + .margin({ + top: 20 + }) + + Text($r('app.string.clipboard_permission_dialog_tip')) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Start) + .width('100%') + .margin({ + top: 16 + }) + .padding({ left: 14, right: 14 }) + + Divider() + .vertical(false) + .width('100%') + .height(1) + .margin({ top: 16 }) + .color($r('app.color.elements_others_divider_primary')) + Row() { + Text($r('app.string.clipboard_permission_cancel')) + .fontSize(17) + .fontColor($r('app.color.elements_text_primary_02')) + .textAlign(TextAlign.Center) + .width('50%') + .height('100%') + .onClick(() => { + this.controller.close() + if (this.cancel) { + this.cancel() + } + }) + Divider().vertical(true).width(1).height('100%').color($r('app.color.elements_others_divider_primary')) + + Text($r('app.string.clipboard_permission_close')) + .fontSize(17) + .fontColor($r('app.color.elements_button_bg_primary')) + .textAlign(TextAlign.Center) + .width('50%') + .height('100%') + .onClick(() => { + this.controller.close() + }) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height(44) + } + .width('100%') + .backgroundColor($r('app.color.surface_layer3_background')) + } +} diff --git a/future_entry_demo/src/main/ets/pages/test/CodeTableTestPage.ets b/future_entry_demo/src/main/ets/pages/test/CodeTableTestPage.ets new file mode 100644 index 0000000..edeb846 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/test/CodeTableTestPage.ets @@ -0,0 +1,14 @@ +import { CodeTableApiTestComponent } from "lib_codetablemanager"; +import { StatusBar } from "../../widget/StatusBar"; + +@Entry +export struct CodeTableTestPage { + build() { + Column() { + StatusBar() + CodeTableApiTestComponent() + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/test/DeleteStockTestPage.ets b/future_entry_demo/src/main/ets/pages/test/DeleteStockTestPage.ets new file mode 100644 index 0000000..8ebbe61 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/test/DeleteStockTestPage.ets @@ -0,0 +1,14 @@ +import { DeleteStockComponent } from "lib_codetablemanager"; +import { StatusBar } from "../../widget/StatusBar"; + +@Entry +export struct DeleteStockTestPage { + build() { + Column() { + StatusBar() + DeleteStockComponent() + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/account/AddAccountPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/account/AddAccountPageContainer.ets new file mode 100644 index 0000000..f53585b --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/account/AddAccountPageContainer.ets @@ -0,0 +1,25 @@ +// import {AddAccountPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct AddAccountPageContainer { + @Provide pageShow: boolean = false; + + build() { + Column() { + StatusBar() + // AddAccountPage() + } + .width('100%') + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/account/ChooseProvincePageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseProvincePageContainer.ets new file mode 100644 index 0000000..74fca32 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseProvincePageContainer.ets @@ -0,0 +1,19 @@ +import { GlobalContext } from 'biz_common' +// import {ChooseProvincePage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct ChooseProvincePageContainer { + @State message: string = 'Hello World' + + build() { + Flex({direction: FlexDirection.Column}) { + StatusBar() + // ChooseProvincePage() + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_foreground_page')).flexShrink(0) + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/account/ChooseQSPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseQSPageContainer.ets new file mode 100644 index 0000000..d1b7bb8 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseQSPageContainer.ets @@ -0,0 +1,16 @@ +import { GlobalContext } from 'biz_common' +// import {ChooseQSPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct ChooseQSPageContainer { + + build() { + Flex({direction: FlexDirection.Column}) { + StatusBar() + // ChooseQSPage() + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_foreground_page')).flexShrink(0) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/account/ChooseYYBPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseYYBPageContainer.ets new file mode 100644 index 0000000..c426e82 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/account/ChooseYYBPageContainer.ets @@ -0,0 +1,19 @@ +import { GlobalContext } from 'biz_common' +// import {ChooseYYBPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct ChooseYYBPageContainer { + @State message: string = 'Hello World' + + build() { + Flex({direction: FlexDirection.Column}) { + StatusBar() + // ChooseYYBPage() + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_foreground_page')).flexShrink(0) + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/account/WTAccountManagerPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/account/WTAccountManagerPageContainer.ets new file mode 100644 index 0000000..205a3cd --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/account/WTAccountManagerPageContainer.ets @@ -0,0 +1,19 @@ +import { GlobalContext } from 'biz_common' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct WTAccountManagerPageContainer { + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // ManageAccountPage() + // Blank() + // .height(px2vp(GlobalContext.navigationBarHeight)) + // .backgroundColor($r('app.color.wt_foreground_page')) + // .flexShrink(0) + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoBuyPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoBuyPageContainer.ets new file mode 100644 index 0000000..1d9367c --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoBuyPageContainer.ets @@ -0,0 +1,28 @@ +// import { RepurchaseTabPage } from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WTGovBondRepoBuyPageContainer { + @Provide pageShow: boolean = false; + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // RepurchaseTabPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor('app.color.wt_foreground_page').flexShrink(0) + } + .height('100%') + } + + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoPageContainer.ets new file mode 100644 index 0000000..9e92826 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/bondRepo/WTGovBondRepoPageContainer.ets @@ -0,0 +1,28 @@ +// import {GovBondRepoPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WTGovBondRepoPageContainer { + @Provide pageShow: boolean = false; + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // GovBondRepoPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor('app.color.wt_foreground_page').flexShrink(0) + } + .height('100%') + } + + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/chaxun/WTTradeRecordPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/chaxun/WTTradeRecordPageContainer.ets new file mode 100644 index 0000000..c4e490d --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/chaxun/WTTradeRecordPageContainer.ets @@ -0,0 +1,30 @@ +// import { TradeRecordPage } from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WTTradeRecordPageContainer { + + @Provide pageShow: boolean = false; + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // TradeRecordPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_foreground_page')).flexShrink(0) + } + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/chicang/WTPositionViewPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/chicang/WTPositionViewPageContainer.ets new file mode 100644 index 0000000..b69861c --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/chicang/WTPositionViewPageContainer.ets @@ -0,0 +1,43 @@ +import { GlobalContext } from 'biz_common'; +// import { WTPanelPage } from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { emitter } from '@kit.BasicServicesKit'; +// import EventConstants from 'biz_trade/src/main/ets/constants/EventConstants'; + +@Entry +@Component +struct WTPositionViewPageContainer { + @Provide pageShow: boolean = false; + /** + * wt用户登录之后的 用户信息标识,改值变化了,就表示收到了用户推送 + */ + @Provide userInfoFlag: number = 0; + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // WTPanelPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_foreground_page')).flexShrink(0) + } + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + // emitter.off(EventConstants.ID_RECEIVE_USER_INFO) + // emitter.on({ + // // 开启事件订阅 + // eventId: EventConstants.ID_RECEIVE_USER_INFO, // 指定事件的ID + // priority: emitter.EventPriority.IMMEDIATE // 事件的优先级 + // }, (data) => { // 事件回调 + // this.userInfoFlag++; + // }) + } + + onPageHide(): void { + this.pageShow = false; + // emitter.off(EventConstants.ID_RECEIVE_USER_INFO) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/newstockapply/WTNewStockApplyPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/newstockapply/WTNewStockApplyPageContainer.ets new file mode 100644 index 0000000..ee4fc1a --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/newstockapply/WTNewStockApplyPageContainer.ets @@ -0,0 +1,29 @@ +// import {NewStockApplyPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WTNewStockApplyPageContainer { + @Provide pageShow: boolean = false; + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // NewStockApplyPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor('app.color.wt_foreground_page').flexShrink(0) + } + // .backgroundColor($r('app.color.wt_foreground_page')) + .height('100%') + } + + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/tradeset/WTTradeSetPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/tradeset/WTTradeSetPageContainer.ets new file mode 100644 index 0000000..840baa7 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/tradeset/WTTradeSetPageContainer.ets @@ -0,0 +1,30 @@ +// import { TradeSetPage } from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WTTradeSetPageContainer { + + @Provide pageShow: boolean = false; + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // TradeSetPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_background_page')).flexShrink(0) + } + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/tradeset/functionswitch/LimitPriceSetPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/tradeset/functionswitch/LimitPriceSetPageContainer.ets new file mode 100644 index 0000000..03e90e0 --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/tradeset/functionswitch/LimitPriceSetPageContainer.ets @@ -0,0 +1,30 @@ +// import { LimitPriceSetPage } from 'biz_trade' +import { StatusBar } from '../../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct LimitPriceSetPageContainer { + + @Provide pageShow: boolean = false; + + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // LimitPriceSetPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_background_page')).flexShrink(0) + } + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoHistoryTransferListPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoHistoryTransferListPageContainer.ets new file mode 100644 index 0000000..bb4d7dd --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoHistoryTransferListPageContainer.ets @@ -0,0 +1,23 @@ +// import {WeituoTransferHistoryListPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' + +@Entry +@Component +struct WeituoHistoryTransferListPageContainer { + @Provide pageShow: boolean = false; + build() { + Column() { + StatusBar() + // WeituoTransferHistoryListPage() + } + .height('100%') + } + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoTransferPageContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoTransferPageContainer.ets new file mode 100644 index 0000000..0a5732f --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/transfer/WeituoTransferPageContainer.ets @@ -0,0 +1,28 @@ +// import {TransferPage} from 'biz_trade' +import { StatusBar } from '../../../widget/StatusBar' +import { GlobalContext } from 'biz_common'; + +@Entry +@Component +struct WeituoTransferPageContainer { + @Provide pageShow: boolean = false; + build() { + Flex({ direction: FlexDirection.Column }) { + StatusBar() + // TransferPage() + // .flexGrow(1) + // .flexBasis('0%') + Blank().height(px2vp(GlobalContext.navigationBarHeight)).backgroundColor($r('app.color.wt_background_page')).flexShrink(0) + } + .height('100%') + } + + + onPageShow(): void { + this.pageShow = true; + } + + onPageHide(): void { + this.pageShow = false; + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/pages/weituo/utils/WTDebugUtilWindowContainer.ets b/future_entry_demo/src/main/ets/pages/weituo/utils/WTDebugUtilWindowContainer.ets new file mode 100644 index 0000000..63c76bc --- /dev/null +++ b/future_entry_demo/src/main/ets/pages/weituo/utils/WTDebugUtilWindowContainer.ets @@ -0,0 +1,15 @@ +// import { WTDebugUtilWindowView } from 'biz_trade' + +@Entry +@Component +struct WTDebugUtilWindowContainer { + + build() { + Column(){ + // WTDebugUtilWindowView() + // .width('100%') + // .height('100%') + } + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/register/cbasregister/CbasServiceRegister.ets b/future_entry_demo/src/main/ets/register/cbasregister/CbasServiceRegister.ets new file mode 100644 index 0000000..70fb83d --- /dev/null +++ b/future_entry_demo/src/main/ets/register/cbasregister/CbasServiceRegister.ets @@ -0,0 +1,55 @@ +import { DeviceInfoStore } from "@b2c-m/ohos_hx_event_capture" +import { bundleManager } from "@kit.AbilityKit" +import { emitter } from "@kit.BasicServicesKit" +import { EmitterConstants } from "biz_common" +import { HxCBASAgentProvider, HxCBASConfig } from "@b2c-spi/hx_cbas_api" + +const THS_APP_KEY: string = '9099f8be61' +const BASE_URL:string = 'https://cbasspider.10jqka.com.cn:8443' +class CbasServiceRegister{ + + async init(udid: string) { + //初始化udid信息 + DeviceInfoStore.getInstance().setUdid(udid) + //获取bundle信息 + const bundleInfo = + await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) + + await HxCBASAgentProvider.getInstance() + .init(new HxCBASConfig(bundleInfo.name, BASE_URL, THS_APP_KEY, bundleInfo.versionName,true)) + + //注册网络状态 + this.registerNetStatus() + } + + registerNetStatus() { + emitter.on({ eventId: EmitterConstants.NETWORK_DISCONNECT }, this.netDisconnect) + emitter.on({ eventId: EmitterConstants.NETWORK_RECONNECT }, this.netRelinkCb) + } + + setUserId(userId: string) { + this.setBizUserId(userId) + } + + private setBizUserId(userId:string) { + HxCBASAgentProvider.getInstance().setUserId(userId) + } + + private netRelinkCb = () => { + HxCBASAgentProvider.getInstance().resetNetStatus() + } + private netDisconnect = () => { + HxCBASAgentProvider.getInstance().setNetDisconnected() + } + + public onForeground() { + HxCBASAgentProvider.getInstance().onForeground() + } + + public onBackground() { + HxCBASAgentProvider.getInstance().onBackground() + + } +} + +export const cbasService = new CbasServiceRegister(); diff --git a/future_entry_demo/src/main/ets/register/elkregister/ElkUploadRegister.ets b/future_entry_demo/src/main/ets/register/elkregister/ElkUploadRegister.ets new file mode 100644 index 0000000..0141496 --- /dev/null +++ b/future_entry_demo/src/main/ets/register/elkregister/ElkUploadRegister.ets @@ -0,0 +1,40 @@ +import { ElkService} from 'service_monitor' +import { UgcUploadService } from 'service_watchlist' +import { ServiceUgcElkUploadRegister } from './ServiceUgcElkUploadRegister' +import { emitter } from '@kit.BasicServicesKit'; +import { EmitterConstants } from 'biz_common'; + +export class ElkUploadRegister { + private coldStart:boolean = true; + private static instance : ElkUploadRegister + public static getInstance() : ElkUploadRegister { + if(!ElkUploadRegister.instance) { + ElkUploadRegister.instance = new ElkUploadRegister() + } + return ElkUploadRegister.instance + } + + /** + * 下沉业务请在这里注册自己的elk upload回调 + * */ + + public async init() { + UgcUploadService.registerElkUploadService(ServiceUgcElkUploadRegister.uploadCallback) + this.registerNetStatus() + } + + private registerNetStatus() { + emitter.on({eventId: EmitterConstants.NETWORK_RECONNECT},this.netStatusCb) + } + + private netStatusCb = () => { + if(this.coldStart) { + this.coldStart = false + ElkService.getInstance().loadRemainedTask() + //readFile + } else { + //retry failed + ElkService.getInstance().retryFailedTask() + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/register/elkregister/ServiceUgcElkUploadRegister.ets b/future_entry_demo/src/main/ets/register/elkregister/ServiceUgcElkUploadRegister.ets new file mode 100644 index 0000000..73569ab --- /dev/null +++ b/future_entry_demo/src/main/ets/register/elkregister/ServiceUgcElkUploadRegister.ets @@ -0,0 +1,56 @@ +import { Message } from '@ohos/protobufjs'; +import { RuntimeStatus } from 'biz_common/src/main/ets/runtime/RuntimeStatus'; +import { ElkUploadMessage, ElkService } from 'service_monitor'; +import { ElkUploadMessageBuilder } from 'service_monitor/src/main/ets/elk/bean/UploadMessage' +import { ElkMessageUploadCallBackOfUgc, ExtraInfo } from 'service_watchlist'; +import { UgcAuthListener } from '../ugcregister/UgcAuthListener'; +import { UgcSelfCodeClient } from '../ugcregister/UgcSelfCodeClient'; + +/** + * ugc elk 上报注册 + * 通过回调注入的方式实现以保证ugc模块的外部能力是注入的从而规避外部引用 + * @author : weiyiguang@myhexin.com + * */ +export class ServiceUgcElkUploadRegister { + static readonly uploadCallback : ElkMessageUploadCallBackOfUgc = ( + biz_level:string, info:ExtraInfo, isInstant:boolean + ) => { + const messageBuilder = new UgcElkBuilder(biz_level) + + if(info) { + messageBuilder.userid(info.uid) + messageBuilder.messageKey(info?.message_key ? info.message_key : '' ) + messageBuilder.messageValue(info?.message_value ? info?.message_value : '') + messageBuilder.messageExt((info?.message_ext ? info?.message_ext: '') + ServiceUgcElkUploadRegister.addExtraInfo()) + //网络状态 + messageBuilder.netStatus(RuntimeStatus.getInstance().getNetStatusStr()) + } + const message = new ElkUploadMessage(messageBuilder) + + ElkService.getInstance().pushBusinessLog(message,isInstant ) + } + + static addExtraInfo():string { + let info : string = '\n ExtraInfo :'; + if(UgcAuthListener.getInstance().isAuthDataPrepared()) { + info = info + ` UgcAuthListener : isConsistency : true` + } else { + info = info + ` UgcAuthListener : isConsistency : false` + } + info = info + ` UgcAuthListener : userid : ${UgcAuthListener.getInstance().getUserId()}` + info = info + ` last sync selfcodeVersion : ${UgcSelfCodeClient.getInstance().getClientSelfCodeVersion()}, + selfGroupVersion ${UgcSelfCodeClient.getInstance().getClientSelfGroupVersion()}` + + return info + } + +} + +class UgcElkBuilder extends ElkUploadMessageBuilder { + constructor(biz_level: string) { + super(Date.now(), "service_watchlist", biz_level); + } +} + + + diff --git a/future_entry_demo/src/main/ets/register/ugcregister/UgcAuthListener.ets b/future_entry_demo/src/main/ets/register/ugcregister/UgcAuthListener.ets new file mode 100644 index 0000000..4414a4c --- /dev/null +++ b/future_entry_demo/src/main/ets/register/ugcregister/UgcAuthListener.ets @@ -0,0 +1,140 @@ +import { IAuthUgcListener } from 'biz_auth'; +import { RuntimeStatus } from 'biz_common'; +import { HXLog } from 'biz_common/src/main/ets/logger/HXLog'; +import { HxTags } from 'biz_common/src/main/ets/tags/DebugTag'; +import { DebugToolUtil } from 'biz_debug'; +import { CookieManager } from 'biz_hxservice'; +import { buildEntryUploadBasicInfo, EntryMonitor } from '../../monitor/EntryMonitor'; + +const TAG: string = 'UgcAuthListener' + +export class UgcAuthListener implements IAuthUgcListener { + private static instance: UgcAuthListener; + private subscribers: Array<(userId: string, userName: string, cookie: string) => void> = [] + private userId: string = '' + private userName: string = '' + private cookie: string = '' + private consistency: boolean = false + private timerId: number | null = null; // 计时器 ID,用于管理 setTimeout + private startTime: number | null = null; // 开始时间 + + static getInstance(): UgcAuthListener { + if (!UgcAuthListener.instance) { + UgcAuthListener.instance = new UgcAuthListener() + } + return UgcAuthListener.instance + } + + onAuthSucceeded(userId: string, userName: string): void { + this.recordCookieTime(true) + this.userId = userId + this.userName = userName + //从cookie缓存中优先读取 + this.cookie = CookieManager.getInstance().getCookieSync() + HXLog.i(TAG, HxTags.entryTags + TAG + ' onAuthSucceeded, restored cookie = ' + this.cookie) + if (this.isCookieConsistency()) { + this.clearRecordTask() + this.notifySubscribers() + this.consistency = true + } else { + this.consistency = false + } + } + + onCookieUpdated(cookie: string): void { + this.cookie = cookie + HXLog.i(TAG, HxTags.entryTags + TAG + ' onCookie Updated') + if (this.isCookieConsistency()) { + this.consistency = true + this.recordCookieTime(false) + this.notifySubscribers() + } else { + this.consistency = false + } + } + + isAuthDataPrepared(): boolean { + return this.consistency + } + + getListenerInstance(): IAuthUgcListener { + return UgcAuthListener.instance + } + + addAuthSubscriber(subscriber: (userId: string, userName: string, cookie: string) => void): void { + this.subscribers.push(subscriber) + } + + private notifySubscribers(): void { + this.subscribers.forEach((cb, index) => { + cb(this.userId, this.userName, this.cookie) + }) + } + + /** + * 检查当前的cookie是否与认证的用户信息一致 + * @returns 如果当前cookie一致且有效 + */ + private isCookieConsistency(): boolean { + if (DebugToolUtil.isInnerNetSimulateMode()) { + return true // 内网debug模式请求不到cookie,强制通过一致性校验 + } + const regex = new RegExp("userid=(\\d+);"); + const match = regex.exec(this.cookie) + if (match) { + const userId = match[1] + return (userId && userId === this.userId) ? true : false + } else { + return false + } + } + + private recordCookieTime(startOrFinish: boolean) { + if (startOrFinish) { + this.clearRecordTask(); + // 记录当前时间戳 + this.startTime = Date.now(); + HXLog.i(TAG, HxTags.entryTags + TAG + ' start record time of cookie sync ') + // 设置5秒超时 + this.timerId = setTimeout(() => { + // 超时后,将开始时间清除 + this.startTime = null; + //上报 + let uploadMessage: string = + ` Timer id ${this.timerId} record timeout finished of 5000ms, cookie not synced, time out! Current userId: ${this.userId} + , current Net status ${RuntimeStatus.getInstance().getNetStatusStr()}` + HXLog.e(TAG, HxTags.entryTags + TAG + uploadMessage) + EntryMonitor.getInstance() + .UploadElkMessage('e', buildEntryUploadBasicInfo(uploadMessage, 'recordCookieTime'), true) + }, 5000); + HXLog.i(TAG, HxTags.entryTags + TAG + `start time on timerid ${this.timerId}`) + } else { + if (this.startTime) { + const currentTime = Date.now(); + const interval = currentTime - this.startTime; // 以秒为单位计算时间间隔 + HXLog.i(TAG, HxTags.entryTags + TAG + `record time finished : ${interval} ms passed, cookied sync succeeded`) + if (this.timerId != null) { + HXLog.i(TAG, HxTags.entryTags + TAG + `before clear on timerid ${this.timerId}`) + clearTimeout(this.timerId) + } + } + } + } + + /** + * 清除cookie的获取计时器 + */ + private clearRecordTask() { + if (this.timerId) { + clearTimeout(this.timerId); + } + } + + getUserName() { + return this.userName + } + + getUserId() { + return this.userId + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/register/ugcregister/UgcClient.ets b/future_entry_demo/src/main/ets/register/ugcregister/UgcClient.ets new file mode 100644 index 0000000..8553197 --- /dev/null +++ b/future_entry_demo/src/main/ets/register/ugcregister/UgcClient.ets @@ -0,0 +1,226 @@ +import { AppConfigManager, EmitterConstants, HXLog, HxTags } from 'biz_common'; +import { emitter } from '@kit.BasicServicesKit'; +import { CookieManager, HxServiceExportConstants, HXUserService } from 'biz_hxservice'; +import { readCachedGroup, UgcHeaderService, UgcUserService } from 'service_watchlist'; +import { UgcSelfCodeClient } from './UgcSelfCodeClient'; +import { UgcAuthListener } from './UgcAuthListener'; +import { MonitorHeaderService } from 'service_monitor'; +import { AIGroupClient } from 'service_aigroup'; + +const UTAG:string = 'UgcClient' +export class UgcClient { + private static instance: UgcClient = new UgcClient() + + public static getInstance() { + if(!UgcClient.instance) { + UgcClient.instance = new UgcClient() + } + return UgcClient.instance + } + + private constructor() { + } + + private authCb = (userId:string, userName:string, cookie:string) => { + HXLog.d(UTAG, HxTags.entryTags + UTAG + ` auth CB userid : ${userId}, username : ${userName}, cookie : ${cookie}`) + this.registerUser() + this.registerHeader() + UgcSelfCodeClient.getInstance().onTempUserAuthSucceed() + UgcSelfCodeClient.getInstance().onSelfCodeRemoteChanged() + if(userName.startsWith('mt_')) { + UgcSelfCodeClient.getInstance().onSelfGroupRemoteChanged() + } + // AI 分组:认证成功后下载云端数据 + AIGroupClient.getInstance().onAuthSucceeded(userId) + } + public init() { + this.subscribeEmitterLogin() + this.subscribeEmitterLogout() + this.subscribeCookieUpdate() + HXLog.d(UTAG,HxTags.entryTags + `ugc client init at time : ${Date.now()}`) + UgcAuthListener.getInstance().addAuthSubscriber(this.authCb) + + } + + private subscribeEmitterLogin() { + emitter.on({eventId:EmitterConstants.USER_LOGIN}, () => { + HXLog.d(UTAG,HxTags.entryTags + ' subscribe login UgcClient triggerd at '+ Date.now()) + this.registerUser() + readCachedGroup().then(() => { + UgcClientEventFSM.getInstance().eventTrigger('USER_LOGIN') + }) + }) + } + + private subscribeEmitterLogout() { + emitter.on({eventId:EmitterConstants.USER_LOGOUT}, () => { + this.registerUser() + UgcClientEventFSM.getInstance().eventTrigger('USER_LOGOUT') + }) + } + + private subscribeCookieUpdate() { + emitter.on(HxServiceExportConstants.COOKIE_UPDATE, () => { + this.registerHeader() + UgcClientEventFSM.getInstance().eventTrigger('COOKIE_UPDATE') + }) + } + + public onHXSocketClose() { + UgcClientEventFSM.getInstance().eventTrigger('CONNECTION_CLOSE') + } + + onSelfCodeRemoteVersion(remoteVersion:number) { + UgcSelfCodeClient.getInstance().setSelfCodeRemoteVersion(remoteVersion) + UgcClientEventFSM.getInstance().eventTrigger('SELFCODE_REMOTE_UPDATE') + if(UgcAuthListener.getInstance().isAuthDataPrepared()) { + UgcSelfCodeClient.getInstance().onSelfCodeRemoteChanged() + } + } + + onSelfGroupRemoteVersion(remoteVersion:number) { + UgcSelfCodeClient.getInstance().setSelfGroupRemoteVersion(remoteVersion) + UgcClientEventFSM.getInstance().eventTrigger('SELFGROUP_REMOTE_UPDATE') + if(UgcAuthListener.getInstance().isAuthDataPrepared()) { + UgcSelfCodeClient.getInstance().onSelfGroupRemoteChanged() + } + } + + private registerUser() { + UgcUserService.getInstance().handleUserStatusUpdate(HXUserService.getInstance() + .getUserId(), HXUserService.getInstance().getUserName()) + } + + private registerHeader() { + let header: Record = { + 'User-Agent': AppConfigManager.getInstance().getUA(), + 'userid': HXUserService.getInstance().getUserId(), + 'Connection': 'keep-alive', + 'Cookie': CookieManager.getInstance().getCookieSync() + } + this.registerUgcServiceHeader(header); + } + + private registerUgcServiceHeader(header: Record) { + UgcHeaderService.getInstance().handleHeaderUpdate(header) + MonitorHeaderService.getInstance().handleHeaderUpdate(header) + + } + +} +const UgcClientEventFSMTAG: string = 'UgcEventFSM' +//简易有限状态机,规避了cookieUpdate的异步返回问题,对USER_LOGIN, USER_LOGOUT, 'cookieUpdate'等eventId解耦 +class UgcClientEventFSM { + private currentState : State = 'INITIAL'; + private events:Set = new Set(); + private formerState:Array = [] + private constructor() { + this.reset() + } + private static instance : UgcClientEventFSM = new UgcClientEventFSM() + + public static getInstance() { + if(!UgcClientEventFSM.instance) { + UgcClientEventFSM.instance = new UgcClientEventFSM() + } + return UgcClientEventFSM.instance + } + + private transition(state:State) { + if( (state === 'SELFCODE_REMOTE_SYNC' || state === 'SELFGROUP_REMOTE_SYNC' ) && !( this.alreadyLogin())) { + return; + } + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +`Transitioning from ${this.currentState} to ${state}`); + this.currentState = state; + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +'Transitioning this.currentState = '+ state) + this.onStateChanged(this.currentState) + this.formerState.push(state) + } + + private onStateChanged(state:State) { + this.events.clear() + switch (state) { + case 'TEMP_AUTH': + //TODO:模拟手炒的临时用户退出登陆后一样重置自选股, 执行重置自选操作 + //此时本地userId和本地cookie均ready,可以执行请求 + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +`FSM TEMP_AUTH triggered at ${Date.now()}`); + UgcSelfCodeClient.getInstance().onTempUserAuthSucceed() + break; + case 'FULL_AUTH': + //TODO:对比远端版本号和本地版本号,不一致就查询,否则本地获取,让selfcode handler处理 + //此时本地userId和本地cookie均ready,同时已经获取了两个版本号,可以执行selfcode handler + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +`FSM FULL_AUTH triggered at ${Date.now()}`); + UgcSelfCodeClient.getInstance().onUserAuthSucceed() + break; + case 'SELFCODE_REMOTE_SYNC': + //TODO:只有在FULL_AUTH之后才能进入同步状态,直接执行查询即可,让selfcode handler处理 + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +`FSM SELFCODE_REMOTE_SYNC triggered at ${Date.now()}`); + //UgcSelfCodeClient.getInstance().onSelfCodeRemoteChanged() + //直接执行 + break; + case 'SELFGROUP_REMOTE_SYNC' : + //TODO:只有在FULL_AUTH之后才能进入group 同步状态,直接执行查询即可,让selfgroup handler处理 + //直接执行 + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +`FSM SELFGROUP_REMOTE_SYNC triggered at ${Date.now()}`); + //UgcSelfCodeClient.getInstance().onSelfGroupRemoteChanged() + break; + case 'INITIAL': + break; + default : + break; + } + } + + private alreadyLogin() { + return this.currentState === 'FULL_AUTH' || this.currentState === 'SELFCODE_REMOTE_SYNC'|| this.currentState === 'SELFGROUP_REMOTE_SYNC' + } + + private checkTransitions() { + HXLog.d(UgcClientEventFSMTAG,HxTags.entryTags +'Fsm has events: ' + Array.from(this.events)) + if (!HXUserService.getInstance().isTemporaryUser() && this.events.has('COOKIE_UPDATE')) { + this.transition('FULL_AUTH'); + } else if(this.currentState === 'TEMP_AUTH') { + this.transition('TEMP_AUTH') + } else if (this.events.has('USER_LOGOUT')) { + this.transition('TEMP_AUTH'); + } else if (this.events.has('SELFCODE_REMOTE_UPDATE')) { + this.transition('SELFCODE_REMOTE_SYNC'); + } else if (!HXUserService.getInstance().isTemporaryUser() && this.events.has('SELFGROUP_REMOTE_UPDATE')) { + this.transition('SELFGROUP_REMOTE_SYNC'); + } else if(this.events.has('CONNECTION_CLOSE')) { + this.reset() + //TODO:除了reset应该还要加入保存本地的相关逻辑 + } + } + + + public eventTrigger(event:Event) { + this.events.add(event); + this.checkTransitions() + } + + private reset() { + this.events.clear(); + this.currentState = 'INITIAL'; + this.formerState = []; + this.transition('INITIAL'); + } + + public getCurrentState() { + return this.currentState; + } + +} + +type Event = 'USER_LOGIN' | 'SELFCODE_REMOTE_UPDATE' | 'COOKIE_UPDATE' | 'USER_LOGOUT'| 'SELFGROUP_REMOTE_UPDATE'|'CONNECTION_CLOSE'; +type State = 'INITIAL' | 'TEMP_AUTH' | 'FULL_AUTH' | 'SELFCODE_REMOTE_SYNC' | 'SELFGROUP_REMOTE_SYNC' + +class SelfcodeUpdateData { + userid:string ='' + version:string = '' +} + + +interface SelfcodeRemoteData { + selfcodeUpdateData:SelfcodeUpdateData +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/register/ugcregister/UgcSelfCodeClient.ets b/future_entry_demo/src/main/ets/register/ugcregister/UgcSelfCodeClient.ets new file mode 100644 index 0000000..ad3501d --- /dev/null +++ b/future_entry_demo/src/main/ets/register/ugcregister/UgcSelfCodeClient.ets @@ -0,0 +1,135 @@ +import { + getSelfCodeContent, + getSelfCodeUpdateClient, + getSelfCodeVersion, + getSelfGroupVersion, + initSelfCodeSync, + ISelfCodeUpdateHandler, + loadSelfCodeSync, + QuerySelfCode, + queryWatchlistGroup, + UgcErrorCode, + UgcUserService +} from 'service_watchlist'; +import { emitter } from '@kit.BasicServicesKit'; +import { EmitterConstants, HXLog, HxTags, StartManager } from 'biz_common'; +import { HXUserService } from 'biz_hxservice'; + +const TAG: string = 'UgcSelfCodeClient' + +export class UgcSelfCodeClient { + private static instance: UgcSelfCodeClient = new UgcSelfCodeClient() + private selfCodeHandlerInstance: ISelfCodeUpdateHandler | null = null + private remoteSelfCodeVersion: number = 0; + private remoteSelfGroupVersion: number = 0; + + public static getInstance() { + if (!UgcSelfCodeClient.instance) { + UgcSelfCodeClient.instance = new UgcSelfCodeClient() + } + return UgcSelfCodeClient.instance + } + + private constructor() { + this.selfCodeHandlerInstance = getSelfCodeUpdateClient() + } + + onHXSocketClose() { + this.remoteSelfCodeVersion = 0; + this.remoteSelfGroupVersion = 0; + } + + onSelfCodeRemoteChanged() { + let localVersion: number = getSelfCodeVersion() + HXLog.i(TAG, HxTags.entryTags + + ` selfcode remote changed and current version ${localVersion}, + remoteVersion is ${this.remoteSelfCodeVersion}, get current selfcode content is ${getSelfCodeContent()}`) + + if (localVersion < this.remoteSelfCodeVersion) { + this.selfCodeHandlerInstance?.handleUpdate().then((result: string) => { + if (Number(result) === UgcErrorCode.UGC_SUCCESS) { + HXLog.i(TAG, HxTags.entryTags + + ` selfcode query succeeded with content ${getSelfCodeContent()} and local version is ${getSelfCodeVersion()}`) + emitter.emit({ eventId: EmitterConstants.SELFCODE_UGC_QUERY_SUCCEEDED }) + } + }) + } else { + //本机主动更新自选成功 + emitter.emit({ eventId: EmitterConstants.SELFCODE_UGC_QUERY_SUCCEEDED }) + } + } + + setSelfCodeRemoteVersion(remoteVersion: number) { + this.remoteSelfCodeVersion = remoteVersion + } + + onSelfGroupRemoteChanged() { + let localVersion: number = getSelfGroupVersion() + HXLog.i(TAG, HxTags.entryTags + + ` selfgroup remote changed and current version ${localVersion}, + remoteVersion is ${this.remoteSelfGroupVersion}`) + if (localVersion < this.remoteSelfGroupVersion) { + queryWatchlistGroup().then((value: number) => { + if (value === 0) { + emitter.emit({ eventId: EmitterConstants.SELFGROUP_UPDATE_QUERY_SUCCEEDED }) + } + }) + } else { + //本机主动更新自选成功 + emitter.emit({ eventId: EmitterConstants.SELFGROUP_UPDATE_QUERY_SUCCEEDED }) + } + } + + setSelfGroupRemoteVersion(remoteGroupVersion: number) { + this.remoteSelfGroupVersion = remoteGroupVersion + } + + //handleFullAuth: + public onUserAuthSucceed() { + let result = loadSelfCodeSync() + let localVersion: number = getSelfCodeVersion() + HXLog.i(TAG, HxTags.entryTags + + ` on UserAuth Succeededlocalselfcodeversion ${localVersion} and remoteVersion ${this.remoteSelfCodeVersion}`) + this.selfCodeHandlerInstance?.handleUpdate().then((result: string) => { + if (Number(result) === UgcErrorCode.UGC_SUCCESS) { + emitter.emit({ eventId: EmitterConstants.SELFCODE_UGC_QUERY_SUCCEEDED }) + } + }) + + queryWatchlistGroup() + } + + //handleTempAuth + public onTempUserAuthSucceed() { + //不是首次安装后进入,理应有缓存 + if (!StartManager.getInstance().getAppInstallStatus()) { + UgcUserService.getInstance().handleUserStatusUpdate(HXUserService.getInstance() + .getUserId(), HXUserService.getInstance().getUserName()) + + let result = loadSelfCodeSync() + //TODO:报错码统一处理【 + if (result === '0') { + //本地读文件错误,必须查询, + //是不是临时用户都可以查,UGC服务器支持临时用户数据缓存了 + this.selfCodeHandlerInstance?.handleUpdate().then((result: string) => { + if (Number(result) === UgcErrorCode.UGC_SUCCESS) { + emitter.emit({ eventId: EmitterConstants.SELFCODE_UGC_QUERY_SUCCEEDED }) + } + }) + } + } else { + //首次安装后肯定还是临时用户,执行init + if(!HXUserService.getInstance().isLoggedIn()) { + initSelfCodeSync() + } + } + } + + getClientSelfCodeVersion() { + return this.remoteSelfCodeVersion + } + + getClientSelfGroupVersion() { + return this.remoteSelfGroupVersion + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/selfcode/SelfCodeEvents.ts b/future_entry_demo/src/main/ets/selfcode/SelfCodeEvents.ts new file mode 100644 index 0000000..b96bd42 --- /dev/null +++ b/future_entry_demo/src/main/ets/selfcode/SelfCodeEvents.ts @@ -0,0 +1,15 @@ + +export type SelfCodeEvent = { + events: { + eventData: { + udid: string; + userid: string; + version: string; + }; + eventID: string; + }[]; +} +export interface EventDataInterface { + [key: string]: object|number|string; +} + diff --git a/future_entry_demo/src/main/ets/selfcode/SelfCodeRemoteHandler.ets b/future_entry_demo/src/main/ets/selfcode/SelfCodeRemoteHandler.ets new file mode 100644 index 0000000..e69de29 diff --git a/future_entry_demo/src/main/ets/tabmanager/IndexTabManager.ets b/future_entry_demo/src/main/ets/tabmanager/IndexTabManager.ets new file mode 100644 index 0000000..1e4a740 --- /dev/null +++ b/future_entry_demo/src/main/ets/tabmanager/IndexTabManager.ets @@ -0,0 +1,16 @@ +import { TabManager, TabManagerConstants } from 'biz_common'; + +export class IndexTabManager extends TabManager { + constructor() { + super(TabManagerConstants.Index); + + this.tabManagerList.push(TabManagerConstants.tabManagerFactory(TabManagerConstants.MarketPage)); + + this.tabManagerList.push(TabManagerConstants.tabManagerFactory(TabManagerConstants.SelfcodePage)); + + this.tabManagerList.push(TabManagerConstants.tabManagerFactory(TabManagerConstants.TradePage)); + + this.tabManagerList.push(TabManagerConstants.tabManagerFactory(TabManagerConstants.NewsPage)); + + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/widget/PrivacyUserDialog.ets b/future_entry_demo/src/main/ets/widget/PrivacyUserDialog.ets new file mode 100644 index 0000000..c471d51 --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/PrivacyUserDialog.ets @@ -0,0 +1,89 @@ +import { router } from '@kit.ArkUI' +import { PRIVACY_PAGE_URL, USER_AGREEMENT_URL } from 'biz_common' + +/** + * 隐私政策协议弹窗 + * @author menghuiyuan@myhexin.com + * 由于直接使用CustomDialog时,跳转页面后dialog就会消失,这里就使用蒙层效果实现。 + */ +@Component +export struct PrivacyUserDialog { + //点击左侧取消按钮的回调 + cancel?: () => void + //点击右侧确认按钮的回调 + confirm?: () => void + //弹窗标题 + @State title?: string | ResourceStr = "" + //弹窗正文 + @State content?: string | ResourceStr = "" + //左侧按钮文本,默认为取消 + @State leftBtnText: string | ResourceStr = "取消" + //右侧按钮文本,默认为确认 + @State rightBtnText: string | ResourceStr = "确认" + + build() { + Column() { + Text(this.title).fontSize(20).margin({ top: 20 }).fontFamily('bold') + Scroll() { + Column() { + Text() { + Span('尊敬的用户:在使用软件之前,为了让您更好地了解我们是如何保护用户的个人信息,请您仔细阅读').fontColor($r("app.color.elements_text_secondary")) + Span('《隐私政策》').fontColor($r("app.color.elements_text_primary_01")).onClick(() => { + router.pushUrl({ + url: 'pages/WebViewComponentPage', + params: { + url:PRIVACY_PAGE_URL, + title:'隐私政策', + updateH5Title: '0' + }// 目标url + }); + }) + Span('和').fontColor($r("app.color.elements_text_secondary")) + Span('《用户协议》').fontColor($r("app.color.elements_text_primary_01")).onClick(() => { + router.pushUrl({ + url: 'pages/WebViewComponentPage', + params: { + url:USER_AGREEMENT_URL, + title:'用户协议', + updateH5Title: '0' + }// 目标url + }); + }) + Span('。\n' + + '重点内容如下:\n' + + '1.当您注册账号和使用服务时,根据监管需要与您的同意,获取手机号码、身份验证信息、设备信息(含MAC地址)等。\n' + + '2.我们会使用Cookie与同类技术来确保服务正常运转。\n' + + '3.未成年人提交个人信息需要在父母或监护人的指导与同意下进行。\n' + + '4.您可以通过隐私政策所列的途径访问、更正与删除个人信息,也可以注销账户。\n' + + '5.未经您许可,我们不会向任何第三方提供您的信息,除法律要求外。\n' + + '6.具体内容点击上方蓝色链接查看,并以它为准。') + .fontColor($r("app.color.elements_text_secondary")) + } + } + }.scrollBar(BarState.Off) + .width('100%') + .margin({ top: 12 }) + .constraintSize({ maxHeight: 263 }) + + Flex({ wrap: FlexWrap.NoWrap }) { + buildBtn(this.leftBtnText,this.cancel) + buildBtn(this.rightBtnText, this.confirm, true) + }.margin({ top: 21, bottom: 16 }) + }.padding({ left: 16, right: 16 }) + .margin({ left: 48, right: 48 }).backgroundColor($r('app.color.surface_layer1_foreground')).borderRadius(6) + } +} + +@Builder +export function buildBtn(btnText: string | ResourceStr, cb?: () => void, positionRight?: boolean) { + Button(btnText, { type: ButtonType.Normal, stateEffect: true }) + .onClick(() => { + if (cb) { cb() } + }) + .backgroundColor($r('app.color.elements_button_bg_tertiary')) + .fontColor(positionRight ? $r('app.color.elements_text_primary_01'): $r('app.color.elements_text_secondary')) + .width('50%') + .borderRadius(4) + .margin({ left: positionRight? 6 : 0 }) + .visibility(cb ? Visibility.Visible : Visibility.None) +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/widget/SearchBar.ets b/future_entry_demo/src/main/ets/widget/SearchBar.ets new file mode 100644 index 0000000..9c2b5dc --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/SearchBar.ets @@ -0,0 +1,11 @@ + +@Component +export default struct SearchBar { + build() { + Flex({ alignItems: ItemAlign.Center }) { + Text("预览词").fontSize("14").fontColor("#D6FFFFFF").margin( {left: 6}).flexGrow(1) + Divider().strokeWidth(1).vertical(true).margin( {right : 6, top : 6, bottom:6}).color("#D6FFFFFF") + Text("搜索").fontSize("14").fontColor("#FFFFFF").margin( {right : 6}) + } + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/widget/StatusBar.ets b/future_entry_demo/src/main/ets/widget/StatusBar.ets new file mode 100644 index 0000000..9c40c73 --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/StatusBar.ets @@ -0,0 +1,26 @@ +import { GlobalContext } from 'biz_common'; + +@Component +export struct StatusBar { + @Prop bkColor: Resource = $r('app.color.surface_layer1_foreground') + @Prop maskColor: Resource = $r('app.color.wt_40_000000') + @Prop maskVisible: boolean = false + + build() { + Stack() { + Row() + .width('100%') + .height('100%') + .backgroundColor(this.bkColor) + if (this.maskVisible) { + Row() + .width('100%') + .height('100%') + .backgroundColor(this.maskColor) + } + } + .width('100%') + .height(px2vp(GlobalContext.statusBarHeight)) + .flexShrink(0) + } +} diff --git a/future_entry_demo/src/main/ets/widget/ToolsBar.ets b/future_entry_demo/src/main/ets/widget/ToolsBar.ets new file mode 100644 index 0000000..a0894be --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/ToolsBar.ets @@ -0,0 +1,25 @@ +import router from '@ohos.router' +import SearchBar from "../widget/SearchBar" + +@Component +export default struct ToolsBar { + + build() { + Flex({ alignItems: ItemAlign.Center }) { + Image($r("app.media.hangqing_logo")).width(50).height(24).margin( { left: 16, right: 12}).flexShrink(0) + SearchBar().margin( { top : 8, bottom : 8}).backgroundColor("#52FFFFFF").borderRadius(4).flexGrow(1) + Image($r("app.media.iwencai")).width(52).height(24).objectFit(ImageFit.Contain).flexShrink(0) + } + .backgroundColor("#F03036").width('100%').onClick(() => { + router.pushUrl({ + url: 'pages/SearchPage' // 目标url + }, router.RouterMode.Standard, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }); + }) + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/widget/common.ets b/future_entry_demo/src/main/ets/widget/common.ets new file mode 100644 index 0000000..982446e --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/common.ets @@ -0,0 +1,74 @@ +import { router } from '@kit.ArkUI' + +@Builder +export function HQPageCommonTitle(title: string, showSearch: boolean = true, showText: boolean = false, + text: string = '', + onTextClick?: () => void) { + RelativeContainer() { + Image($r('app.media.toolbar_arrow_left')) + .width(20) + .height(24) + .alignRules({ + center: { anchor: '__container__', align: VerticalAlign.Center }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + .margin({ + left: 16 + }) + .onClick(() => router.back()) + Text(title) + .fontSize(17) + .fontColor($r('app.color.start_window_background')) + .lineHeight($r('app.float.line_height_22p')) + .alignRules({ + center: { anchor: '__container__', align: VerticalAlign.Center }, + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + if (showSearch) { + Image($r('app.media.iconsearch2_svg_2x')) + .width(20) + .height(24) + .onClick(() => { + jumpToSearchPage() + }) + .margin({ + right: 16 + }) + .alignRules({ + center: { anchor: '__container__', align: VerticalAlign.Center }, + right: { anchor: '__container__', align: HorizontalAlign.End } + }) + } else if (showText) { + Text(text) + .padding({ + left: 13, + right: 13 + }) + .fontColor('#fff') + .fontSize(15) + .onClick(() => { + if (onTextClick) { + onTextClick() + } + }) + .alignRules({ + center: { anchor: '__container__', align: VerticalAlign.Center }, + right: { anchor: '__container__', align: HorizontalAlign.End } + }) + } + }.width('100%') + .height(44) + .backgroundColor($r('app.color.subpage_title_red')) +} + +export function jumpToSearchPage() { + router.pushUrl({ + url: 'pages/SearchPage' + }, router.RouterMode.Single, (err) => { + if (err) { + console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); + return; + } + console.info('Invoke pushUrl succeeded.'); + }) +} \ No newline at end of file diff --git a/future_entry_demo/src/main/ets/widget/tabWidget.ets b/future_entry_demo/src/main/ets/widget/tabWidget.ets new file mode 100644 index 0000000..bae0f4b --- /dev/null +++ b/future_entry_demo/src/main/ets/widget/tabWidget.ets @@ -0,0 +1,22 @@ + + +@Component +export default struct TabWidget { + + @Prop name: string = ''; + + build() { + Column() { + Text(this.name) + .fontSize(36) + .fontColor('#182431') + .fontWeight(500) + .opacity(0.4) + .margin({ top: 30, bottom: 56.5 }) + Divider() + .strokeWidth(0.5) + .color('#182431') + .opacity(0.05) + }.width('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/model/EmitterInterface.ets b/future_entry_demo/src/main/model/EmitterInterface.ets new file mode 100644 index 0000000..28ac845 --- /dev/null +++ b/future_entry_demo/src/main/model/EmitterInterface.ets @@ -0,0 +1,4 @@ +export interface ISelfcodeUpdateData { + userid:string; + version:string; +} \ No newline at end of file diff --git a/future_entry_demo/src/main/module.json5 b/future_entry_demo/src/main/module.json5 new file mode 100644 index 0000000..fe61b32 --- /dev/null +++ b/future_entry_demo/src/main/module.json5 @@ -0,0 +1,200 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.PRIVACY_WINDOW", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.ACCESS_BIOMETRIC", + "reason": "$string:permission_biometric_reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.GET_NETWORK_INFO", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.APP_TRACKING_CONSENT", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + }, + "reason": "$string:app_tracking_consent_reason" + }, + { + "name": "ohos.permission.VIBRATE", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:location_reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.GET_WIFI_INFO" + }, + { + "name": "ohos.permission.LOCATION", + "reason": "$string:permission_reason_location", + "usedScene": { + "abilities": [ + 'EntryAbility' + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.LOCATION_IN_BACKGROUND", + "reason": "$string:permission_reason_location", + "usedScene": { + "abilities": [ + 'EntryAbility' + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.READ_PASTEBOARD", + "reason": "$string:permission_read_pasteboard", + "usedScene": { + "abilities": [ + 'EntryAbility' + ], + "when": "inuse" + } + } + ], + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "continuable": true, + "continueType": [ + 'EntryAbility_ContinueQuickStart' + ], + "orientation": "follow_desktop", + "launchType": "singleton", + "startWindowIcon": "$media:ths_desktop_icon", + "startWindowBackground": "$color:start_window_background_new", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + }, + { + //push跳转 + "actions": [ + "com.push.action" + ] + }, + { + "actions": [ + "action.ohos.push.listener" + ] + }, + { + //端外跳转 + "actions": [ + "ohos.want.action.viewData" + ], + "uris": [ + { + "scheme": "thsfutures" + } + ] + } + ] + } + ], + "extensionAbilities": [ + { + "description": "$string:ServiceExtAbility", + "icon": "$media:icon", + "name": "BackupExtensionAbility", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + // 在BackupExtension.ts文件里自定义继承BackupExtensionAbility,重写其中的onBackup和onRestore方法。 + // 如果没有特殊要求可以空实现,则备份恢复服务会按照统一的备份恢复数据规则进行备份恢复。 + "srcEntry": "./ets/backupExtension/BackupExtension.ets" + }, + { + "name": "RemoteNotificationExtAbility", + "type": "remoteNotification", + "srcEntry": "./ets/entryability/RemoteNotificationExtAbility.ets", + "description": "RemoteNotificationExtAbility", + "exported": false, + "skills": [ + { + "actions": [ + "action.hms.push.extension.remotenotification" + ] + } + ] + } + ], + "querySchemes": [ + "weixin" + ] + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/base/element/color.json b/future_entry_demo/src/main/resources/base/element/color.json new file mode 100644 index 0000000..3dbc84a --- /dev/null +++ b/future_entry_demo/src/main/resources/base/element/color.json @@ -0,0 +1,341 @@ +{ + "color": [ + { + "name": "entry_navigation_bar_content_with_mask_color", + "value": "#FF000000" + }, + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "start_window_background_new", + "value": "#FFFFFF" + }, + { + "name": "navbar_bg_red", + "value": "#F03036" + }, + { + "name": "subpage_title_red", + "value": "#FFF03036" + }, + { + "name": "99000000", + "value": "#99000000" + }, + { + "name": "wt_color_main", + "value": "#F03036" + }, + { + "name": "wt_100_F8C1C1", + "value": "#F8C1C1" + }, + { + "name": "wt_100_3B3B3B", + "value": "#3B3B3B" + }, + { + "name": "wt_96_3B3B3B", + "value": "#F53B3B3B" + }, + { + "name": "wt_100_323232", + "value": "#323232" + }, + { + "name": "wt_100_CCCCCC", + "value": "#CCCCCC" + }, + { + "name": "wt_100_DDDDDD", + "value": "#DDDDDD" + }, + { + "name": "wt_100_009900", + "value": "#009900" + }, + { + "name": "wt_100_999999", + "value": "#999999" + }, + { + "name": "wt_100_FF801A", + "value": "#FF801A" + }, + { + "name": "wt_84_000000", + "value": "#D6000000" + }, + { + "name": "wt_84_FFFFFF", + "value": "#D6FFFFFF" + }, + { + "name": "wt_40_000000", + "value": "#66000000" + }, + { + "name": "wt_4_000000", + "value": "#0A000000" + }, + { + "name": "wt_10_000000", + "value": "#1A000000" + }, + { + "name": "wt_05_000000", + "value": "#0D000000" + }, + { + "name": "wt_8_000000", + "value": "#14000000" + }, + { + "name": "wt_60_000000", + "value": "#99000000" + }, + { + "name": "wt_24_000000", + "value": "#3D000000" + }, + { + "name": "wt_100_FF2436", + "value": "#FF2436" + }, + { + "name": "wt_100_4F5777", + "value": "#4F5777" + }, + { + "name": "wt_100_4691EE", + "value": "#4691EE" + },{ + "name": "wt_100_2662ae", + "value": "#2662ae" + }, + { + "name": "wt_10_4691EE", + "value": "#1A4691EE" + }, + { + "name": "wt_100_3366FF", + "value": "#3366FF" + }, + { + "name": "wt_100_d12b12", + "value": "#d12b12" + }, + { + "name": "wt_10_FF2436", + "value": "#1AFF2436" + }, + { + "name": "wt_50_FF2436", + "value": "#80FF2436" + }, + { + "name": "wt_100_FA1500", + "value": "#FA1500" + }, + { + "name": "wt_50_FFFFFF", + "value": "#80FFFFFF" + }, + { + "name": "wt_100_FF9500", + "value": "#FF9500" + }, + { + "name": "wt_100_FF661A", + "value": "#FF661A" + }, + { + "name": "wt_100_EEEEEE", + "value": "#EEEEEE" + }, + { + "name": "wt_100_666666", + "value": "#666666" + }, + { + "name": "wt_100_F6F6F6", + "value": "#F6F6F6" + }, + + { + "name": "wt_background_page", + "value": "#F5F5F5" + }, + { + "name": "wt_foreground_page", + "value": "#FFFFFF" + }, + + + { + "name": "wt_btn_red", + "value": "#FF2436" + }, + + + { + "name": "wt_font_profit_positive", + "value": "#FF2436" + }, + { + "name": "wt_font_profit_negative", + "value": "#4691EE" + }, + { + "name": "wt_font_profit_zero", + "value": "#D6000000" + }, + { + "name": "wt_font_link", + "value": "#4167D9" + }, + { + "name": "wt_font_white", + "value": "#FFFFFF" + }, + { + "name": "wt_divide", + "value": "#14000000" + }, + { + "name": "entry_navbar_bg", + "value": "#FFFFFF" + }, + { + "name": "about_text_dark", + "value": "#FF000000" + }, + { + "name": "theme_color_99000000_99ffffff", + "value": "#99000000" + }, + { + "name": "about_text_light_gray", + "value": "#666666" + }, + + { + "name": "color_000000_alp_8", + "value": "#14000000" + }, + { + "name": "color_4973f2", + "value": "#4973f2" + }, + { + "name": "contact_header_text", + "value": "#4A5F7A" + }, + { + "name": "contact_header_bg", + "value": "#E8F0FA" + }, + { + "name": "product_header_bg", + "value": "#4A90E2" + }, + { + "name": "hq_title_mask_color_start", + "value": "#00FFFFFF" + }, + { + "name": "hq_title_mask_color_end", + "value": "#FFFFFFFF" + }, + { + "name": "blue", + "value": "#2e58ff" + }, + { + "name": "color_800000", + "value": "#80000000" + }, + { + "name": "period_setting_add_border", + "value": "#3D000000" + }, + { + "name": "market_heat_price_line", + "value": "#3366FF" + }, + { + "name": "market_heat_price_legend", + "value": "#3366FF" + }, + { + "name": "market_heat_fund_line", + "value": "#FF5500" + }, + { + "name": "market_heat_root_background", + "value": "#FFFFFF" + }, + { + "name": "market_heat_content_background", + "value": "#FFFFFF" + }, + { + "name": "market_heat_section_background", + "value": "#F6F7F8" + }, + { + "name": "market_heat_primary_text", + "value": "#222222" + }, + { + "name": "market_heat_title_text", + "value": "#000000" + }, + { + "name": "market_heat_empty_text", + "value": "#666666" + }, + { + "name": "market_heat_self_empty_text", + "value": "#4765C4" + }, + { + "name": "market_heat_up_text", + "value": "#FF0012" + }, + { + "name": "market_heat_down_text", + "value": "#0E8936" + }, + { + "name": "market_heat_axis_text", + "value": "#999999" + }, + { + "name": "market_heat_grid_line", + "value": "#EEEEEE" + }, + { + "name": "market_heat_cursor_background", + "value": "#FFFFFF" + }, + { + "name": "market_heat_back_icon", + "value": "#444444" + }, + { + "name": "market_heat_themed_tag_text", + "value": "#FFFFFF" + }, + { + "name": "market_heat_realtime_tag_text", + "value": "#FFFFFF" + }, + { + "name": "market_heat_selected_row", + "value": "#1A4765C4" + } + ] +} diff --git a/future_entry_demo/src/main/resources/base/element/string.json b/future_entry_demo/src/main/resources/base/element/string.json new file mode 100644 index 0000000..0fe165d --- /dev/null +++ b/future_entry_demo/src/main/resources/base/element/string.json @@ -0,0 +1,364 @@ +{ + "string": [ + { + "name": "entry_navigation_bar_content_with_mask_color", + "value": "#FF000000" + }, + { + "name": "entry_navigation_bar_content_without_mask_color", + "value": "#FF000000" + }, + { + "name": "entry_transparent_color", + "value": "#00000000" + }, + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "同花顺期货通" + }, + { + "name": "webview_default_title", + "value": " " + }, + { + "name": "app_tracking_consent_reason", + "value": "因交易合规需要,需要读取开放匿名设备标识符" + }, + { + "name": "ServiceExtAbility", + "value": "abilityIntro" + }, + { + "name": "withdraw_agree_sub_title", + "value": "撤回同意" + }, + { + "name": "withdraw_agree_title", + "value": "提示" + }, + { + "name": "withdraw_agree_content", + "value": "撤回同意将退出同花顺期货通,我们无法再为您提供服务" + }, + { + "name": "withdraw_agree_confirm", + "value": "撤回并退出" + }, + { + "name": "withdraw_agree_cancel", + "value": "继续使用" + }, + { + "name": "dialog_sure_btn_text", + "value": "确定" + }, + { + "name": "location_reason", + "value": "登录交易/查找附件营业部,需要获取您的位置信息" + }, + + { + "name": "permission_reason_location", + "value": "登录交易/查找附件营业部,需要获取您的位置信息" + }, + { + "name": "permission_read_pasteboard", + "value": "用于跳转至指定功能页面等" + }, + { + "name": "about_us_title", + "value": "关于我们" + }, + { + "name": "about_us_app_name", + "value": "同花顺期货通" + }, + { + "name": "about_us_product_intro", + "value": "产品介绍" + }, + { + "name": "about_us_contact", + "value": "联系我们" + }, + { + "name": "about_us_privacy_policy", + "value": "隐私政策" + }, + { + "name": "about_us_personal_info_download", + "value": "个人信息下载" + }, + { + "name": "about_us_check_update", + "value": "检查新版本" + }, + { + "name": "about_us_recommendation_settings", + "value": "定向推荐设置" + }, + { + "name": "about_us_device_id", + "value": "设备标识" + }, + { + "name": "about_us_disclaimer", + "value": "免责条款" + }, + { + "name": "about_us_icp", + "value": "ICP备案号:浙ICP备10056399号-16A >" + }, + { + "name": "about_us_platform_desc", + "value": "同花顺旗下期货交易平台" + }, + { + "name": "contact_us_title", + "value": "联系我们" + }, + { + "name": "contact_us_hotline", + "value": "同花顺全国统一客服热线:952555" + }, + { + "name": "contact_us_service_time", + "value": "热线服务时间:09:00-24:00" + }, + { + "name": "contact_us_address_title", + "value": "地址" + }, + { + "name": "contact_us_address", + "value": "杭州市余杭区同顺街18号同花顺新园区" + }, + { + "name": "user_agreement_text", + "value": "用户协议" + }, + { + "name": "product_intro_title", + "value": "产品介绍" + }, + { + "name": "product_intro_desc", + "value": "同花顺期货通由同花顺(股票代码:300033)独家研发的一款期货app,集行情、资讯、特色服务为一体的专业级期货平台终端。" + }, + { + "name": "product_intro_market_title", + "value": "行情:" + }, + { + "name": "product_intro_market_desc", + "value": "毫秒级精确行情及20个指标与多种周期分析让你实时把握市场动态行情,画线功能让你随时随地妙手生花。" + }, + { + "name": "product_intro_data_title", + "value": "数据:" + }, + { + "name": "product_intro_data_desc", + "value": "同花顺专业级数据平台为您提供精准、丰富的数据支持,在手机上也能随时随地掌握市场命脉。" + }, + { + "name": "product_intro_service_title", + "value": "服务:" + }, + { + "name": "product_intro_service_desc", + "value": "同花顺期货通为你提供丰富的辅助交易工具和海量的实时资讯,助您在决策中快人一步。" + }, + { + "name": "copy_success", + "value": "复制成功" + }, + { + "name": "copy_fail", + "value": "复制失败" + }, + { + "name": "about_beianhao_query_text", + "value": "备案查询" + }, + { + "name": "web_link_fail", + "value": "跳转失败!" + },{ + "name": "permission_biometric_reason", + "value": "生物识别功能可以让您更安全快捷地解锁" + }, + { + "name": "trade_daily_abandon_exercise", + "value": "放弃行权" + }, + { + "name": "trade_daily_order_no_prefix_with_comma", + "value": ",委托号:" + }, + { + "name": "trade_daily_transaction_report", + "value": "交易回报" + }, + { + "name": "fu_trade_setting_trade_notification_setting", + "value": "交易提醒设置" + }, + { + "name": "fu_trade_notification_report", + "value": "交易回报浮窗" + }, + { + "name": "fu_trade_notification_tip", + "value": "关闭后将不在交易浮窗中显示提醒" + }, + { + "name": "fu_trade_notification_tip_gphone", + "value": "当前版本暂不支持条件单" + }, + { + "name": "fu_trade_notification_weituo", + "value": "委托回报" + }, + { + "name": "fu_trade_notification_weituo_tip", + "value": "委托单状态变化时接收通知" + }, + { + "name": "fu_trade_notification_chengjiao", + "value": "成交回报" + }, + { + "name": "fu_trade_notification_chengjiao_tip", + "value": "委托成交时接收通知" + }, + { + "name": "fu_trade_notification_chedan", + "value": "撤单回报" + }, + { + "name": "fu_trade_notification_chedan_tip", + "value": "撤单结果通知" + }, + { + "name": "fu_trade_notification_condition", + "value": "条件单触发" + }, + { + "name": "fu_trade_notification_condition_tip", + "value": "条件单触发时接收通知" + }, + { + "name": "fu_trade_notification_grid", + "value": "网格交易运行" + }, + { + "name": "fu_trade_notification_grid_tip", + "value": "网格交易运行时接收通知" + }, + { + "name": "fu_trade_notification_sound", + "value": "响铃" + }, + { + "name": "fu_trade_notification_shock", + "value": "震动" + }, + { + "name": "fu_trade_notification_profit_loss", + "value": "止盈止损触发" + }, + { + "name": "fu_trade_notification_profit_loss_tip", + "value": "止盈止损触发时接收通知" + }, + { + "name": "kline_interaction_setting_title", + "value": "K线滑动设置" + }, + { + "name": "kline_interaction_period_change", + "value": "切换周期" + }, + { + "name": "kline_interaction_kline_scroll", + "value": "移动K线" + }, + { + "name": "kline_interaction_preview_title", + "value": "效果预览" + }, + { + "name": "custom_period_more_separator_tip", + "value": "以下在 “更多” 展示,支持拖动调整" + }, + { + "name": "custom_period_introduction_title", + "value": "常用周期说明" + }, + { + "name": "custom_period_introduction_tip", + "value": "前%d个周期在蓝色区域展示,剩余周期在 “更多” 中展示" + }, + { + "name": "custom_period_tab_futures", + "value": "期货" + }, + { + "name": "custom_period_tab_options", + "value": "期权" + }, + { + "name": "custom_period_setting_title", + "value": "常用周期设置" + }, + { + "name": "custom_period_common_title", + "value": "常用周期" + }, + { + "name": "custom_period_drag_sort_tip", + "value": "按住拖动排序" + }, + { + "name": "custom_period_add_button", + "value": "+添加周期" + }, + { + "name": "custom_period_added_to_more_toast", + "value": "已添加到更多周期" + }, + { + "name": "custom_period_introduction_known_button", + "value": "我知道了" + }, + { + "name": "clipboard_permission_text", + "value": "允许读取剪贴板" + }, + { + "name": "clipboard_permission_dialog_tip", + "value": "关闭后,将不再读取剪贴板信息,在分享、邀请等场景下,使用体验可能会受到影响。是否确认关闭?" + }, + { + "name": "clipboard_permission_cancel", + "value": "取消" + }, + { + "name": "clipboard_permission_close", + "value": "关闭" + }, + { + "name": "clipboard_permission_title", + "value": "提示" + } + ] +} diff --git a/future_entry_demo/src/main/resources/base/element/url.json b/future_entry_demo/src/main/resources/base/element/url.json new file mode 100644 index 0000000..0e71eec --- /dev/null +++ b/future_entry_demo/src/main/resources/base/element/url.json @@ -0,0 +1,57 @@ +{ + "string": [ + { + "name": "about_statement_url", + "value": "https://fupage.10jqka.com.cn/activity/33285/index.html#channelid=100" + }, + { + "name": "revoke_privacy_protocol_url", + "value": "https://fupage.10jqka.com.cn/projects/m/privatepolicy/index.html#showButton=true&channelid=100" + }, + { + "name": "fu_user_policy", + "value": "https://fupage.10jqka.com.cn/projects/m/privatepolicy/index_v2.html#channelid=100" + }, + { + "desc": "个人信息下载", + "name": "user_info_download_url", + "value": "https://fupage.10jqka.com.cn/futures-frontend-kamis-renderer/index.0.3.6.html?token=K5BMjA5NDQB5#channelid=100" + }, + { + "name": "about_direct_promotion", + "value": "https://fupage.10jqka.com.cn/activity/33293/index.html#channelid=100" + }, + { + "name": "about_beianhao_query_url", + "value": "https://beian.miit.gov.cn/" + }, + { + "name": "about_user_agreement_url", + "value": "https://mams.10jqka.com.cn/new/server/html/74162.html?injectAdaptive=true" + }, + { + "name": "fu_marketoverview_url", + "value": "https://fupage.10jqka.com.cn/projects/m/marketoverview/index.html" + }, + { + "name": "base_url", + "value": "https://cbasspider.10jqka.com.cn:8443" + }, + { + "name": "kline_interaction_period_change_url", + "value": "https://fupage.10jqka.com.cn/activity-v2/1/index.html?showType=cycle" + }, + { + "name": "kline_interaction_kline_scroll_url", + "value": "https://fupage.10jqka.com.cn/activity-v2/1/index.html?showType=candle" + }, + { + "name": "custom_authority_url", + "value": "https://ftapi.10jqka.com.cn/futgwapi/api/account/custom_authority/v1/user_authorities" + }, + { + "name": "custom_authority_url_test", + "value": "https://futures-test.10jqka.com.cn/futgwapi/api/account/custom_authority/v1/user_authorities" + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/base/media/about_product.png b/future_entry_demo/src/main/resources/base/media/about_product.png new file mode 100644 index 0000000..e0cfa3b Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/about_product.png differ diff --git a/future_entry_demo/src/main/resources/base/media/app_icon.png b/future_entry_demo/src/main/resources/base/media/app_icon.png new file mode 100644 index 0000000..3c5690a Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/app_icon.png differ diff --git a/future_entry_demo/src/main/resources/base/media/back_white2x_svg.svg b/future_entry_demo/src/main/resources/base/media/back_white2x_svg.svg new file mode 100644 index 0000000..b0ef858 --- /dev/null +++ b/future_entry_demo/src/main/resources/base/media/back_white2x_svg.svg @@ -0,0 +1,27 @@ + + + 导航菜单返回 2@2x + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/base/media/bmp_american.png b/future_entry_demo/src/main/resources/base/media/bmp_american.png new file mode 100644 index 0000000..01a6786 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/bmp_american.png differ diff --git a/future_entry_demo/src/main/resources/base/media/bmp_candle_rect.png b/future_entry_demo/src/main/resources/base/media/bmp_candle_rect.png new file mode 100644 index 0000000..54f598b Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/bmp_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/base/media/bmp_close.png b/future_entry_demo/src/main/resources/base/media/bmp_close.png new file mode 100644 index 0000000..4cf657f Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/bmp_close.png differ diff --git a/future_entry_demo/src/main/resources/base/media/bmp_solid_candle_rect.png b/future_entry_demo/src/main/resources/base/media/bmp_solid_candle_rect.png new file mode 100644 index 0000000..eec12c6 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/bmp_solid_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/base/media/bmp_tower.png b/future_entry_demo/src/main/resources/base/media/bmp_tower.png new file mode 100644 index 0000000..eef288c Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/bmp_tower.png differ diff --git a/future_entry_demo/src/main/resources/base/media/contact_us.png b/future_entry_demo/src/main/resources/base/media/contact_us.png new file mode 100644 index 0000000..0a17601 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/contact_us.png differ diff --git a/future_entry_demo/src/main/resources/base/media/hangqing_logo.png b/future_entry_demo/src/main/resources/base/media/hangqing_logo.png new file mode 100644 index 0000000..5815d15 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/hangqing_logo.png differ diff --git a/future_entry_demo/src/main/resources/base/media/hq_indicator_edit.png b/future_entry_demo/src/main/resources/base/media/hq_indicator_edit.png new file mode 100644 index 0000000..e3dc3d0 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/hq_indicator_edit.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ic_american.png b/future_entry_demo/src/main/resources/base/media/ic_american.png new file mode 100644 index 0000000..b047d9f Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ic_american.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ic_candle_rect.png b/future_entry_demo/src/main/resources/base/media/ic_candle_rect.png new file mode 100644 index 0000000..2259922 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ic_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ic_close.png b/future_entry_demo/src/main/resources/base/media/ic_close.png new file mode 100644 index 0000000..bba4587 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ic_close.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ic_solid_candle_rect.png b/future_entry_demo/src/main/resources/base/media/ic_solid_candle_rect.png new file mode 100644 index 0000000..8310bb8 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ic_solid_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ic_tower.png b/future_entry_demo/src/main/resources/base/media/ic_tower.png new file mode 100644 index 0000000..82ac588 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ic_tower.png differ diff --git a/future_entry_demo/src/main/resources/base/media/icon_search_black.png b/future_entry_demo/src/main/resources/base/media/icon_search_black.png new file mode 100644 index 0000000..0d3f5c9 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/icon_search_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/icon_system_config_app_2.png b/future_entry_demo/src/main/resources/base/media/icon_system_config_app_2.png new file mode 100644 index 0000000..1691101 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/icon_system_config_app_2.png differ diff --git a/future_entry_demo/src/main/resources/base/media/indicator_square_icon.png b/future_entry_demo/src/main/resources/base/media/indicator_square_icon.png new file mode 100644 index 0000000..7b63f47 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/indicator_square_icon.png differ diff --git a/future_entry_demo/src/main/resources/base/media/iwencai.png b/future_entry_demo/src/main/resources/base/media/iwencai.png new file mode 100644 index 0000000..bea9bad Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/iwencai.png differ diff --git a/future_entry_demo/src/main/resources/base/media/launch_logo.png b/future_entry_demo/src/main/resources/base/media/launch_logo.png new file mode 100644 index 0000000..5560649 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/launch_logo.png differ diff --git a/future_entry_demo/src/main/resources/base/media/launcher_desc.png b/future_entry_demo/src/main/resources/base/media/launcher_desc.png new file mode 100644 index 0000000..0bea1e1 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/launcher_desc.png differ diff --git a/future_entry_demo/src/main/resources/base/media/launcher_icon.png b/future_entry_demo/src/main/resources/base/media/launcher_icon.png new file mode 100644 index 0000000..3c49025 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/launcher_icon.png differ diff --git a/future_entry_demo/src/main/resources/base/media/market_heat_arrow.png b/future_entry_demo/src/main/resources/base/media/market_heat_arrow.png new file mode 100644 index 0000000..c60ba48 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/market_heat_arrow.png differ diff --git a/future_entry_demo/src/main/resources/base/media/navigate_back_icon.svg b/future_entry_demo/src/main/resources/base/media/navigate_back_icon.svg new file mode 100644 index 0000000..c8890df --- /dev/null +++ b/future_entry_demo/src/main/resources/base/media/navigate_back_icon.svg @@ -0,0 +1,11 @@ + + + A21 2@2x + + + + + + + + \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/base/media/pdf_pagination.png b/future_entry_demo/src/main/resources/base/media/pdf_pagination.png new file mode 100644 index 0000000..3c62f52 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/pdf_pagination.png differ diff --git a/future_entry_demo/src/main/resources/base/media/sort_down_qihuo.png b/future_entry_demo/src/main/resources/base/media/sort_down_qihuo.png new file mode 100644 index 0000000..9d7b0de Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/sort_down_qihuo.png differ diff --git a/future_entry_demo/src/main/resources/base/media/sort_none.png b/future_entry_demo/src/main/resources/base/media/sort_none.png new file mode 100644 index 0000000..eb06dac Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/sort_none.png differ diff --git a/future_entry_demo/src/main/resources/base/media/sort_up_qihuo.png b/future_entry_demo/src/main/resources/base/media/sort_up_qihuo.png new file mode 100644 index 0000000..22a537c Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/sort_up_qihuo.png differ diff --git a/future_entry_demo/src/main/resources/base/media/square_icon_white_2x.png b/future_entry_demo/src/main/resources/base/media/square_icon_white_2x.png new file mode 100644 index 0000000..700905e Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/square_icon_white_2x.png differ diff --git a/future_entry_demo/src/main/resources/base/media/square_icon_white_3x.png b/future_entry_demo/src/main/resources/base/media/square_icon_white_3x.png new file mode 100644 index 0000000..7b63f47 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/square_icon_white_3x.png differ diff --git a/future_entry_demo/src/main/resources/base/media/standard_price_check.png b/future_entry_demo/src/main/resources/base/media/standard_price_check.png new file mode 100644 index 0000000..f3ac1ea Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/standard_price_check.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_discover_normal.png b/future_entry_demo/src/main/resources/base/media/tab_discover_normal.png new file mode 100644 index 0000000..5a020be Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_discover_normal.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_discover_pressed.png b/future_entry_demo/src/main/resources/base/media/tab_discover_pressed.png new file mode 100644 index 0000000..0cca016 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_discover_pressed.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_firstpage_normal.png b/future_entry_demo/src/main/resources/base/media/tab_firstpage_normal.png new file mode 100644 index 0000000..6e76ca5 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_firstpage_normal.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_firstpage_pressed.png b/future_entry_demo/src/main/resources/base/media/tab_firstpage_pressed.png new file mode 100644 index 0000000..01f0e2e Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_firstpage_pressed.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_hangqing_normal.png b/future_entry_demo/src/main/resources/base/media/tab_hangqing_normal.png new file mode 100644 index 0000000..4c98459 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_hangqing_normal.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_hangqing_pressed.png b/future_entry_demo/src/main/resources/base/media/tab_hangqing_pressed.png new file mode 100644 index 0000000..be3ab47 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_hangqing_pressed.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_trade_normal.png b/future_entry_demo/src/main/resources/base/media/tab_trade_normal.png new file mode 100644 index 0000000..d119632 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_trade_normal.png differ diff --git a/future_entry_demo/src/main/resources/base/media/tab_trade_pressed.png b/future_entry_demo/src/main/resources/base/media/tab_trade_pressed.png new file mode 100644 index 0000000..b550fed Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/tab_trade_pressed.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right.png b/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right.png new file mode 100644 index 0000000..4c096f8 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right_night.png b/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right_night.png new file mode 100644 index 0000000..dfb0742 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_arrow_right_night.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_item_del.png b/future_entry_demo/src/main/resources/base/media/theme_icon_item_del.png new file mode 100644 index 0000000..ba6ed1a Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_item_del.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_res_search.png b/future_entry_demo/src/main/resources/base/media/theme_icon_res_search.png new file mode 100644 index 0000000..0d3f5c9 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_res_search.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_res_setting.png b/future_entry_demo/src/main/resources/base/media/theme_icon_res_setting.png new file mode 100644 index 0000000..3d9fc87 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_res_setting.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app.png b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app.png new file mode 100644 index 0000000..637f562 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app_night.png b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app_night.png new file mode 100644 index 0000000..0988a63 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_app_night.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy.png b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy.png new file mode 100644 index 0000000..c613c4e Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy_night.png b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy_night.png new file mode 100644 index 0000000..a58fbcf Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_icon_system_config_copy_night.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_img_period_introduction.png b/future_entry_demo/src/main/resources/base/media/theme_img_period_introduction.png new file mode 100644 index 0000000..579bc58 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_img_period_introduction.png differ diff --git a/future_entry_demo/src/main/resources/base/media/theme_kline_scroll_selected.png b/future_entry_demo/src/main/resources/base/media/theme_kline_scroll_selected.png new file mode 100644 index 0000000..3dcdec9 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/theme_kline_scroll_selected.png differ diff --git a/future_entry_demo/src/main/resources/base/media/ths_desktop_icon.png b/future_entry_demo/src/main/resources/base/media/ths_desktop_icon.png new file mode 100644 index 0000000..3c5690a Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/ths_desktop_icon.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull.png new file mode 100644 index 0000000..ea29735 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_black.png new file mode 100644 index 0000000..ea29735 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_white.png new file mode 100644 index 0000000..147d31a Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_pull_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push.png new file mode 100644 index 0000000..e154d5c Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_black.png new file mode 100644 index 0000000..e154d5c Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_white.png new file mode 100644 index 0000000..9156823 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_arrow_push_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close.png new file mode 100644 index 0000000..126d4ee Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_black.png new file mode 100644 index 0000000..126d4ee Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_white.png new file mode 100644 index 0000000..d20b492 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_close_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open.png new file mode 100644 index 0000000..3ff5f5f Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_black.png new file mode 100644 index 0000000..3ff5f5f Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_white.png new file mode 100644 index 0000000..e2a0377 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_charts_open_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_black.png new file mode 100644 index 0000000..8f3adc2 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_white.png new file mode 100644 index 0000000..1c26d79 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_data_error_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction.png new file mode 100644 index 0000000..fbffc79 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_black.png new file mode 100644 index 0000000..fbffc79 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_white.png new file mode 100644 index 0000000..041bad3 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_instruction_white.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search.png new file mode 100644 index 0000000..c68aca1 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_black.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_black.png new file mode 100644 index 0000000..c68aca1 Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_black.png differ diff --git a/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_white.png b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_white.png new file mode 100644 index 0000000..46390bb Binary files /dev/null and b/future_entry_demo/src/main/resources/base/media/trend_forecast_icon_search_white.png differ diff --git a/future_entry_demo/src/main/resources/base/profile/backup_config.json b/future_entry_demo/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000..b1ccada --- /dev/null +++ b/future_entry_demo/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,8 @@ +{ + "allowToBackupRestore": true, + "extraInfo": { + "supportScene": [ + "hmos2next" + ] + } +} \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/base/profile/main_pages.json b/future_entry_demo/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..06d47ea --- /dev/null +++ b/future_entry_demo/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,79 @@ +{ + "src": [ + "pages/LauncherPage", + "pages/Index", + "pages/QuoteDetailPage", + "pages/TrendForecastPage", + "pages/FloatingWindowPage", + "pages/MinimizedIconPage", + "pages/quote/IndicatorManagerPage", + "pages/quote/KlineStyleSettingPage", + "pages/quote/IndicatorSearchPage", + "pages/quote/ChartSettingPage", + "pages/quote/KLineInteractionSettingPage", + "pages/quote/StandardPriceSettingPage", + "pages/quote/LargeOrderSettingPage", + "pages/quote/CustomPeriodSettingPage", + "pages/quote/SettingIntroducePage", + "pages/quote/CurveBottomToolSortPage", + "pages/StockListPage", + "pages/login/LoginPage", + "pages/login/LoginMainPage", + "pages/login/CheckNumberPage", + "pages/login/AccountSecurityPage", + "pages/setting/SystemSettingPage", + "pages/setting/HQTableSettingPage", + "pages/SearchPage", + "pages/SelfCodeSecondPage", + "pages/CommonWebPage", + "pages/WebViewComponentPage", + "pages/LandscapeWebViewPage", + "pages/CommonListPage", + "pages/weituo/account/ChooseQSPageContainer", + "pages/weituo/account/ChooseProvincePageContainer", + "pages/weituo/account/ChooseYYBPageContainer", + "pages/weituo/account/AddAccountPageContainer", + "pages/weituo/transfer/WeituoTransferPageContainer", + "pages/weituo/transfer/WeituoHistoryTransferListPageContainer", + "pages/weituo/chicang/WTPositionViewPageContainer", + "pages/weituo/chaxun/WTTradeRecordPageContainer", + "pages/weituo/tradeset/WTTradeSetPageContainer", + "pages/weituo/tradeset/functionswitch/LimitPriceSetPageContainer", + "pages/weituo/utils/WTDebugUtilWindowContainer", + "pages/weituo/bondRepo/WTGovBondRepoPageContainer", + "pages/weituo/bondRepo/WTGovBondRepoBuyPageContainer", + "pages/weituo/newstockapply/WTNewStockApplyPageContainer", + "pages/TemporaryBlockWebPage", + "pages/selfcode/SelfGroupAddStockPage", + "pages/selfcode/SelfGroupRenamePage", + "pages/selfcode/SelfGroupSettingsPage", + "pages/selfcode/SelfStockSettingPage", + "pages/selfcode/SelfCodeToMainContractPage", + "pages/selfcode/SelfStockIndexSettingPage", + "pages/future/ContractListPage", + "pages/future/MarketHeatPage", + "pages/weituo/account/WTAccountManagerPageContainer", + "pages/aboutus/AboutUs", + "pages/aboutus/ContactUs", + "pages/aboutus/ProductIntroduction", + "pages/debug/GrayTestDebugPage", + "pages/debug/TradeDebugPage", + "pages/debug/DebugYYBConfigPage", + "pages/debug/TradeCloudStorageDebugPage", + "pages/debug/DebugSwitchConfigPage", + "pages/debug/LeakMonitorPage", + "pages/debug/DebugModifyUrlPage", + "pages/debug/DebugFloatingBallWindow", + "pages/debug/DebugBSPointMockDataPage", + "pages/debug/KLineBSPointMockDataPage", + "pages/debug/IBSPointDetailDebugPage", + "pages/test/CodeTableTestPage", + "pages/test/DeleteStockTestPage", + "pages/IndicatorParamPage", + "pages/IndicatorDraftManagePage", + "pages/IndicatorParserTestPage", + "pages/SystemIndicatorsTestPage", + "network/NetworkAnomalyPage", + "pages/pdf/PDFViewer" + ] +} diff --git a/future_entry_demo/src/main/resources/dark/element/color.json b/future_entry_demo/src/main/resources/dark/element/color.json new file mode 100644 index 0000000..ca85f32 --- /dev/null +++ b/future_entry_demo/src/main/resources/dark/element/color.json @@ -0,0 +1,88 @@ +{ + "color": [ + { + "name": "entry_navigation_bar_content_with_mask_color1", + "value": "#FFFFFFFF" + }, + { + "name": "period_setting_add_border", + "value": "#3DFFFFFF" + }, + { + "name": "market_heat_price_line", + "value": "#D6FFFFFF" + }, + { + "name": "market_heat_price_legend", + "value": "#D6FFFFFF" + }, + { + "name": "market_heat_fund_line", + "value": "#FF5500" + }, + { + "name": "market_heat_root_background", + "value": "#212125" + }, + { + "name": "market_heat_content_background", + "value": "#27272C" + }, + { + "name": "market_heat_section_background", + "value": "#212125" + }, + { + "name": "market_heat_primary_text", + "value": "#DDDDDD" + }, + { + "name": "market_heat_title_text", + "value": "#EEEEEE" + }, + { + "name": "market_heat_empty_text", + "value": "#AAAAAA" + }, + { + "name": "market_heat_self_empty_text", + "value": "#4765C4" + }, + { + "name": "market_heat_up_text", + "value": "#FF2433" + }, + { + "name": "market_heat_down_text", + "value": "#009B32" + }, + { + "name": "market_heat_axis_text", + "value": "#999999" + }, + { + "name": "market_heat_grid_line", + "value": "#3D3D42" + }, + { + "name": "market_heat_cursor_background", + "value": "#000000" + }, + { + "name": "market_heat_back_icon", + "value": "#DDDDDD" + }, + { + "name": "market_heat_themed_tag_text", + "value": "#000000" + }, + { + "name": "market_heat_realtime_tag_text", + "value": "#FFFFFF" + }, + { + "name": "market_heat_selected_row", + "value": "#1A628AED" + } + ] +} diff --git a/future_entry_demo/src/main/resources/dark/element/string.json b/future_entry_demo/src/main/resources/dark/element/string.json new file mode 100644 index 0000000..bdfa708 --- /dev/null +++ b/future_entry_demo/src/main/resources/dark/element/string.json @@ -0,0 +1,16 @@ +{ + "string":[ + { + "name": "entry_navigation_bar_content_with_mask_color", + "value": "#FFFFFFFF" + }, + { + "name": "entry_navigation_bar_content_without_mask_color", + "value": "#FFFFFFFF" + }, + { + "name": "entry_transparent_color", + "value": "#00000000" + } + ] +} diff --git a/future_entry_demo/src/main/resources/dark/media/bmp_american.png b/future_entry_demo/src/main/resources/dark/media/bmp_american.png new file mode 100644 index 0000000..81d7e30 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/bmp_american.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/bmp_candle_rect.png b/future_entry_demo/src/main/resources/dark/media/bmp_candle_rect.png new file mode 100644 index 0000000..8962923 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/bmp_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/bmp_close.png b/future_entry_demo/src/main/resources/dark/media/bmp_close.png new file mode 100644 index 0000000..7e401b7 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/bmp_close.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/bmp_solid_candle_rect.png b/future_entry_demo/src/main/resources/dark/media/bmp_solid_candle_rect.png new file mode 100644 index 0000000..5f21a4c Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/bmp_solid_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/bmp_tower.png b/future_entry_demo/src/main/resources/dark/media/bmp_tower.png new file mode 100644 index 0000000..cd9e607 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/bmp_tower.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/hq_indicator_edit.png b/future_entry_demo/src/main/resources/dark/media/hq_indicator_edit.png new file mode 100644 index 0000000..d2b51fe Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/hq_indicator_edit.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/ic_american.png b/future_entry_demo/src/main/resources/dark/media/ic_american.png new file mode 100644 index 0000000..b047d9f Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/ic_american.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/ic_candle_rect.png b/future_entry_demo/src/main/resources/dark/media/ic_candle_rect.png new file mode 100644 index 0000000..2259922 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/ic_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/ic_close.png b/future_entry_demo/src/main/resources/dark/media/ic_close.png new file mode 100644 index 0000000..bba4587 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/ic_close.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/ic_solid_candle_rect.png b/future_entry_demo/src/main/resources/dark/media/ic_solid_candle_rect.png new file mode 100644 index 0000000..8310bb8 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/ic_solid_candle_rect.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/ic_tower.png b/future_entry_demo/src/main/resources/dark/media/ic_tower.png new file mode 100644 index 0000000..82ac588 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/ic_tower.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/indicator_square_icon.png b/future_entry_demo/src/main/resources/dark/media/indicator_square_icon.png new file mode 100644 index 0000000..6e2d856 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/indicator_square_icon.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/market_heat_arrow.png b/future_entry_demo/src/main/resources/dark/media/market_heat_arrow.png new file mode 100644 index 0000000..b3d1632 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/market_heat_arrow.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/standard_price_check.png b/future_entry_demo/src/main/resources/dark/media/standard_price_check.png new file mode 100644 index 0000000..7ae04d8 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/standard_price_check.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/theme_icon_item_del.png b/future_entry_demo/src/main/resources/dark/media/theme_icon_item_del.png new file mode 100644 index 0000000..60497da Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/theme_icon_item_del.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_app.png b/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_app.png new file mode 100644 index 0000000..31c47f6 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_app.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_copy.png b/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_copy.png new file mode 100644 index 0000000..a566c97 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/theme_icon_system_config_copy.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/theme_img_period_introduction.png b/future_entry_demo/src/main/resources/dark/media/theme_img_period_introduction.png new file mode 100644 index 0000000..ee60cb1 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/theme_img_period_introduction.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/theme_kline_scroll_selected.png b/future_entry_demo/src/main/resources/dark/media/theme_kline_scroll_selected.png new file mode 100644 index 0000000..29fabc6 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/theme_kline_scroll_selected.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_pull.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_pull.png new file mode 100644 index 0000000..147d31a Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_pull.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_push.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_push.png new file mode 100644 index 0000000..9156823 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_arrow_push.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_close.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_close.png new file mode 100644 index 0000000..d20b492 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_close.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_open.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_open.png new file mode 100644 index 0000000..e2a0377 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_charts_open.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_instruction.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_instruction.png new file mode 100644 index 0000000..041bad3 Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_instruction.png differ diff --git a/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_search.png b/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_search.png new file mode 100644 index 0000000..46390bb Binary files /dev/null and b/future_entry_demo/src/main/resources/dark/media/trend_forecast_icon_search.png differ diff --git a/future_entry_demo/src/main/resources/en_US/element/string.json b/future_entry_demo/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..e851e7b --- /dev/null +++ b/future_entry_demo/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "同花顺期货通" + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/rawfile/SystemIndicatorsData.json b/future_entry_demo/src/main/resources/rawfile/SystemIndicatorsData.json new file mode 100644 index 0000000..868170a --- /dev/null +++ b/future_entry_demo/src/main/resources/rawfile/SystemIndicatorsData.json @@ -0,0 +1,3223 @@ +{ + "version": "1.0.0", + "indicators": [ + { + "indicatorId": "BBIZHUTU", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "BBI主图", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "M1", + "default": 3, + "low": 1, + "high": 250 + }, + { + "param_name": "M2", + "default": 6, + "low": 1, + "high": 250 + }, + { + "param_name": "M3", + "default": 12, + "low": 1, + "high": 250 + }, + { + "param_name": "M4", + "default": 24, + "low": 1, + "high": 250 + } + ], + "lineStyleArray": [ + { + "line_name": "BBI", + "color_key": "Brush.Yellow5", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BBI:(MA(CLOSE,M1)+MA(CLOSE,M2)+MA(CLOSE,M3)+MA(CLOSE,M4))/4;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中线,均线型", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"BBI\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"中线,均线型\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "432", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "多空线", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "M", + "default": 10, + "low": 2, + "high": 250 + } + ], + "lineStyleArray": [ + { + "line_name": "DKX", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MADKX", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MID:=(3*CLOSE+LOW+OPEN+HIGH)/6;\nDKX:(20*MID+19*REF(MID,1)+18*REF(MID,2)+17*REF(MID,3)+16*REF(MID,4)+15*REF(MID,5)+14*REF(MID,6)+13*REF(MID,7)+12*REF(MID,8)+11*REF(MID,9)+10*REF(MID,10)+9*REF(MID,11)+8*REF(MID,12)+7*REF(MID,13)+6*REF(MID,14)+5*REF(MID,15)+4*REF(MID,16)+3*REF(MID,17)+2*REF(MID,18)+REF(MID,20))/210;\nMADKX:MA(DKX,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "金叉死叉,长线", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DKX\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MADKX\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"金叉死叉,长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942199801086213", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DKX", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "M", + "default": 10, + "low": 2, + "high": 250 + } + ], + "lineStyleArray": [ + { + "line_name": "DKX", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MADKX", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MID:=(3*CLOSE+LOW+OPEN+HIGH)/6;\nDKX:(20*MID+19*REF(MID,1)+18*REF(MID,2)+17*REF(MID,3)+16*REF(MID,4)+15*REF(MID,5)+14*REF(MID,6)+13*REF(MID,7)+12*REF(MID,8)+11*REF(MID,9)+10*REF(MID,10)+9*REF(MID,11)+8*REF(MID,12)+7*REF(MID,13)+6*REF(MID,14)+5*REF(MID,15)+4*REF(MID,16)+3*REF(MID,17)+2*REF(MID,18)+REF(MID,20))/210;\nMADKX:MA(DKX,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "金叉死叉,长线", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DKX\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MADKX\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"金叉死叉,长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "QIHUOENE", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ENE", + "category": "5", + "categoryName": "抄底逃顶", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "M1", + "default": 6, + "low": 1, + "high": 1000 + }, + { + "param_name": "M2", + "default": 6, + "low": 1, + "high": 1000 + }, + { + "param_name": "M3", + "default": 25, + "low": 1, + "high": 1000 + } + ], + "lineStyleArray": [ + { + "line_name": "UPPER", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "LOWER", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ENE", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "UPPER:(1+M1/100)*MA(CLOSE,M3);\nLOWER:(1-M2/100)*MA(CLOSE,M3);\nENE:(UPPER+LOWER)/2;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "支撑压力,波段", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"UPPER\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"LOWER\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ENE\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"支撑压力,波段\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "DQATD", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "唐奇安通道", + "category": "4", + "categoryName": "波段顶底", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "P1", + "default": 20, + "low": 1, + "high": 1000 + }, + { + "param_name": "P2", + "default": 20, + "low": 1, + "high": 1000 + } + ], + "lineStyleArray": [ + { + "line_name": "H1", + "color_key": "Brush.Yellow5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "L1", + "color_key": "Brush.Pink5", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "H1:HHV(H,P1);\nL1:LLV(L,P2);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "突破指标", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"H1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"L1\\\",\\n \\\"color_key\\\": \\\"Brush.Pink5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"突破指标\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "C09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "不显示指标", + "category": "", + "categoryName": "", + "attachedType": "0", + "paramsArray": [], + "lineStyleArray": [], + "descripe": "", + "sourceCode": "DRAWKLINE(HIGH,OPEN,LOW,CLOSE);", + "sourceCodeLanguage": "", + "sourceCodeEncrypt": false, + "tags": "", + "version": 0, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "1C09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MA", + "category": "10", + "categoryName": "支撑压力", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "P1", + "default": 5, + "low": 1, + "high": 1000 + }, + { + "param_name": "P2", + "default": 10, + "low": 1, + "high": 1000 + }, + { + "param_name": "P3", + "default": 20, + "low": 1, + "high": 1000 + }, + { + "param_name": "P4", + "default": 30, + "low": 1, + "high": 1000 + }, + { + "param_name": "P5", + "default": 60, + "low": 1, + "high": 1000 + }, + { + "param_name": "P6", + "default": 1, + "low": 1, + "high": 1000 + }, + { + "param_name": "P7", + "default": 1, + "low": 1, + "high": 1000 + }, + { + "param_name": "P8", + "default": 1, + "low": 1, + "high": 1000 + } + ], + "lineStyleArray": [ + { + "line_name": "MA1", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA4", + "color_key": "Brush.Cyan6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA5", + "color_key": "Brush.DarkBlue6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA6", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "MA7", + "color_key": "Brush.Purple6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "MA8", + "color_key": "Brush.Red6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "MA1:MA(CLOSE,P1);\nMA2:MA(CLOSE,P2);\nMA3:MA(CLOSE,P3);\nMA4:MA(CLOSE,P4);\nMA5:MA(CLOSE,P5);\nMA6:MA(CLOSE,P6);\nMA7:MA(CLOSE, P7);\nMA8:MA(CLOSE, P8);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 50, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA4\\\",\\n \\\"color_key\\\": \\\"Brush.Cyan6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA5\\\",\\n \\\"color_key\\\": \\\"Brush.DarkBlue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA6\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"MA7\\\",\\n \\\"color_key\\\": \\\"Brush.Purple6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"MA8\\\",\\n \\\"color_key\\\": \\\"Brush.Red6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"-1\",\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "1609D3", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "SAR主图", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "N", + "low": 1, + "high": 100, + "default": 4 + }, + { + "param_name": "STEP", + "low": 1, + "high": 100, + "default": 2 + }, + { + "param_name": "MVALUE", + "low": 1, + "high": 100, + "default": 20 + } + ], + "lineStyleArray": [ + { + "line_name": "SARVAL", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "STEP1:=STEP/100;\nMVALUE1:=MVALUE/100;\nSARVAL:SAR(N, STEP1, MVALUE1), NODRAW;\nSARTYPE:=SARSHOWTYPE(N, STEP1, MVALUE1);\nDRAWTEXT(SARTYPE=1, -SARVAL, '●'), COLORGREEN;\nDRAWTEXT(SARTYPE=0, SARVAL, '●'), COLORRED;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中短期,买卖信号", + "version": 76, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"SARVAL\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"-1\",\"tags\":\"中短期,买卖信号\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2809C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "EXPMA", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "P1", + "default": 5, + "low": 1, + "high": 1000 + }, + { + "param_name": "P2", + "default": 10, + "low": 1, + "high": 1000 + }, + { + "param_name": "P3", + "default": 20, + "low": 1, + "high": 1000 + }, + { + "param_name": "P4", + "default": 60, + "low": 1, + "high": 1000 + }, + { + "param_name": "P5", + "default": 120, + "low": 1, + "high": 1000 + }, + { + "param_name": "P6", + "default": 250, + "low": 1, + "high": 1000 + }, + { + "param_name": "P7", + "default": 1, + "low": 1, + "high": 1000 + }, + { + "param_name": "P8", + "default": 1, + "low": 1, + "high": 1000 + } + ], + "lineStyleArray": [ + { + "line_name": "EMA1", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA_2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA4", + "color_key": "Brush.Cyan6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA5", + "color_key": "Brush.DarkBlue6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA6", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA7", + "color_key": "Brush.Purple5", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA8", + "color_key": "Brush.Red6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "EMA1:EMA(CLOSE,P1);\nEMA_2:EMA(CLOSE,P2);\nEMA3:EMA(CLOSE,P3);\nEMA4:EMA(CLOSE,P4);\nEMA5:EMA(CLOSE,P5);\nEMA6:EMA(CLOSE,P6);\nEMA7:EMA(CLOSE,P7);\nEMA8:EMA(CLOSE,P8);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 62, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"EMA1\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA_2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA4\\\",\\n \\\"color_key\\\": \\\"Brush.Cyan6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA5\\\",\\n \\\"color_key\\\": \\\"Brush.DarkBlue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA6\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA7\\\",\\n \\\"color_key\\\": \\\"Brush.Purple5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA8\\\",\\n \\\"color_key\\\": \\\"Brush.Red6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"-1\",\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3D09CB", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "BOLL", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "1", + "paramsArray": [ + { + "param_name": "N", + "default": 20, + "low": 5, + "high": 300 + }, + { + "param_name": "M", + "default": 20, + "low": 1, + "high": 100 + }, + { + "param_name": "P", + "default": 2, + "low": 1, + "high": 10 + }, + { + "param_name": "M1", + "default": 5, + "low": 1, + "high": 999 + }, + { + "param_name": "M2", + "default": 10, + "low": 1, + "high": 999 + } + ], + "lineStyleArray": [ + { + "line_name": "MID", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "UPPER", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "LOWER", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA1", + "color_key": "Brush.Cyan6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "MA2", + "color_key": "Brush.Purple6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "MID:MA(CLOSE, N);\nTMP2:=STD(CLOSE, M);\nUPPER:MID+P*TMP2;\nLOWER:MID-P*TMP2;\nMA1:MA(C, M1);\nMA2:MA(C, M2);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "支撑压力,波段", + "version": 34, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MID\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"UPPER\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"LOWER\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Cyan6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"MA2\\\",\\n \\\"color_key\\\": \\\"Brush.Purple6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"tags\":\"支撑压力,波段\",\"precision\":\"-1\",\"draw_line_type\":0}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "74360288657096969", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "现货价格", + "category": "99", + "categoryName": "其他指标", + "attachedType": "1", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "现货价格", + "color_key": "Brush.Gray9", + "line_thick": 2, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "现货价格:COMMODITY;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"现货价格\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 2,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2D09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "OBV", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "OBV", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "VA:=IF(CLOSE>REF(CLOSE,1),VOL,IF(CLOSELC, VOL, 0), N)/SUM(IFELSE(CLOSE<=LC, VOL, 0), N) * 100;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "波段,强弱指标", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"VR\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"A\\\",\\n \\\"color_key\\\": \\\"Brush.Gray5\\\",\\n \\\"line_thick\\\": 2,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"B\\\",\\n \\\"color_key\\\": \\\"Brush.Gray5\\\",\\n \\\"line_thick\\\": 2,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"波段,强弱指标\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3089DA", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "成交量", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N1", + "default": 5, + "low": 1, + "high": 999 + }, + { + "param_name": "N2", + "default": 10, + "low": 1, + "high": 999 + }, + { + "param_name": "N3", + "default": 20, + "low": 1, + "high": 999 + } + ], + "lineStyleArray": [ + { + "line_name": "MA1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA2", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA3", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "总量:V/SHARECOUNTPERUNIT,VOLSTICK;\nMA1:MA(总量, N1);\nMA2:MA(总量, N2);\nMA3:MA(总量, N3);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA2\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA3\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":0}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711509255", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MTM", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 6, + "low": 2, + "high": 120 + }, + { + "param_name": "M", + "default": 6, + "low": 2, + "high": 60 + } + ], + "lineStyleArray": [ + { + "line_name": "MTM", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MTMMA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MTM:CLOSE-REF(CLOSE,N);\nMTMMA:MA(MTM,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MTM\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MTMMA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711488774", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "CR", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 26, + "low": 5, + "high": 300 + }, + { + "param_name": "M1", + "default": 5, + "low": 1, + "high": 100 + }, + { + "param_name": "M2", + "default": 10, + "low": 1, + "high": 100 + }, + { + "param_name": "M3", + "default": 20, + "low": 1, + "high": 100 + }, + { + "param_name": "M4", + "default": 40, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "CR", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "CRMA1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "CRMA2", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "CRMA3", + "color_key": "Brush.Blue8", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "CRMA4", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MID := (HIGH+LOW+CLOSE)/3;\nCR:SUM(MAX(0,HIGH-REF(MID,1)),N)/SUM(MAX(0,REF(MID,1)-LOW),N)*100;\nCRMA1:REF(MA(CR,M1),M1/2.5+1);\nCRMA2:REF(MA(CR,M2),M2/2.5+1);\nCRMA3:REF(MA(CR,M3),M3/2.5+1);\nCRMA4:REF(MA(CR,M4),M4/2.5+1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中长线,买卖强弱", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"CR\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"CRMA1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"CRMA2\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"CRMA3\\\",\\n \\\"color_key\\\": \\\"Brush.Blue8\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"CRMA4\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"中长线,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2E09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ASI", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "M1", + "default": 26, + "low": 5, + "high": 300 + }, + { + "param_name": "M2", + "default": 10, + "low": 1, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "ASI", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ASIT", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "LC:=REF(CLOSE,1);\nAA:=ABS(HIGH-LC);\nBB:=ABS(LOW-LC);\nCC:=ABS(HIGH-REF(LOW,1));\nDD:=ABS(LC-REF(OPEN,1));\nR:=IF(AA>BB AND AA>CC,AA+BB/2+DD/4,IF(BB>CC AND BB>AA,BB+AA/2+DD/4,CC+DD/4));\nX:=(CLOSE-LC+(CLOSE-OPEN)/2+LC-REF(OPEN,1));\nSI:=16*X/R*MAX(AA,BB);\nASI:SUM(SI,M1);\nASIT:MA(ASI,M2);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"ASI\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ASIT\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"短线,背离\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2609C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DMI", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "low": 1, + "high": 300, + "default": 14 + }, + { + "param_name": "M", + "low": 1, + "high": 300, + "default": 6 + } + ], + "lineStyleArray": [ + { + "line_name": "DI1", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "DI2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ADX", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ADXR", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "TR1:= SUM(MAX(MAX(HIGH-LOW,ABS(HIGH-REF(CLOSE,1))),ABS(LOW-REF(CLOSE,1))),N);\nHD:= HIGH-REF(HIGH,1);\nLD:= REF(LOW,1)-LOW;\nDMP:= SUM(IF(HD>0 AND HD>LD,HD,0),N);\nDMM:= SUM(IF(LD>0 AND LD>HD,LD,0),N);\nDI1: DMP*100/TR1;\nDI2: DMM*100/TR1;\nADX: MA(ABS(DI2-DI1)/(DI2+DI1)*100,M);\nADXR:(ADX+REF(ADX,M))/2;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中长期,买卖强弱", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DI1\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"DI2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ADX\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ADXR\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"中长期,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2A09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ARBR", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 26, + "low": 2, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "AR", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "BR", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "AR:SUM(HIGH-OPEN,N)/SUM(OPEN-LOW,N)*100;\nBR:SUM(MAX(0,HIGH-REF(CLOSE,1)),N)/SUM(MAX(0,REF(CLOSE,1)-LOW),N)*100;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "波段,买卖强弱", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"AR\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"BR\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"波段,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2109CE", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ATR", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 14, + "low": 1, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "TR", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ATR", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "TR:MAX(MAX((HIGH-LOW),ABS(REF(CLOSE,1)-HIGH)),ABS(REF(CLOSE,1)-LOW));\nATR:MA(TR,N);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "波段顶底", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"TR\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ATR\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"波段顶底\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711525638", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ROC", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 12, + "low": 1, + "high": 100 + }, + { + "param_name": "M", + "default": 6, + "low": 1, + "high": 50 + } + ], + "lineStyleArray": [ + { + "line_name": "ROC", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ROCMA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "ROC:(CLOSE-REF(CLOSE,N))/REF(CLOSE,N)*100;\nROCMA:MA(ROC,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"ROC\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ROCMA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711492870", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ADTM", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 23, + "low": 1, + "high": 100 + }, + { + "param_name": "M", + "default": 8, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "ADTM", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ADTMMA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "DTM:=IF(OPEN<=REF(OPEN,1),0,MAX((HIGH-OPEN),(OPEN-REF(OPEN,1))));\nDBM:=IF(OPEN>=REF(OPEN,1),0,MAX((OPEN-LOW),(REF(OPEN,1)-OPEN)));\nSTM:=SUM(DTM,N);\nSBM:=SUM(DBM,N);\nADTM:IF(STM>SBM,(STM-SBM)/STM,IF(STM==SBM,0,(STM-SBM)/SBM));\nADTMMA:MA(ADTM,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"ADTM\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ADTMMA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711476487", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "QHLSR", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "QHL5", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "QHL10", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "QHL:=(CLOSE-REF(CLOSE,1))-(VOL-REF(VOL,1))*(REF(HIGH,1)-REF(LOW,1))/REF(VOL,1);\nA:=SUM(IF(QHL>0,QHL,0),5);\nE:=SUM(IF(QHL>0,QHL,0),10);\nB:=ABS(SUM(IF(QHL<0,QHL,0),5));\nF:=ABS(SUM(IF(QHL<0,QHL,0),10));\nD:=A/(A+B);\nG:=E/(E+F);\nQHL5:IF(SUM(IF(QHL>0,1,0),5)=5,1,IF(SUM(IF(QHL<0,1,0),5)=5,0,D));\nQHL10:G;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "支撑压力", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"QHL5\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"QHL10\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"支撑压力\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711476486", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "RC", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 59, + "low": 1, + "high": 200 + } + ], + "lineStyleArray": [ + { + "line_name": "ATR", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "RC:=CLOSE/REF(CLOSE,N);\nATR:SMA(REF(RC,1),N,1);\n// 若不想要展示参考线,则把下面语句删除\n参考线1:1,DOTLINE,COLOR91374C6F,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "均线", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"ATR\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"均线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711468294", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "SHORT短线", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "low": 2, + "high": 250, + "default": 5 + } + ], + "lineStyleArray": [ + { + "line_name": "SHORT", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "VOL1:=MA((VOL-REF(VOL,1))/REF(VOL,1), 5);\nJC2:=(CLOSE-MA(CLOSE,24))/MA(CLOSE,24)*100;\nSHORT:JC2*(1+VOL1);\nMA1:MA(SHORT,N);\nSHORT_STICK:SHORT,COLORSTICK;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"SHORT\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"短线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711464199", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "LON长线", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 10, + "low": 2, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "LONG", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "LC:=REF(CLOSE,1);\nVID:=SUM(VOL,2)/((HHV(HIGH,2)-LLV(LOW,2))*100);\nRC:=(CLOSE-LC)*VID;\nLON:=SUM(RC,0);\nLONMA1:=SMA(LON,10,1);\nLONMA2:=SMA(LON,20,1);\nLONG:LONMA1-LONMA2;\nMA1:MA(LONG, N);\nLONG_STICK:LONG,COLORSTICK;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中线,资金动向", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"LONG\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"中线,资金动向\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711472390", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "SRDM", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 30, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "SRDM", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "ASRDM", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "DMZ:=IF((HIGH+LOW)<=(REF(HIGH,1)+REF(LOW,1)),0,MAX(ABS(HIGH-REF(HIGH,1)),ABS(LOW-REF(LOW,1))));\nDMF:=IF((HIGH+LOW)>=(REF(HIGH,1)+REF(LOW,1)),0,MAX(ABS(HIGH-REF(HIGH,1)),ABS(LOW-REF(LOW,1))));\nADMZ:=MA(DMZ,10);\nADMF:=MA(DMF,10);\nSRDM:IF(ADMZ>ADMF,(ADMZ-ADMF)/ADMZ,IF(ADMZ==ADMF,0,(ADMZ-ADMF)/ADMF));\nASRDM:SMA(SRDM,N,1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短期,买卖强弱", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"SRDM\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"ASRDM\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"短期,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "50CCC", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DPO", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "low": 2, + "high": 90, + "default": 20 + }, + { + "param_name": "M", + "low": 2, + "high": 60, + "default": 6 + } + ], + "lineStyleArray": [ + { + "line_name": "DPO", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MADPO", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "DPO:CLOSE-REF(MA(CLOSE,N),N/2+1);\nMADPO:MA(DPO,M);\nDRAWTEXT(CROSS(DPO, MADPO), DPO, '↑买'), COLORRED;\nDRAWTEXT(CROSS(MADPO, DPO), MADPO, '↓卖'),COLORGREEN;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中短期,金叉死叉", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DPO\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MADPO\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"中短期,金叉死叉\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3509C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "BIAS", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N1", + "default": 6, + "low": 1, + "high": 300 + }, + { + "param_name": "N2", + "default": 12, + "low": 1, + "high": 300 + }, + { + "param_name": "N3", + "default": 24, + "low": 1, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "BIAS1", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "BIAS2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "BIAS3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MAN1:=MA(CLOSE,N1);\nMAN2:=MA(CLOSE,N2);\nMAN3:=MA(CLOSE,N3);\nBIAS1:(CLOSE-MAN1)/MAN1*100;\nBIAS2:(CLOSE-MAN2)/MAN2*100;\nBIAS3:(CLOSE-MAN3)/MAN3*100;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,偏离率", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"BIAS1\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"BIAS2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"BIAS3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"短线,偏离率\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3189D4", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "CCI", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 14, + "low": 2, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "CCI", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "A", + "color_key": "Brush.Gray5", + "line_thick": 2, + "is_selected": 1 + }, + { + "line_name": "B", + "color_key": "Brush.Gray5", + "line_thick": 2, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "A:100,DOTLINE,LINETHICK2;\nB:-100,DOTLINE,LINETHICK2;\nTYP:=(HIGH+LOW+CLOSE)/3;\nCCI:(TYP-MA(TYP,N))/(0.015*AVEDEV(TYP,N));", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"CCI\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"A\\\",\\n \\\"color_key\\\": \\\"Brush.Gray5\\\",\\n \\\"line_thick\\\": 2,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"B\\\",\\n \\\"color_key\\\": \\\"Brush.Gray5\\\",\\n \\\"line_thick\\\": 2,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"短线,背离\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "80DAF", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "KD", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 9, + "low": 1, + "high": 100 + }, + { + "param_name": "M1", + "default": 3, + "low": 2, + "high": 40 + }, + { + "param_name": "M2", + "default": 3, + "low": 2, + "high": 40 + } + ], + "lineStyleArray": [ + { + "line_name": "K", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "D", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "RSV:=(CLOSE-LLV(LOW,N))/(HHV(HIGH,N)-LLV(LOW,N))*100;\nK:SMA(RSV,M1,1);\nD:SMA(K,M2,1);\n// 若不想要展示参考线,则把下面语句删除\n参考线1:20,DOTLINE,COLOR91374C6F,NOTEXT;\n参考线2:80,DOTLINE,COLOR91374C6F,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"K\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"D\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"短线,背离\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711521543", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "PSY", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 12, + "low": 2, + "high": 100 + }, + { + "param_name": "M", + "default": 6, + "low": 2, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "PSY", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "PSYMA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "PSY:COUNT(CLOSE>REF(CLOSE,1),N)/N*100;\nPSYMA:MA(PSY, M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"PSY\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"PSYMA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3312084", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "SKDJ", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "low": 2, + "high": 90, + "default": 9 + }, + { + "param_name": "M", + "low": 2, + "high": 30, + "default": 3 + } + ], + "lineStyleArray": [ + { + "line_name": "K", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "D", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "LOWV:=LLV(LOW,N);\nHIGHV:=HHV(HIGH,N);\nRSV:=EMA((CLOSE-LOWV)/(HIGHV-LOWV)*100,M);\nK:EMA(RSV,M);\nD:MA(K,M);\n// 若不想要展示参考线,则把下面语句删除\n参考线1:20,DOTLINE,COLOR91374C6F,NOTEXT;\n参考线2:80,DOTLINE,COLOR91374C6F,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"K\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"D\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"短线,背离\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "1F09CF", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "BBI", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "M1", + "default": 3, + "low": 1, + "high": 300 + }, + { + "param_name": "M2", + "default": 6, + "low": 1, + "high": 300 + }, + { + "param_name": "M3", + "default": 12, + "low": 1, + "high": 300 + }, + { + "param_name": "M4", + "default": 24, + "low": 1, + "high": 300 + } + ], + "lineStyleArray": [ + { + "line_name": "BBI", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "A", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BBI:(MA(CLOSE,M1)+MA(CLOSE,M2)+MA(CLOSE,M3)+MA(CLOSE,M4))/4;\nA:CLOSE;\nDRAWTEXT(CROSS(BBI, C),C,'↓卖'),COLORGREEN;\nDRAWTEXT(CROSS(C, BBI), C, '↑买'),COLORRED;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中线,均线型", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"BBI\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"A\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"中线,均线型\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "BOLLFUTU", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "BOLL副图", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 20, + "low": 5, + "high": 300 + }, + { + "param_name": "M", + "default": 20, + "low": 1, + "high": 100 + }, + { + "param_name": "P", + "default": 2, + "low": 1, + "high": 10 + }, + { + "param_name": "M1", + "default": 5, + "low": 1, + "high": 999 + }, + { + "param_name": "M2", + "default": 10, + "low": 1, + "high": 999 + } + ], + "lineStyleArray": [ + { + "line_name": "MID", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "TOP", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "BOTTOM", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA1", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "MA2", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "KLINE:=DRAWBARLINE(HIGH,OPEN,LOW,CLOSE);\nMID:MA(CLOSE, N);\nTMP2:=STD(CLOSE, M);\nUPPER:MID+P*TMP2;\nLOWER:MID-P*TMP2;\nMA1:MA(C, M1);\nMA2:MA(C, M2);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "支撑压力", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MID\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"TOP\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"BOTTOM\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"MA2\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"支撑压力\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "EMAFUTU", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "EMA", + "category": "8", + "categoryName": "趋势预测", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "P1", + "default": 5, + "low": 1, + "high": 1000 + }, + { + "param_name": "P2", + "default": 10, + "low": 1, + "high": 1000 + }, + { + "param_name": "P3", + "default": 20, + "low": 1, + "high": 1000 + }, + { + "param_name": "P4", + "default": 60, + "low": 1, + "high": 1000 + }, + { + "param_name": "P5", + "default": 120, + "low": 1, + "high": 1000 + }, + { + "param_name": "P6", + "default": 250, + "low": 1, + "high": 1000 + }, + { + "param_name": "P7", + "default": 1, + "low": 1, + "high": 1000 + }, + { + "param_name": "P8", + "default": 1, + "low": 1, + "high": 1000 + } + ], + "lineStyleArray": [ + { + "line_name": "EMA1", + "color_key": "Brush.Blue5", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA_2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA4", + "color_key": "Brush.Cyan6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "EMA5", + "color_key": "Brush.DarkBlue6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA6", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA7", + "color_key": "Brush.Purple5", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "EMA8", + "color_key": "Brush.Red6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "KLINE:=DRAWKLINE(HIGH,OPEN,LOW,CLOSE);\nEMA1:EMA(CLOSE,P1);\nEMA_2:EMA(CLOSE,P2);\nEMA3:EMA(CLOSE,P3);\nEMA4:EMA(CLOSE,P4);\nEMA5:EMA(CLOSE,P5);\nEMA6:EMA(CLOSE,P6);\nEMA7:EMA(CLOSE,P7);\nEMA8:EMA(CLOSE,P8);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"EMA1\\\",\\n \\\"color_key\\\": \\\"Brush.Blue5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA_2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA4\\\",\\n \\\"color_key\\\": \\\"Brush.Cyan6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"EMA5\\\",\\n \\\"color_key\\\": \\\"Brush.DarkBlue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA6\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA7\\\",\\n \\\"color_key\\\": \\\"Brush.Purple5\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"EMA8\\\",\\n \\\"color_key\\\": \\\"Brush.Red6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711488775", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "B3612", + "category": "4", + "categoryName": "波段顶底", + "attachedType": "0", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "B36", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "B612", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "B36:MA(CLOSE,3)-MA(CLOSE,6);\nB612:MA(CLOSE,6)-MA(CLOSE,12);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"B36\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"B612\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2909C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "TRIX", + "category": "3", + "categoryName": "金叉死叉", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "P", + "low": 1, + "high": 300, + "default": 12 + }, + { + "param_name": "N", + "low": 1, + "high": 300, + "default": 20 + } + ], + "lineStyleArray": [ + { + "line_name": "TRIX", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "TRMA1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "TR1:=EMA(EMA(EMA(CLOSE,P),P),P);\nTRIX:(TR1-REF(TR1,1))/REF(TR1,1)*100;\nTRMA1:MA(TRIX,N);\nDRAWTEXT(CROSS(TRIX, TRMA1), TRIX, '↑买'),COLORRED;\nDRAWTEXT(CROSS(TRMA1, TRIX), TRMA1, '↓卖'),COLORGREEN;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"TRIX\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"TRMA1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711480583", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MI", + "category": "3", + "categoryName": "金叉死叉", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 12, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "A", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MI", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "A:CLOSE-REF(CLOSE,N);\nMI:SMA(A,N,1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"A\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MI\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3F09CF", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DMA", + "category": "3", + "categoryName": "金叉死叉", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "SHORT", + "default": 10, + "low": 2, + "high": 500 + }, + { + "param_name": "LONG", + "default": 50, + "low": 2, + "high": 500 + }, + { + "param_name": "M", + "default": 10, + "low": 2, + "high": 500 + } + ], + "lineStyleArray": [ + { + "line_name": "DDD", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "DDDMA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "DDD:MA(CLOSE,SHORT)-MA(CLOSE,LONG);\nDDDMA:MA(DDD,M);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "中短期,买卖能量", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DDD\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"DDDMA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"中短期,买卖能量\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711484679", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DBCD", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 5, + "low": 1, + "high": 100 + }, + { + "param_name": "M", + "default": 16, + "low": 1, + "high": 100 + }, + { + "param_name": "T", + "default": 76, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "DBCD", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MM", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BIAS1:=(CLOSE-MA(CLOSE,N))/MA(CLOSE,N);\nDIF:=(BIAS1-REF(BIAS1,M));\nDBCD:SMA(DIF,T,1);\nMM:MA(DBCD,5);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,偏离率", + "version": 2, + "sdkVersion": "", + "precision": 4, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DBCD\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MM\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":4,\"tags\":\"短线,偏离率\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711468295", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "ZDZB", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N1", + "default": 125, + "low": 5, + "high": 250 + }, + { + "param_name": "N2", + "default": 5, + "low": 1, + "high": 20 + }, + { + "param_name": "N3", + "default": 20, + "low": 5, + "high": 60 + } + ], + "lineStyleArray": [ + { + "line_name": "B", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "D", + "color_key": "Brush.Blue6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "A := COUNT(CLOSE >= REF(CLOSE, 1), N1) / COUNT(CLOSE < REF(CLOSE, 1), N1);\nB :MA(A, N2);\nD :MA(A, N3);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "筑底", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"B\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"D\\\",\\n \\\"color_key\\\": \\\"Brush.Blue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":2,\"tags\":\"筑底\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711480582", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MICD", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 3, + "low": 1, + "high": 100 + }, + { + "param_name": "N1", + "default": 10, + "low": 1, + "high": 100 + }, + { + "param_name": "N2", + "default": 20, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "DIF", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MICD", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "MI:=CLOSE-REF(CLOSE,1);\nAMI:=SMA(MI,N,1);\nDIF:MA(REF(AMI,1),N1)-MA(REF(AMI,1),N2);\nMICD:SMA(DIF,10,1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线,金叉死叉", + "version": 2, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DIF\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MICD\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"长线,金叉死叉\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711472391", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "RCCD", + "category": "11", + "categoryName": "摆动分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 59, + "low": 1, + "high": 200 + }, + { + "param_name": "N1", + "default": 21, + "low": 1, + "high": 200 + }, + { + "param_name": "N2", + "default": 28, + "low": 1, + "high": 200 + } + ], + "lineStyleArray": [ + { + "line_name": "DIF", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "RCCD", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "RC:=CLOSE/REF(CLOSE,N);\nARC:=SMA(REF(RC,1),N,1);\nDIF:MA(REF(ARC,1),N1)-MA(REF(ARC,1),N2);\nRCCD:SMA(DIF,N,1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "均线", + "version": 2, + "sdkVersion": "", + "precision": 4, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DIF\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"RCCD\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":4,\"tags\":\"均线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "61942286711484678", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "DDI", + "category": "2", + "categoryName": "红涨绿跌", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 13, + "low": 1, + "high": 100 + }, + { + "param_name": "N1", + "default": 30, + "low": 1, + "high": 100 + }, + { + "param_name": "M", + "default": 10, + "low": 1, + "high": 100 + }, + { + "param_name": "M1", + "default": 5, + "low": 1, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "ADDI", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "AD", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "TR:=MAX(ABS(HIGH-REF(HIGH,1)),ABS(LOW-REF(LOW,1)));\nDMZ:=IF((HIGH+LOW)<=(REF(HIGH,1)+REF(LOW,1)),0,MAX(ABS(HIGH-REF(HIGH,1)),ABS(LOW-REF(LOW,1))));\nDMF:=IF((HIGH+LOW)>=(REF(HIGH,1)+REF(LOW,1)),0,MAX(ABS(HIGH-REF(HIGH,1)),ABS(LOW-REF(LOW,1))));\nDIZ:=SUM(DMZ,N)/(SUM(DMZ,N)+SUM(DMF,N));\nDIF:=SUM(DMF,N)/(SUM(DMF,N)+SUM(DMZ,N));\nDDI:=DIZ-DIF;\nDDI,COLORSTICK;\nADDI:SMA(DDI,N1,M);\nAD:MA(ADDI,M1);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线,金叉死叉", + "version": 2, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"ADDI\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"AD\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":-1,\"tags\":\"长线,金叉死叉\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "20A9F", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "持仓量", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "持仓量", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "成交量:V/SHARECOUNTPERUNIT,VOLSTICK;\n持仓量:OPI;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 30, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"持仓量\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"0\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "9C89DB", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "成交额", + "category": "12", + "categoryName": "量价分析", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "M1", + "default": 5, + "low": 1, + "high": 999 + }, + { + "param_name": "M2", + "default": 10, + "low": 1, + "high": 999 + }, + { + "param_name": "M3", + "default": 20, + "low": 1, + "high": 999 + }, + { + "param_name": "M4", + "default": 1, + "low": 1, + "high": 999 + }, + { + "param_name": "M5", + "default": 1, + "low": 1, + "high": 999 + } + ], + "lineStyleArray": [ + { + "line_name": "MA1", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "MA4", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 0 + }, + { + "line_name": "MA5", + "color_key": "Brush.Blue6", + "line_thick": 1, + "is_selected": 0 + } + ], + "descripe": "", + "sourceCode": "总金额:AMOUNT,VOLSTICK;\nMA1:MA(总金额, M1);\nMA2:MA(总金额, M2);\nMA3:MA(总金额, M3);\nMA4:MA(总金额, M4);\nMA5:MA(总金额, M5);", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 68, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"MA1\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"MA4\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n },\\n {\\n \\\"line_name\\\": \\\"MA5\\\",\\n \\\"color_key\\\": \\\"Brush.Blue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 0\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"0\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3E8C93", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MACD", + "category": "3", + "categoryName": "金叉死叉", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "SHORT", + "low": 2, + "high": 200, + "default": 12 + }, + { + "param_name": "LONG", + "low": 2, + "high": 200, + "default": 26 + }, + { + "param_name": "M", + "low": 2, + "high": 200, + "default": 9 + } + ], + "lineStyleArray": [ + { + "line_name": "DIFF", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "DEA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BACKGROUNDSTYLE(2);\nDIFF:EMA(CLOSE,SHORT)-EMA(CLOSE,LONG);\nDEA:EMA(DIFF,M);\nMACD:(DIFF-DEA)*2,COLORSTICK;\nJC:=CROSS(DIFF,DEA);\nSC:=CROSSDOWN(DIFF,DEA);\nN1:=BARSLAST(JC) + 1;\nN2:=BARSLAST(SC) + 1;\nHH:=VALUEWHEN(SC, HHV(H, N1));\nHH2:=VALUEWHEN(SC,REF(HH,1));\nMHD:=VALUEWHEN(SC,HHV(MACD,N1));\nMHD2:=VALUEWHEN(SC,REF(MHD,1));\nLL:=VALUEWHEN(JC, LLV(L, N2));\nLL2:=VALUEWHEN(JC,REF(LL,1));\nMLD:=VALUEWHEN(JC,LLV(MACD,N2));\nMLD2:=VALUEWHEN(JC,REF(MLD,1));\nA:=SC&&HH>HH2&&MHDMLD2;//底背离\nDRAWTEXT(A, DIFF, '顶背离'),COLORGREEN;\nDRAWTEXT(B, DIFF, '底背离'),COLORRED;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线", + "version": 34, + "sdkVersion": "", + "precision": -1, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DIFF\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"DEA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"-1\",\"tags\":\"长线\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "1A09C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "KDJ", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "P1", + "default": 9, + "low": 1, + "high": 100 + }, + { + "param_name": "P2", + "default": 3, + "low": 2, + "high": 40 + }, + { + "param_name": "P3", + "default": 3, + "low": 2, + "high": 40 + } + ], + "lineStyleArray": [ + { + "line_name": "K", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "D", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "J", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "RSV:=(CLOSE-LLV(LOW,P1))/(HHV(HIGH,P1)-LLV(LOW,P1))*100;\nK:SMA(RSV,P2,1);\nD:SMA(K,P3,1);\nJ:3*K-2*D;\n// 若不想要展示参考线,则把下面语句删除\n参考线1:20,DOTLINE,COLOR91374C6F,NOTEXT;\n参考线2:80,DOTLINE,COLOR91374C6F,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 30, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"K\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"D\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"J\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"2\",\"tags\":\"短线,背离\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "3009C8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "RSI", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "P1", + "default": 6, + "low": 2, + "high": 250 + }, + { + "param_name": "P2", + "default": 12, + "low": 2, + "high": 250 + }, + { + "param_name": "P3", + "default": 24, + "low": 2, + "high": 250 + } + ], + "lineStyleArray": [ + { + "line_name": "RSI1", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "RSI2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "RSI3", + "color_key": "Brush.Pink6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "LC:=REF(CLOSE,1);\nTEMP1:=MAX(CLOSE-LC,0);\nTEMP2:=ABS(CLOSE-LC);\nRSI1:SMA(TEMP1,P1,1)/SMA(TEMP2,P1,1)*100;\nRSI2:SMA(TEMP1,P2,1)/SMA(TEMP2,P2,1)*100;\nRSI3:SMA(TEMP1,P3,1)/SMA(TEMP2,P3,1)*100;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,买卖强弱", + "version": 38, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"RSI1\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"RSI2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"RSI3\\\",\\n \\\"color_key\\\": \\\"Brush.Pink6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"2\",\"tags\":\"短线,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "50C83", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "WR", + "category": "4", + "categoryName": "波段顶底", + "attachedType": "0", + "paramsArray": [ + { + "param_name": "N", + "default": 10, + "low": 2, + "high": 100 + }, + { + "param_name": "N1", + "default": 6, + "low": 2, + "high": 100 + } + ], + "lineStyleArray": [ + { + "line_name": "WR1", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "WR2", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BACKGROUNDSTYLE(1);\nWR1:100*(HHV(HIGH,N)-CLOSE)/(HHV(HIGH,N)-LLV(LOW,N));\nWR2:100*(HHV(HIGH,N1)-CLOSE)/(HHV(HIGH,N1)-LLV(LOW,N1));\n// 若不想要展示参考线,则把下面语句删除\n参考线1:20,DOTLINE,COLOR91374C6F,NOTEXT;\n参考线2:80,DOTLINE,COLOR91374C6F,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,买卖强弱", + "version": 28, + "sdkVersion": "", + "precision": 2, + "drawLineType": 0, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"extra_yaxis_value\":\"\",\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"WR1\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"WR2\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"draw_line_type\":0,\"precision\":\"2\",\"tags\":\"短线,买卖强弱\"}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "98C0F", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "MACD", + "category": "3", + "categoryName": "金叉死叉", + "attachedType": "2", + "paramsArray": [ + { + "param_name": "SHORT", + "default": 12, + "low": 5, + "high": 40 + }, + { + "param_name": "LONG", + "default": 26, + "low": 10, + "high": 200 + }, + { + "param_name": "M", + "default": 9, + "low": 2, + "high": 40 + } + ], + "lineStyleArray": [ + { + "line_name": "DIFF", + "color_key": "Brush.Gray9", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "DEA", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "BACKGROUNDSTYLE(2);\nDIFF:EMA(CLOSE,SHORT)-EMA(CLOSE,LONG);\nDEA:EMA(DIFF,M);\nMACD:(DIFF-DEA)*2,COLORSTICK;\nJC:=CROSS(DIFF,DEA);\nSC:=CROSSDOWN(DIFF,DEA);\nN1:=BARSLAST(JC) + 1;\nN2:=BARSLAST(SC) + 1;\nHH:=VALUEWHEN(SC, HHV(H, N1));\nHH2:=VALUEWHEN(SC,REF(HH,1));\nMHD:=VALUEWHEN(SC,HHV(MACD,N1));\nMHD2:=VALUEWHEN(SC,REF(MHD,1));\nLL:=VALUEWHEN(JC, LLV(L, N2));\nLL2:=VALUEWHEN(JC,REF(LL,1));\nMLD:=VALUEWHEN(JC,LLV(MACD,N2));\nMLD2:=VALUEWHEN(JC,REF(MLD,1));\nA:=SC&&HH>HH2&&MHDMLD2;//底背离\nDRAWTEXT(A, DIFF, '顶背离'),COLORGREEN;\nDRAWTEXT(B, DIFF, '底背离'),COLORRED;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "长线,金叉死叉", + "version": 26, + "sdkVersion": "", + "precision": 2, + "drawLineType": 2, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"DIFF\\\",\\n \\\"color_key\\\": \\\"Brush.Gray9\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"DEA\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"tags\":\"长线,金叉死叉\",\"precision\":\"2\",\"draw_line_type\":2}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "2989D8", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "持仓量", + "category": "12", + "categoryName": "量价分析", + "attachedType": "2", + "paramsArray": [], + "lineStyleArray": [ + { + "line_name": "持仓量", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "成交量:V/SHARECOUNTPERUNIT,VOLSTICK;\n持仓量:OPI;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "", + "version": 28, + "sdkVersion": "", + "precision": 2, + "drawLineType": 2, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"持仓量\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"precision\":\"2\",\"draw_line_type\":2}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "98C11", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "RSI", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "2", + "paramsArray": [ + { + "param_name": "P1", + "default": 6, + "low": 2, + "high": 200 + }, + { + "param_name": "P2", + "default": 12, + "low": 2, + "high": 200 + }, + { + "param_name": "P3", + "default": 24, + "low": 2, + "high": 200 + } + ], + "lineStyleArray": [ + { + "line_name": "RSI1", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "RSI2", + "color_key": "Brush.Green6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "RSI3", + "color_key": "Brush.Blue6", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "LC:=REF(CLOSE,1);\nTEMP1:=MAX(CLOSE-LC,0);\nTEMP2:=ABS(CLOSE-LC);\nRSI1:SMA(TEMP1,P1,1)/SMA(TEMP2,P1,1)*100;\nRSI2:SMA(TEMP1,P2,1)/SMA(TEMP2,P2,1)*100;\nRSI3:SMA(TEMP1,P3,1)/SMA(TEMP2,P3,1)*100;\n// 若不想要展示参考线,则把下面语句删除\n参考线1:15,DASHLINE,COLORGRAY,NOTEXT;\n参考线2:85,DASHLINE,COLORGRAY,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,买卖强弱", + "version": 22, + "sdkVersion": "", + "precision": 2, + "drawLineType": 2, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"RSI1\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"RSI2\\\",\\n \\\"color_key\\\": \\\"Brush.Green6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"RSI3\\\",\\n \\\"color_key\\\": \\\"Brush.Blue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"tags\":\"短线,买卖强弱\",\"precision\":\"2\",\"draw_line_type\":2}", + "isUnable": false, + "features": [] + }, + { + "indicatorId": "98C10", + "indicatorType": "SYSTEM", + "businessType": "FUTURES", + "name": "KDJ", + "category": "7", + "categoryName": "超买超卖", + "attachedType": "2", + "paramsArray": [ + { + "param_name": "P1", + "default": 9, + "low": 1, + "high": 100 + }, + { + "param_name": "P2", + "default": 3, + "low": 2, + "high": 40 + }, + { + "param_name": "P3", + "default": 3, + "low": 2, + "high": 40 + } + ], + "lineStyleArray": [ + { + "line_name": "K", + "color_key": "Brush.Yellow6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "D", + "color_key": "Brush.Blue6", + "line_thick": 1, + "is_selected": 1 + }, + { + "line_name": "J", + "color_key": "Brush.Pink8", + "line_thick": 1, + "is_selected": 1 + } + ], + "descripe": "", + "sourceCode": "RSV:=(CLOSE-LLV(LOW,P1))/(HHV(HIGH,P1)-LLV(LOW,P1))*100;\nK:SMA(RSV,P2,1);\nD:SMA(K,P3,1);\nJ:3*K-2*D;\n// 若不想要展示参考线,则把下面语句删除\n参考线1:20,DASHLINE,COLORGRAY,NOTEXT;\n参考线2:80,DASHLINE,COLORGRAY,NOTEXT;", + "sourceCodeLanguage": "MYLANGUAGE", + "sourceCodeEncrypt": false, + "tags": "短线,背离", + "version": 28, + "sdkVersion": "", + "precision": 2, + "drawLineType": 2, + "protocol": "", + "protocolVersion": "", + "extraYaxisValue": "", + "attribute": "{\"line_style\":\"[\\n {\\n \\\"line_name\\\": \\\"K\\\",\\n \\\"color_key\\\": \\\"Brush.Yellow6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"D\\\",\\n \\\"color_key\\\": \\\"Brush.Blue6\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n },\\n {\\n \\\"line_name\\\": \\\"J\\\",\\n \\\"color_key\\\": \\\"Brush.Pink8\\\",\\n \\\"line_thick\\\": 1,\\n \\\"is_selected\\\": 1\\n }\\n]\",\"tags\":\"短线,背离\",\"precision\":\"2\",\"draw_line_type\":2}", + "isUnable": false, + "features": [] + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/main/resources/rawfile/debug_yyb_config.json b/future_entry_demo/src/main/resources/rawfile/debug_yyb_config.json new file mode 100644 index 0000000..83bf902 --- /dev/null +++ b/future_entry_demo/src/main/resources/rawfile/debug_yyb_config.json @@ -0,0 +1,130 @@ +{ + "yybList": [ + { + "qsName": "银河仿真", + "qsType": "CTP", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6714", + "testIp": "114.255.171.180", + "testPort": 5050 + }, + { + "qsName": "国君仿真", + "qsType": "CTP", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6686", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "simnow", + "qsType": "CTP", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6680", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "金信", + "qsType": "JSD", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6701", + "testIp": "172.19.80.16", + "testPort": 5050 + }, + { + "qsName": "simnow(盘后)", + "qsType": "CTP", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6683", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "mock", + "qsType": "CTP", + "counterDes": "mockCTP", + "qsId": "777", + "wtId": "7777", + "testIp": "172.19.80.16", + "testPort": 5060 + }, + { + "qsName": "华创仿真", + "qsType": "飞创", + "counterDes": "仿真柜台", + "qsId": "755", + "wtId": "7244", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "新纪元仿真", + "qsType": "飞创", + "counterDes": "仿真柜台", + "qsId": "755", + "wtId": "7245", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "中原仿真", + "qsType": "UFX", + "counterDes": "仿真柜台", + "qsId": "755", + "wtId": "7238", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "港信", + "qsType": "UFX", + "counterDes": "仿真柜台", + "qsId": "755", + "wtId": "7300", + "testIp": "172.19.80.15", + "testPort": 5050 + }, + { + "qsName": "金信仿真", + "qsType": "JSD", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6701", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "津投", + "qsType": "CTP", + "counterDes": "津投测试柜台", + "qsId": "607", + "wtId": "7473", + "testIp": "221.239.34.149", + "testPort": 5050 + }, + { + "qsName": "中航", + "qsType": "FEMAS", + "counterDes": "测试柜台", + "qsId": "6000", + "wtId": "6704", + "testIp": "172.19.82.54", + "testPort": 5050 + }, + { + "qsName": "同花顺moni", + "qsType": "CTP", + "counterDes": "同花顺moni-online", + "qsId": "6408", + "wtId": "6916", + "testIp": "122.112.150.61", + "testPort": 5051 + } + ] +} diff --git a/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Medium.otf b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Medium.otf new file mode 100644 index 0000000..967cf6f Binary files /dev/null and b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Medium.otf differ diff --git a/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Regular.otf b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Regular.otf new file mode 100644 index 0000000..07db6b8 Binary files /dev/null and b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_Regular.otf differ diff --git a/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_bold.otf b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_bold.otf new file mode 100644 index 0000000..09343d2 Binary files /dev/null and b/future_entry_demo/src/main/resources/rawfile/fonts/THSJinRongTi_bold.otf differ diff --git a/future_entry_demo/src/main/resources/zh_CN/element/string.json b/future_entry_demo/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..0fe92de --- /dev/null +++ b/future_entry_demo/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "同花顺期货通" + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/test/Ability.test.ets b/future_entry_demo/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..8aa3749 --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import hilog from '@ohos.hilog'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/test/List.test.ets b/future_entry_demo/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/testability/HqTestReqClient.ets b/future_entry_demo/src/ohosTest/ets/testability/HqTestReqClient.ets new file mode 100644 index 0000000..34bf5b3 --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/testability/HqTestReqClient.ets @@ -0,0 +1,52 @@ +import { ITCPClient, RequestEncrypt, RequestHelper, RequestQueueManagement, StuffBaseStruct} from "@kernel/lib_communication" +import { HXRequestParams } from '@kernel/lib_communication/src/main/ets/components/communication/request/HXRequestParams'; + +/** + * 行情请求测试类,有以下用途 + * 1.可以用于快速测试自己的行情请求是否正常收发。只需要修改request()方法中的参数即可 + * 2.提供连接库使用的例子,如认证,数据转化等。 + */ +export class HqTestReqClient implements ITCPClient { + + receive(stuffbase : StuffBaseStruct) { + console.log("connection received") + } + + /** + * 开启初始化行情认证 + */ + auth() { + + } + + /** + * 发送一个普通的行情请求 + * 注意,目前由于认证还未接入主流程,建议先触发一次认证再请求。 + */ + request() { + RequestHelper.build().base(2203, 1282, this, "sortid=10\n" + + "sortorder=0\nmarketId=1\nrowcount=100\nstartrow=0").request(); + } + + stopRequest() { + RequestQueueManagement.getInstance().removeByClient(this) + RequestHelper.getHqManager().removeRequestStructBuff(2203, 1282, RequestQueueManagement.getInstance().getInstanceId(this)) + } + + /** + * 对请求进行加密操作 + */ + encryptData(dataBuff : Uint8Array, sessionId : string) { + RequestEncrypt.doRequestEncrypt(dataBuff, sessionId); + } + + /** + * 将一个HXRequestParam请求实例转换成miniPackage处理的字节数组 + * @returns + */ + paramToBuff() : Uint8Array { + let param = new HXRequestParams() + return RequestHelper.paramToBuff(param); + } + +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/testability/TestAbility.ets b/future_entry_demo/src/ohosTest/ets/testability/TestAbility.ets new file mode 100644 index 0000000..34e1259 --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/testability/TestAbility.ets @@ -0,0 +1,45 @@ +import UIAbility from '@ohos.app.ability.UIAbility'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import hilog from '@ohos.hilog'; +import testsuite from '../test/List.test'; +import window from '@ohos.window'; +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; + +export default class TestAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); + hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:'+ JSON.stringify(launchParam) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); + + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); + windowStage.loadContent('testability/pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/testability/pages/Index.ets b/future_entry_demo/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000..423b427 --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,17 @@ +@Entry +@Component +struct Index { + @State message: string = 'Hello World'; + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + } +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets b/future_entry_demo/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets new file mode 100644 index 0000000..917d27a --- /dev/null +++ b/future_entry_demo/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets @@ -0,0 +1,47 @@ +import hilog from '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import Want from '@ohos.app.ability.Want'; + +let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator | undefined = undefined +let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs | undefined = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err : Error) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + const bundleName = abilityDelegatorArguments.bundleName; + const testAbilityName = 'TestAbility'; + let lMonitor: AbilityDelegatorRegistry.AbilityMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + const want: Want = { + bundleName: bundleName, + abilityName: testAbilityName + }; + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + abilityDelegator.startAbility(want, (err, data) => { + hilog.info(0x0000, 'testTag', 'startAbility : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'startAbility : data : %{public}s',JSON.stringify(data) ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/module.json5 b/future_entry_demo/src/ohosTest/module.json5 new file mode 100644 index 0000000..d0dc5d5 --- /dev/null +++ b/future_entry_demo/src/ohosTest/module.json5 @@ -0,0 +1,38 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/future_entry_demo/src/ohosTest/resources/base/element/color.json b/future_entry_demo/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/future_entry_demo/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/resources/base/element/string.json b/future_entry_demo/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000..65d8fa5 --- /dev/null +++ b/future_entry_demo/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/future_entry_demo/src/ohosTest/resources/base/media/icon.png b/future_entry_demo/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/future_entry_demo/src/ohosTest/resources/base/media/icon.png differ diff --git a/future_entry_demo/src/ohosTest/resources/base/profile/test_pages.json b/future_entry_demo/src/ohosTest/resources/base/profile/test_pages.json new file mode 100644 index 0000000..b7e7343 --- /dev/null +++ b/future_entry_demo/src/ohosTest/resources/base/profile/test_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "testability/pages/Index" + ] +} diff --git a/future_entry_demo/src/test/List.test.ets b/future_entry_demo/src/test/List.test.ets new file mode 100644 index 0000000..00ebafc --- /dev/null +++ b/future_entry_demo/src/test/List.test.ets @@ -0,0 +1,9 @@ +import tradeSessionScheduleManagerTest from './biz_tradecondition/TradeSessionScheduleManager.test' +import triggerExecutionTimeManagerTest from './biz_tradecondition/TriggerExecutionTimeManager.test' +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest() + tradeSessionScheduleManagerTest() + triggerExecutionTimeManagerTest() +} diff --git a/future_entry_demo/src/test/LocalUnit.test.ets b/future_entry_demo/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..5bdb8d1 --- /dev/null +++ b/future_entry_demo/src/test/LocalUnit.test.ets @@ -0,0 +1,10 @@ +import { describe, it, expect } from '@ohos/hypium' + +export default function localUnitTest() { + describe('localUnitTest', () => { + it('should run basic assertion', 0, () => { + expect('abc').assertContain('b') + expect(1 + 1).assertEqual(2) + }) + }) +} diff --git a/future_entry_demo/src/test/biz_tradecondition/TradeSessionScheduleManager.test.ets b/future_entry_demo/src/test/biz_tradecondition/TradeSessionScheduleManager.test.ets new file mode 100644 index 0000000..88bc137 --- /dev/null +++ b/future_entry_demo/src/test/biz_tradecondition/TradeSessionScheduleManager.test.ets @@ -0,0 +1,279 @@ +import { describe, it, expect } from '@ohos/hypium' +import { + TradeSessionDataProvider, + TradeSessionDateHelper, + TradeSessionScheduleManager, + TradeSessionType +} from 'biz_tradecondition/src/main/ets/manager/schedule/TradeSessionScheduleManager' + +class MockTradeSessionDataProvider implements TradeSessionDataProvider { + currentTimeHHmmss: string = '000000' + currentDateYYYYMMDD: string = '20250101' + dayTradableDates: Set = new Set() + nightTradableDates: Set = new Set() + defaultTradableWhenMissing: boolean = false + + getCurrentServerTimeHHmmss(): string { + return this.currentTimeHHmmss + } + + getCurrentServerDateYYYYMMDD(): string { + return this.currentDateYYYYMMDD + } + + isSessionTradableOnDate(dateYYYYMMDD: string, sessionType: TradeSessionType): boolean { + if (sessionType === TradeSessionType.DAY) { + if (this.dayTradableDates.has(dateYYYYMMDD)) { + return true + } + return this.defaultTradableWhenMissing + } + if (sessionType === TradeSessionType.NIGHT) { + if (this.nightTradableDates.has(dateYYYYMMDD)) { + return true + } + return this.defaultTradableWhenMissing + } + return true + } +} + +function createProvider(): MockTradeSessionDataProvider { + return new MockTradeSessionDataProvider() +} + +function createTradeTimeJson(): string { + return JSON.stringify([ + { begin: '09:00:00', close: '11:30:00', type: '1' }, + { begin: '13:00:00', close: '15:00:00', type: '1' }, + { begin: '21:00:00', close: '02:30:00', type: '2' } + ]) +} + +function createAdjacentWindowJson(): string { + return JSON.stringify([ + { begin: '09:00:00', close: '10:15:00', type: '1' }, + { begin: '10:15:00', close: '11:30:00', type: '1' } + ]) +} + +function createNormalNightTradeTimeJson(): string { + return JSON.stringify([ + { begin: '09:00:00', close: '11:30:00', type: '1' }, + { begin: '13:00:00', close: '15:00:00', type: '1' }, + { begin: '21:00:00', close: '23:00:00', type: '2' } + ]) +} + +function createEveningToMorningJson(): string { + return JSON.stringify([ + { begin: '21:00:00', close: '24:00:00', type: '2' }, + { begin: '00:00:00', close: '02:30:00', type: '2' } + ]) +} + +function createFullEveningToMorningJson(): string { + return JSON.stringify([ + { begin: '09:00:00', close: '11:30:00', type: '1' }, + { begin: '13:00:00', close: '15:00:00', type: '1' }, + { begin: '21:00:00', close: '24:00:00', type: '2' }, + { begin: '00:00:00', close: '02:30:00', type: '2' } + ]) +} + +export default function tradeSessionScheduleManagerTest() { + describe('TradeSessionScheduleManagerTest', () => { + it('should parse date string strictly', 0, () => { + const validDate = TradeSessionDateHelper.parseDateString('20250101') + expect(validDate !== null).assertEqual(true) + expect(validDate?.getFullYear()).assertEqual(2025) + expect((validDate?.getMonth() ?? -1) + 1).assertEqual(1) + expect(validDate?.getDate()).assertEqual(1) + + expect(TradeSessionDateHelper.parseDateString('2025011') === null).assertEqual(true) + expect(TradeSessionDateHelper.parseDateString('2025-01-01') === null).assertEqual(true) + expect(TradeSessionDateHelper.parseDateString('20250231') === null).assertEqual(true) + expect(TradeSessionDateHelper.parseDateString('20251301') === null).assertEqual(true) + }) + + it('should return false when trade time json or current time is invalid', 0, () => { + const provider = createProvider() + const invalidJson = 'invalid-json' + + expect(TradeSessionScheduleManager.isInTradeTime('', createTradeTimeJson(), provider, false)).assertEqual(false) + expect(TradeSessionScheduleManager.isInTradeTime('09:30:00', '', provider, false)).assertEqual(false) + expect(TradeSessionScheduleManager.isInTradeTime('09:30:00', invalidJson, provider, false)).assertEqual(false) + }) + + it('should match day session and ignore trade day when disabled like iOS picker warning', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.dayTradableDates = new Set() + provider.nightTradableDates = new Set() + + const result = TradeSessionScheduleManager.isInTradeTime('09:30:00', createTradeTimeJson(), provider, false) + expect(result).assertEqual(true) + }) + + it('should return false when current time is outside all windows', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + + const result = TradeSessionScheduleManager.isInTradeTime('16:00:00', createTradeTimeJson(), provider, false) + expect(result).assertEqual(false) + }) + + it('should reject day session when current date is not tradable', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.isInTradeTime('09:30:00', createTradeTimeJson(), provider, true) + expect(result).assertEqual(false) + }) + + it('should allow night session when current date night market is tradable', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.isInTradeTime('21:30:00', createNormalNightTradeTimeJson(), provider, true) + expect(result).assertEqual(true) + }) + + it('should not match early-morning time in single cross-midnight window', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250927' + provider.dayTradableDates = new Set(['20250927']) + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.isInTradeTime('01:30:00', createTradeTimeJson(), provider, true) + expect(result).assertEqual(false) + }) + + it('should allow early-morning split night session by previous trade date', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250927' + provider.dayTradableDates = new Set(['20250927']) + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.isInTradeTime('01:30:00', createFullEveningToMorningJson(), provider, true) + expect(result).assertEqual(true) + }) + + it('should reject early-morning night session when previous trade date is not tradable', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250927' + provider.dayTradableDates = new Set(['20250927']) + + const result = TradeSessionScheduleManager.isInTradeTime('01:30:00', createTradeTimeJson(), provider, true) + expect(result).assertEqual(false) + }) + + it('should align iOS and not support single cross-midnight window matching', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + + const beforeClose = TradeSessionScheduleManager.isInTradeTime('02:29:59', createTradeTimeJson(), provider, false) + const afterClose = TradeSessionScheduleManager.isInTradeTime('02:30:00', createTradeTimeJson(), provider, false) + + expect(beforeClose).assertEqual(false) + expect(afterClose).assertEqual(false) + }) + + it('should match early-morning split night session', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + + const beforeClose = TradeSessionScheduleManager.isInTradeTime('02:29:59', createEveningToMorningJson(), provider, false) + const afterClose = TradeSessionScheduleManager.isInTradeTime('02:30:00', createEveningToMorningJson(), provider, false) + + expect(beforeClose).assertEqual(true) + expect(afterClose).assertEqual(false) + }) + + it('should match begin time and reject close time', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + + const beginResult = TradeSessionScheduleManager.isInTradeTime('09:00:00', createTradeTimeJson(), provider, false) + const closeResult = TradeSessionScheduleManager.isInTradeTime('11:30:00', createTradeTimeJson(), provider, false) + + expect(beginResult).assertEqual(true) + expect(closeResult).assertEqual(false) + }) + + it('should parse invalid appointed time as zero like iOS integerValue', 0, () => { + const provider = createProvider() + const tradeTimeJson = JSON.stringify([ + { begin: '', close: '00:00:01', type: '1' } + ]) + + const result = TradeSessionScheduleManager.isInTradeTime('', tradeTimeJson, provider, false) + expect(result).assertEqual(true) + }) + + it('should return null for invalid next open time input', 0, () => { + const provider = createProvider() + + const emptyResult = TradeSessionScheduleManager.getNextOpenTimeHHmm('', provider) + const invalidResult = TradeSessionScheduleManager.getNextOpenTimeHHmm('invalid-json', provider) + + expect(emptyResult === null).assertEqual(true) + expect(invalidResult === null).assertEqual(true) + }) + + it('should prefer first day session when day session is not tradable', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.currentTimeHHmmss = '08:00:00' + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.getNextOpenTimeHHmm(createTradeTimeJson(), provider) + expect(result).assertEqual('09:00') + }) + + it('should return nearest future window when current time is before next open', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.currentTimeHHmmss = '12:00:00' + provider.dayTradableDates = new Set(['20250926']) + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.getNextOpenTimeHHmm(createTradeTimeJson(), provider) + expect(result).assertEqual('13:00') + }) + + it('should fallback to first day session when next window is night but night is not tradable', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.currentTimeHHmmss = '15:30:00' + provider.dayTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.getNextOpenTimeHHmm(createTradeTimeJson(), provider) + expect(result).assertEqual('09:00') + }) + + it('should merge adjacent windows when computing next open time', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.currentTimeHHmmss = '10:00:00' + provider.dayTradableDates = new Set(['20250926']) + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.getNextOpenTimeHHmm(createAdjacentWindowJson(), provider) + expect(result).assertEqual('09:00') + }) + + it('should merge evening and morning windows across midnight boundary', 0, () => { + const provider = createProvider() + provider.currentDateYYYYMMDD = '20250926' + provider.currentTimeHHmmss = '21:30:00' + provider.dayTradableDates = new Set(['20250926']) + provider.nightTradableDates = new Set(['20250926']) + + const result = TradeSessionScheduleManager.getNextOpenTimeHHmm(createEveningToMorningJson(), provider) + expect(result).assertEqual('21:00') + }) + }) +} diff --git a/future_entry_demo/src/test/biz_tradecondition/TriggerExecutionTimeManager.test.ets b/future_entry_demo/src/test/biz_tradecondition/TriggerExecutionTimeManager.test.ets new file mode 100644 index 0000000..37c0009 --- /dev/null +++ b/future_entry_demo/src/test/biz_tradecondition/TriggerExecutionTimeManager.test.ets @@ -0,0 +1,342 @@ +import { describe, it, expect } from '@ohos/hypium' +import { + TriggerExecutionTimeManager, + TriggerTimeResolverProvider +} from 'biz_tradecondition/src/main/ets/manager/trigger/TriggerExecutionTimeManager' +import { TradeSessionType } from 'biz_tradecondition/src/main/ets/manager/schedule/TradeSessionScheduleManager' + +class TriggerExecutionExpectation { + currentDate: string = '' + currentTime: string = '' + triggerTime: string = '' + expectedTimestamp: string | null = null + + constructor(currentDate: string, currentTime: string, triggerTime: string, expectedTimestamp: string | null) { + this.currentDate = currentDate + this.currentTime = currentTime + this.triggerTime = triggerTime + this.expectedTimestamp = expectedTimestamp + } +} + +class MockTriggerTimeResolverProvider implements TriggerTimeResolverProvider { + currentTimeHHmmss: string = '000000' + currentDateYYYYMMDD: string = '20250101' + dayTradableDates: Set = new Set() + nightTradableDates: Set = new Set() + defaultTradableWhenMissing: boolean = false + queryCountByKey: Map = new Map() + + getCurrentServerTimeHHmmss(): string { + return this.currentTimeHHmmss + } + + getCurrentServerDateYYYYMMDD(): string { + return this.currentDateYYYYMMDD + } + + isSessionTradableOnDate(dateYYYYMMDD: string, sessionType: TradeSessionType): boolean { + const queryKey = `${dateYYYYMMDD}_${sessionType}` + const currentCount = this.queryCountByKey.get(queryKey) ?? 0 + this.queryCountByKey.set(queryKey, currentCount + 1) + + if (sessionType === TradeSessionType.DAY) { + if (this.dayTradableDates.has(dateYYYYMMDD)) { + return true + } + return this.defaultTradableWhenMissing + } + if (sessionType === TradeSessionType.NIGHT) { + if (this.nightTradableDates.has(dateYYYYMMDD)) { + return true + } + return this.defaultTradableWhenMissing + } + return true + } + + setCurrentDateTime(currentDate: string, currentTime: string): void { + this.currentDateYYYYMMDD = currentDate + this.currentTimeHHmmss = currentTime + } + + getTotalQueryCount(sessionType: TradeSessionType): number { + let totalCount = 0 + this.queryCountByKey.forEach((count: number, key: string) => { + if (key.endsWith(`_${sessionType}`)) { + totalCount += count + } + }) + return totalCount + } + + getQueryCount(dateYYYYMMDD: string, sessionType: TradeSessionType): number { + return this.queryCountByKey.get(`${dateYYYYMMDD}_${sessionType}`) ?? 0 + } +} + +function createTriggerProvider(): MockTriggerTimeResolverProvider { + const provider = new MockTriggerTimeResolverProvider() + + provider.dayTradableDates = new Set([ + '20250401', '20250402', '20250403', '20250407', '20250408', '20250409', '20250410', '20250411', + '20250414', '20250415', '20250416', '20250418', '20250421', '20250422', '20250423', '20250424', + '20250425', '20250428', '20250429', '20250430', + '20250901', '20250902', '20250903', '20250904', '20250905', '20250908', '20250909', '20250910', + '20250911', '20250912', '20250915', '20250916', '20250917', '20250918', '20250919', '20250922', + '20250923', '20250924', '20250925', '20250926', '20250929', '20250930', + '20251009', '20251010', '20251013', '20251014', '20251015', '20251016', '20251017', '20251020', + '20251021', '20251022', '20251023', '20251024', '20251027', '20251028', '20251029', '20251030', + '20251031' + ]) + + provider.nightTradableDates = new Set([ + '20250401', '20250402', '20250407', '20250408', '20250409', '20250410', '20250411', '20250414', + '20250415', '20250418', '20250421', '20250422', '20250423', '20250424', '20250425', '20250428', + '20250429', + '20250901', '20250902', '20250903', '20250904', '20250905', '20250908', '20250909', '20250910', + '20250911', '20250912', '20250915', '20250916', '20250917', '20250918', '20250919', '20250922', + '20250923', '20250924', '20250925', '20250926', '20250929', + '20251009', '20251010', '20251013', '20251014', '20251015', '20251016', '20251017', '20251020', + '20251021', '20251022', '20251023', '20251024', '20251027', '20251028', '20251029', '20251030', + '20251031' + ]) + return provider +} + +function assertExecutionCase(testCase: TriggerExecutionExpectation, provider: MockTriggerTimeResolverProvider): void { + provider.setCurrentDateTime(testCase.currentDate, testCase.currentTime) + const actual = TriggerExecutionTimeManager.resolveExecutionTimestamp(testCase.triggerTime) + expect(actual).assertEqual(testCase.expectedTimestamp) +} + +function assertExecutionCases(testCases: TriggerExecutionExpectation[], provider: MockTriggerTimeResolverProvider): void { + for (const testCase of testCases) { + assertExecutionCase(testCase, provider) + } +} + +export default function triggerExecutionTimeManagerTest() { + describe('TriggerExecutionTimeManagerTest', () => { + it('should normalize supported time formats and reject invalid input', 0, () => { + const provider = createTriggerProvider() + provider.setCurrentDateTime('20250926', '08:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual('20250926093000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30')).assertEqual('20250926093000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('0930')).assertEqual('20250926093000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('093015')).assertEqual('20250926093015') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('15:15')).assertEqual('20250926151500') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('2100')).assertEqual('20250926210000') + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('25:00:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('12:60:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('12:30:60')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('24:00:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('-01:00:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('invalid')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('')).assertEqual(null) + }) + + it('should match friday trading day detailed cases', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + assertExecutionCases([ + new TriggerExecutionExpectation('20250926', '00:00:00', '00:30:00', '20250926003000'), + new TriggerExecutionExpectation('20250926', '00:30:00', '01:00:00', '20250926010000'), + new TriggerExecutionExpectation('20250926', '01:00:00', '00:30:00', '20250927003000'), + new TriggerExecutionExpectation('20250926', '02:00:00', '01:30:00', '20250927013000'), + new TriggerExecutionExpectation('20250926', '02:29:59', '02:30:00', '20250926023000'), + new TriggerExecutionExpectation('20250926', '02:30:00', '03:00:00', '20250926030000'), + new TriggerExecutionExpectation('20250926', '08:00:00', '09:30:00', '20250926093000'), + new TriggerExecutionExpectation('20250926', '09:30:00', '10:00:00', '20250926100000'), + new TriggerExecutionExpectation('20250926', '11:30:00', '13:30:00', '20250926133000'), + new TriggerExecutionExpectation('20250926', '13:30:00', '15:00:00', '20250926150000'), + new TriggerExecutionExpectation('20250926', '15:00:00', '09:30:00', '20250929093000'), + new TriggerExecutionExpectation('20250926', '15:15:00', '09:30:00', '20250929093000'), + new TriggerExecutionExpectation('20250926', '16:00:00', '21:00:00', '20250926210000'), + new TriggerExecutionExpectation('20250926', '20:00:00', '21:00:00', '20250926210000'), + new TriggerExecutionExpectation('20250926', '21:00:00', '22:00:00', '20250926220000'), + new TriggerExecutionExpectation('20250926', '22:00:00', '21:30:00', '20250929213000'), + new TriggerExecutionExpectation('20250926', '23:30:00', '22:00:00', '20250929220000'), + new TriggerExecutionExpectation('20250926', '23:59:59', '21:00:00', '20250929210000') + ], provider) + }) + + it('should match saturday non-trading day detailed cases', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + assertExecutionCases([ + new TriggerExecutionExpectation('20250927', '00:00:00', '00:30:00', '20250927003000'), + new TriggerExecutionExpectation('20250927', '00:30:00', '01:00:00', '20250927010000'), + new TriggerExecutionExpectation('20250927', '01:00:00', '01:30:00', '20250927013000'), + new TriggerExecutionExpectation('20250927', '01:30:00', '02:00:00', '20250927020000'), + new TriggerExecutionExpectation('20250927', '02:00:00', '02:30:00', '20250927023000'), + new TriggerExecutionExpectation('20250927', '02:30:00', '00:30:00', '20250930003000'), + new TriggerExecutionExpectation('20250927', '09:00:00', '09:30:00', '20250929093000'), + new TriggerExecutionExpectation('20250927', '13:30:00', '15:00:00', '20250929150000'), + new TriggerExecutionExpectation('20250927', '15:15:00', '09:30:00', '20250929093000'), + new TriggerExecutionExpectation('20250927', '16:00:00', '21:00:00', '20250929210000'), + new TriggerExecutionExpectation('20250927', '21:00:00', '22:00:00', '20250929220000'), + new TriggerExecutionExpectation('20250927', '23:00:00', '21:30:00', '20250929213000') + ], provider) + }) + + it('should match national holiday detailed cases', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + assertExecutionCases([ + new TriggerExecutionExpectation('20251001', '00:00:00', '00:30:00', '20251010003000'), + new TriggerExecutionExpectation('20251001', '00:30:00', '01:00:00', '20251010010000'), + new TriggerExecutionExpectation('20251001', '01:00:00', '01:30:00', '20251010013000'), + new TriggerExecutionExpectation('20251001', '01:30:00', '02:00:00', '20251010020000'), + new TriggerExecutionExpectation('20251001', '02:00:00', '02:30:00', '20251010023000'), + new TriggerExecutionExpectation('20251001', '02:30:00', '00:30:00', '20251010003000'), + new TriggerExecutionExpectation('20251001', '08:00:00', '09:30:00', '20251009093000'), + new TriggerExecutionExpectation('20251001', '09:30:00', '13:30:00', '20251009133000'), + new TriggerExecutionExpectation('20251001', '13:30:00', '15:00:00', '20251009150000'), + new TriggerExecutionExpectation('20251001', '15:15:00', '09:30:00', '20251009093000'), + new TriggerExecutionExpectation('20251001', '16:00:00', '21:00:00', '20251009210000'), + new TriggerExecutionExpectation('20251001', '21:00:00', '22:00:00', '20251009220000'), + new TriggerExecutionExpectation('20251001', '22:00:00', '21:30:00', '20251009213000'), + new TriggerExecutionExpectation('20251001', '23:30:00', '21:00:00', '20251009210000') + ], provider) + }) + + it('should match boundary and common trading time cases', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + assertExecutionCases([ + new TriggerExecutionExpectation('20250926', '02:29:59', '02:30:00', '20250926023000'), + new TriggerExecutionExpectation('20250926', '02:30:00', '02:30:01', '20250926023001'), + new TriggerExecutionExpectation('20250926', '02:30:01', '02:30:00', '20250927023000'), + new TriggerExecutionExpectation('20250927', '02:30:00', '02:30:01', '20250929023001'), + new TriggerExecutionExpectation('20250926', '15:14:59', '15:15:00', '20250926151500'), + new TriggerExecutionExpectation('20250926', '15:15:00', '15:15:01', '20250926151501'), + new TriggerExecutionExpectation('20250926', '15:15:01', '15:15:00', '20250929151500'), + new TriggerExecutionExpectation('20250926', '00:00:00', '23:59:59', '20250926235959'), + new TriggerExecutionExpectation('20250926', '23:59:59', '00:00:00', '20250927000000'), + new TriggerExecutionExpectation('20250927', '23:59:59', '00:00:00', '20250930000000'), + new TriggerExecutionExpectation('20250926', '02:29:59', '15:15:00', '20250926151500'), + new TriggerExecutionExpectation('20250926', '15:14:59', '21:00:00', '20250926210000') + ], provider) + + provider.setCurrentDateTime('20250926', '08:00:00') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:00:00')).assertEqual('20250926090000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('10:15:00')).assertEqual('20250926101500') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('10:30:00')).assertEqual('20250926103000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('11:30:00')).assertEqual('20250926113000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('14:10:00')).assertEqual('20250926141000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('15:00:00')).assertEqual('20250926150000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('21:00:00')).assertEqual('20250926210000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('22:00:00')).assertEqual('20250926220000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('23:00:00')).assertEqual('20250926230000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('00:00:00')).assertEqual('20250927000000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('01:00:00')).assertEqual('20250927010000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('02:30:00')).assertEqual('20250927023000') + }) + + it('should match cross-month and april holiday scenarios', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + assertExecutionCases([ + new TriggerExecutionExpectation('20250930', '20:00:00', '01:00:00', '20251010010000'), + new TriggerExecutionExpectation('20250930', '20:00:00', '21:00:00', '20251009210000'), + new TriggerExecutionExpectation('20250930', '23:00:00', '22:00:00', '20251009220000'), + new TriggerExecutionExpectation('20250930', '15:30:00', '09:30:00', '20251009093000'), + new TriggerExecutionExpectation('20251008', '21:00:00', '21:30:00', '20251009213000'), + new TriggerExecutionExpectation('20251008', '09:00:00', '09:30:00', '20251009093000'), + new TriggerExecutionExpectation('20250403', '00:15:00', '00:30:00', '20250403003000'), + new TriggerExecutionExpectation('20250403', '01:30:00', '01:00:00', '20250408010000'), + new TriggerExecutionExpectation('20250403', '14:00:00', '09:30:00', '20250407093000'), + new TriggerExecutionExpectation('20250403', '16:00:00', '21:00:00', '20250407210000'), + new TriggerExecutionExpectation('20250404', '00:15:00', '00:30:00', '20250408003000'), + new TriggerExecutionExpectation('20250404', '09:00:00', '09:30:00', '20250407093000'), + new TriggerExecutionExpectation('20250404', '16:00:00', '21:00:00', '20250407210000'), + new TriggerExecutionExpectation('20250417', '00:15:00', '00:30:00', '20250419003000'), + new TriggerExecutionExpectation('20250417', '09:00:00', '09:30:00', '20250418093000'), + new TriggerExecutionExpectation('20250417', '16:00:00', '21:00:00', '20250418210000'), + new TriggerExecutionExpectation('20250418', '14:00:00', '09:30:00', '20250421093000'), + new TriggerExecutionExpectation('20250418', '22:00:00', '21:00:00', '20250421210000') + ], provider) + }) + + it('should return null when provider current date or time is invalid', 0, () => { + const provider = createTriggerProvider() + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + provider.setCurrentDateTime('2025-09-26', '08:00:00') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual(null) + + provider.setCurrentDateTime('20250926', '08:00') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual('20250926093000') + + provider.setCurrentDateTime('20250926', 'invalid') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual(null) + }) + + it('should return null for day session when day tradable dates are empty only', 0, () => { + const provider = createTriggerProvider() + provider.dayTradableDates = new Set() + provider.setCurrentDateTime('20250926', '08:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('21:00:00')).assertEqual('20250926210000') + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('00:30:00')).assertEqual('20250927003000') + }) + + it('should return null for night session when night tradable dates are empty only', 0, () => { + const provider = createTriggerProvider() + provider.nightTradableDates = new Set() + provider.setCurrentDateTime('20250926', '08:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('21:00:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('00:30:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual('20250926093000') + }) + + it('should stop searching after max search days when no tradable date exists', 0, () => { + const provider = createTriggerProvider() + provider.dayTradableDates = new Set() + provider.nightTradableDates = new Set() + provider.defaultTradableWhenMissing = false + provider.setCurrentDateTime('20251001', '09:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('21:00:00')).assertEqual(null) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('00:30:00')).assertEqual(null) + expect(provider.getTotalQueryCount(TradeSessionType.DAY)).assertEqual(90) + expect(provider.getTotalQueryCount(TradeSessionType.NIGHT)).assertEqual(180) + }) + + it('should cache repeated tradable checks within one resolution call', 0, () => { + const provider = createTriggerProvider() + provider.setCurrentDateTime('20251001', '00:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('00:30:00')).assertEqual('20251010003000') + expect(provider.getQueryCount('20251008', TradeSessionType.NIGHT)).assertEqual(1) + expect(provider.getQueryCount('20251009', TradeSessionType.NIGHT)).assertEqual(1) + }) + + it('should reset test provider to default provider safely', 0, () => { + const provider = createTriggerProvider() + provider.setCurrentDateTime('20250926', '08:00:00') + TriggerExecutionTimeManager.setDataProviderForTest(provider) + expect(TriggerExecutionTimeManager.resolveExecutionTimestamp('09:30:00')).assertEqual('20250926093000') + + TriggerExecutionTimeManager.setDataProviderForTest(null) + const result = TriggerExecutionTimeManager.resolveExecutionTimestamp('invalid') + expect(result).assertEqual(null) + }) + }) +}