From 8410ac66d8821061bc40c7782ed331dc5a49f07b Mon Sep 17 00:00:00 2001 From: ZOB2077 <56506480+ZOB2077@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:42:00 +0800 Subject: [PATCH 1/3] Professionalize project and sanitize demo data --- .github/ISSUE_TEMPLATE/bug_report.yml | 41 +++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 24 +++ .github/pull_request_template.md | 14 ++ .github/workflows/ci.yml | 32 ++++ CHANGELOG.md | 25 +++ CODE_OF_CONDUCT.md | 21 +++ CONTRIBUTING.md | 38 +++++ Gridnote.xcodeproj/project.pbxproj | 4 +- Gridnote/Office/OfficeGrid.swift | 131 ++++++++------- Gridnote/Settings/SettingsView.swift | 2 +- GridnoteTests/Office/OfficeGridTests.swift | 16 +- .../Release/ReleaseFixtureTests.swift | 2 +- KNOWN_LIMITATIONS.md | 4 +- PRODUCT_SPEC.md | 67 ++++---- README.md | 153 ++++++++++-------- RELEASE_NOTES.md | 10 +- RELEASE_READINESS.md | 14 +- SECURITY.md | 19 +++ SUPPORT.md | 15 ++ TEST_PLAN.md | 17 +- 21 files changed, 460 insertions(+), 197 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 SUPPORT.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..798a648 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: Bug report +description: Report a reproducible Gridnote defect. +title: "[Bug]: " +labels: [bug] +body: + - type: markdown + attributes: + value: "Do not include book contents, private files, access bookmarks, or credentials." + - type: input + id: version + attributes: + label: Version or commit + placeholder: "v0.1.0 or c89333f" + validations: + required: true + - type: input + id: macos + attributes: + label: macOS version and hardware + placeholder: "macOS 27 beta, Apple Silicon" + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Import a sample TXT..." + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0e3d2c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/ZOB2077/Gridnote/security/advisories/new + about: Report vulnerabilities privately under the security policy. + - name: Commercial licensing + url: https://github.com/ZOB2077 + about: Contact the copyright holder for commercial licensing. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..5864c13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: Feature request +description: Suggest a focused improvement to Gridnote. +title: "[Feature]: " +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem to solve + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + validations: + required: true + - type: textarea + id: boundaries + attributes: + label: Scope and privacy impact + description: "Explain whether this affects local-only behavior, supported formats, or office presentation." + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ea77b6a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Summary + +Describe the user-visible change and why it is needed. + +## Validation + +- [ ] I ran the relevant unit tests. +- [ ] I performed manual verification where UI behavior changed. +- [ ] I updated documentation and release notes when needed. + +## Scope And Privacy + +- [ ] This change adds no network, analytics, DRM, private API, or office-vendor trademark dependency. +- [ ] This pull request does not contain private books, credentials, or generated build artifacts. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3f8c37a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: macOS CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + unit-tests: + name: Build and unit test + runs-on: macos-26 + timeout-minutes: 20 + steps: + - name: Check out source + uses: actions/checkout@v4 + - name: Show Xcode version + run: xcodebuild -version + - name: Build release configuration + run: >- + xcodebuild -project Gridnote.xcodeproj -scheme Gridnote + -configuration Release -destination 'platform=macOS' + CODE_SIGNING_ALLOWED=NO build + - name: Build and run unit tests + run: >- + xcodebuild -project Gridnote.xcodeproj -scheme Gridnote + -destination 'platform=macOS' + -parallel-testing-enabled NO + -skip-testing:GridnoteUITests test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..795ae97 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes are documented in this file. Gridnote follows [Semantic Versioning](https://semver.org/) for public releases where practical. + +## [0.1.0] - 2026-07-13 + +### Added + +- Local TXT and non-DRM EPUB import, parsing, search, bookmarks, and persistent reading locations. +- Original office-style phone-rental data workspace with formula-bar reading presentation. +- Floating reader, Super Stealth mode, configurable appearance, progress indicator, and menu-bar controls. +- Default shortcut profile: `F7` previous, `F8` next, and `F9` show or hide the floating reader. +- Per-book aliases, local workbook titles, dense synthetic office templates, and privacy shielding when the app loses focus. +- Source-available non-commercial license and commercial-licensing path. + +### Changed + +- Office formula-bar navigation and the floating reader now synchronize reading progress in both directions. +- The office workspace uses an original presentation and does not expose book text inside spreadsheet cells. + +### Known Limitations + +- PDF, cloud sync, network features, accounts, DRM handling, XLSX compatibility, notarization, and global shortcuts are not included. + +[0.1.0]: https://github.com/ZOB2077/Gridnote/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..db6067d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,21 @@ +# Code Of Conduct + +## Our Pledge + +We are committed to a respectful, professional, and inclusive community. Harassment, personal attacks, discrimination, threats, doxxing, and deliberately disruptive behavior are not acceptable. + +## Expected Behavior + +- Focus feedback on code, product behavior, and evidence. +- Respect differing experience levels and communicate in good faith. +- Protect private reading material, personal data, and security-sensitive details. + +## Unacceptable Behavior + +- Harassment, hateful conduct, intimidation, or unwanted sexual attention. +- Publishing private information without permission. +- Submitting malicious code, pirated material, DRM-circumvention tools, or content intended to bypass platform protections. + +## Enforcement + +Report conduct concerns privately through the repository owner's GitHub profile: [ZOB2077](https://github.com/ZOB2077). Reports will be reviewed in good faith. Responses may include a warning, removal of content, temporary restriction, or permanent ban. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6ed3461 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing To Gridnote + +Thank you for improving Gridnote. Contributions are reviewed for product fit, privacy impact, maintenance cost, and compatibility with the source-available license. + +## Before You Start + +- Read [PRODUCT_SPEC.md](PRODUCT_SPEC.md), [ARCHITECTURE.md](ARCHITECTURE.md), and [AGENTS.md](AGENTS.md). +- Search existing issues and pull requests before opening a new one. +- Discuss substantial features in an issue before implementation. Keep pull requests narrowly scoped. +- Do not add networking, analytics, DRM handling, private APIs, or office-suite trademark assets. +- Use synthetic fixtures only. Never commit real business data, private books, customer records, or screenshots containing them. + +## Development Setup + +1. Fork and clone the repository. +2. Open `Gridnote.xcodeproj` in Xcode 27 beta. +3. Select the `Gridnote` scheme and run it on macOS. +4. Run the unit-test command from the README before submitting a pull request. + +## Pull Request Expectations + +- Describe the user-visible behavior and technical approach. +- Add or update unit tests for changed business logic. +- Keep parser, persistence, and UI responsibilities separate. +- Update documentation when behavior, shortcuts, support boundaries, or release status change. +- Do not include build products, DerivedData, local test fixtures containing private material, or unrelated formatting churn. + +## Commit Style + +Use concise imperative subjects, such as `Synchronize office and floating reader progress`. Prefer one logical change per commit when practical. + +## Contributor License Grant + +By submitting a contribution, you confirm that you have the right to submit it and agree to the contribution terms in [LICENSE](LICENSE), including the copyright holder's right to use and commercialize accepted contributions as part of Gridnote. + +## Reporting Problems + +Use the bug-report template for reproducible defects. Do not include book contents, personally identifiable information, or private source files in public issues. Security-sensitive reports must follow [SECURITY.md](SECURITY.md). diff --git a/Gridnote.xcodeproj/project.pbxproj b/Gridnote.xcodeproj/project.pbxproj index 88d2e8c..0c06f8e 100644 --- a/Gridnote.xcodeproj/project.pbxproj +++ b/Gridnote.xcodeproj/project.pbxproj @@ -319,8 +319,8 @@ /* Begin XCBuildConfiguration section */ B10000000000000000000001 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; MACOSX_DEPLOYMENT_TARGET = 26.0; SDKROOT = macosx; SWIFT_VERSION = 6.0; }; name = Debug; }; B10000000000000000000002 /* Release */ = {isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; MACOSX_DEPLOYMENT_TARGET = 26.0; SDKROOT = macosx; SWIFT_VERSION = 6.0; }; name = Release; }; - B10000000000000000000003 /* Gridnote Debug */ = {isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_TESTABILITY = YES; EXECUTABLE_NAME = DataHub; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Gridnote/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Debug; }; - B10000000000000000000004 /* Gridnote Release */ = {isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_TESTABILITY = YES; EXECUTABLE_NAME = DataHub; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Gridnote/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Release; }; + B10000000000000000000003 /* Gridnote Debug */ = {isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_TESTABILITY = YES; EXECUTABLE_NAME = DataHub; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Gridnote/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Debug; }; + B10000000000000000000004 /* Gridnote Release */ = {isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_TESTABILITY = YES; EXECUTABLE_NAME = DataHub; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Gridnote/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; name = Release; }; B10000000000000000000005 /* GridnoteTests Debug */ = {isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gridnote.app/Contents/MacOS/DataHub"; }; name = Debug; }; B10000000000000000000006 /* GridnoteTests Release */ = {isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gridnote.app/Contents/MacOS/DataHub"; }; name = Release; }; B10000000000000000000007 /* GridnoteUITests Debug */ = {isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.gridnote.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 6.0; TEST_TARGET_NAME = Gridnote; }; name = Debug; }; diff --git a/Gridnote/Office/OfficeGrid.swift b/Gridnote/Office/OfficeGrid.swift index 7daa680..891fbdd 100644 --- a/Gridnote/Office/OfficeGrid.swift +++ b/Gridnote/Office/OfficeGrid.swift @@ -32,91 +32,92 @@ struct OfficeGridSnapshot: Codable, Equatable { } } + // Every built-in table is deterministic synthetic data and contains no customer or business records. static var defaultOperations: OfficeGridSnapshot { - var snapshot = OfficeGridSnapshot(values: [:]) - let rows = [ - ["统计周期", "一级订单归属", "商户类型", "小程序", "商品简称(机型)", "下单数", "前置通过订单数", "前置通过率", "预授权调用订单数", "预授权签约订单数", "预授权签约率", "全免订单数", "全免率", "进入后置订单数", "可分流订单数", "分流成功订单数", "后置通过订单数", "后置通过率"], - ["2026-07-09", "02_租物", "商户", "优品go", "一加 Ace 6", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "优品go", "华为 Mate 80", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "优品go", "华为 Pura 80 Pro", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "优品go", "红米 K90", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "优品租", "vivo iQOO Z10x", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "租手机青年优品", "OPPO Find X9 Pro", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "租手机青年优品", "OPPO K13 Turbo", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "租手机青年优品", "一加 15", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "商户", "租手机青年优品", "小米17Pro Max", "2", "2", "100.00%", "2", "2", "100.00%", "2", "100.00%", "2", "2", "2", "0", "0.00%"], - ["2026-07-09", "02_租物", "商户", "青友租", "一加 15", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "1", "1", "1", "100.00%"], - ["2026-07-09", "02_租物", "自营", "优品go", "Galaxy S26+", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "HUAWEI Pura X", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "IQOO Z11 Turbo", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "OPPO K12s", "1", "1", "100.00%", "1", "1", "100.00%", "0", "0.00%", "1", "1", "0", "0", "0.00%"], - ["2026-07-09", "02_租物", "自营", "优品go", "OPPO Reno14", "4", "4", "100.00%", "3", "1", "33.33%", "0", "0.00%", "1", "1", "0", "0", "0.00%"], - ["2026-07-09", "02_租物", "自营", "优品go", "OPPO Reno16", "4", "4", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "REDMI K90", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "1", "0", "0", "0", "0.00%"], - ["2026-07-09", "02_租物", "自营", "优品go", "REDMI K90 Pro Max", "1", "1", "100.00%", "1", "1", "100.00%", "1", "100.00%", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "VIVO X200S", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "iQOO 15", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "iQOO 15 Ultra", "1", "1", "100.00%", "1", "0", "0.00%", "0", "0.00%", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "iQOO 15T", "4", "4", "100.00%", "3", "2", "66.67%", "2", "66.67%", "2", "2", "1", "1", "50.00%"], - ["2026-07-09", "02_租物", "自营", "优品go", "iQOO Neo11", "1", "1", "100.00%", "0", "0", "--", "0", "--", "0", "0", "0", "0", "--"], - ["2026-07-09", "02_租物", "自营", "优品go", "vivo S60", "6", "6", "100.00%", "4", "4", "100.00%", "4", "100.00%", "4", "4", "2", "2", "50.00%"] - ] - for (rowIndex, row) in rows.enumerated() { - for (columnIndex, value) in row.enumerated() { - snapshot[OfficeCellCoordinate(row: rowIndex, column: columnIndex)] = value - } + let headers = ["演示日期", "业务单元", "渠道类型", "演示渠道", "设备型号", "申请量", "初审通过", "初审通过率", "签约发起", "签约完成", "签约率", "审核完成", "审核完成率", "进入履约", "可处理", "处理成功", "最终完成", "完成率"] + let rows = (0..<(rowCount - 1)).map { index in + let applications = 18 + index % 13 + let approved = max(1, applications - 2 - index % 4) + let signed = max(1, approved - 1 - index % 3) + let completed = max(0, signed - index % 2) + return [ + String(format: "2026-07-%02d", 1 + index % 7), + "演示业务 \(letter(index % 3))", + index.isMultiple(of: 2) ? "演示直营" : "演示合作", + "模拟渠道 \(letter(index % 4))", + String(format: "测试设备 G-%02d", index + 1), + "\(applications)", "\(approved)", percentage(approved, of: applications), + "\(approved)", "\(signed)", percentage(signed, of: approved), + "\(signed)", "100.00%", "\(signed)", "\(signed)", + "\(completed)", "\(completed)", percentage(completed, of: signed) + ] } - return snapshot + return denseSnapshot(headers: headers, rows: rows) } static func snapshot(for template: OfficeTemplateFamily) -> OfficeGridSnapshot { switch template { - case .operations: - defaultOperations - case .projectTracking: - channelConversion - case .budget: - inventoryFulfillment - case .notes: - deviceLedger + case .operations: defaultOperations + case .projectTracking: channelConversion + case .budget: inventoryFulfillment + case .notes: deviceLedger } } static var channelConversion: OfficeGridSnapshot { - let headers = ["统计周期", "活动名称", "区域", "小程序", "商品简称(机型)", "曝光量", "访问量", "加购量", "下单数", "支付数", "支付转化率", "前置通过", "预授权签约", "签约率", "进入后置", "后置通过", "最终发货", "备注"] - let models = ["iPhone 17", "华为 Mate 80", "vivo X200S", "OPPO Reno16", "一加 15", "小米 17 Pro", "荣耀 Magic8", "iQOO 15", "三星 S26", "REDMI K90", "华为 Pura 80", "OPPO Find X9", "vivo S60", "荣耀 X70", "iPhone 16 Pro", "一加 Ace 6", "小米 Civi 6", "华为 nova 15", "iQOO Neo11", "OPPO K13", "三星 A77", "荣耀 500", "REDMI Note 15", "vivo Y300"] - let regions = ["华东", "华南", "华北", "西南"] - let channels = ["优品go", "租手机青年优品", "青友租", "优品租"] - let rows = models.enumerated().map { index, model in + let headers = ["演示日期", "活动代号", "区域", "演示渠道", "设备型号", "展示量", "访问量", "意向量", "申请量", "完成量", "完成率", "初审通过", "签约完成", "签约率", "进入履约", "履约完成", "最终完成", "备注"] + let rows = (0..<(rowCount - 1)).map { index in let exposure = 260 + index * 37 let visits = 74 + index * 9 let orders = 4 + index % 7 - let paid = max(1, orders - index % 3) - return ["2026-07-13", "暑期换新", regions[index % regions.count], channels[index % channels.count], model, "\(exposure)", "\(visits)", "\(orders + 6)", "\(orders)", "\(paid)", String(format: "%.2f%%", Double(paid) / Double(visits) * 100), "\(max(1, paid - 1))", "\(max(1, paid - 1))", String(format: "%.2f%%", Double(max(1, paid - 1)) / Double(paid) * 100), "\(max(1, paid - 2))", "\(max(0, paid - 2))", "\(max(0, paid - 2))", index.isMultiple(of: 5) ? "重点观察" : "正常"] + let completed = max(1, orders - index % 3) + let approved = max(1, completed - 1) + let fulfilled = max(0, completed - 2) + return [ + "2026-07-01", "演示活动", "区域 \(index % 4 + 1)", + "模拟渠道 \(index % 4 + 1)", String(format: "测试设备 C-%02d", index + 1), + "\(exposure)", "\(visits)", "\(orders + 6)", "\(orders)", "\(completed)", + percentage(completed, of: visits), "\(approved)", "\(approved)", + percentage(approved, of: completed), "\(fulfilled)", "\(fulfilled)", "\(fulfilled)", + index.isMultiple(of: 5) ? "演示关注" : "演示正常" + ] } return denseSnapshot(headers: headers, rows: rows) } static var inventoryFulfillment: OfficeGridSnapshot { - let headers = ["统计日期", "仓库", "城市", "渠道", "商品简称(机型)", "在租台数", "可租库存", "调拨中", "近7日下单", "待审核", "履约中", "待发货", "已签收", "回收中", "签收率", "周转天数", "库存预警", "处理建议"] - let models = ["iPhone 17", "华为 Mate 80", "vivo X200S", "OPPO Reno16", "一加 15", "小米 17 Pro", "荣耀 Magic8", "iQOO 15", "三星 S26", "REDMI K90", "华为 Pura 80", "OPPO Find X9", "vivo S60", "荣耀 X70", "iPhone 16 Pro", "一加 Ace 6", "小米 Civi 6", "华为 nova 15", "iQOO Neo11", "OPPO K13", "三星 A77", "荣耀 500", "REDMI Note 15", "vivo Y300"] - let cities = ["上海", "广州", "北京", "成都", "杭州", "武汉"] - let rows = models.enumerated().map { index, model in - let rentable = 8 + index % 18 + let headers = ["演示日期", "仓库", "区域", "演示渠道", "设备型号", "使用中", "可用库存", "调拨中", "近期申请", "待审核", "处理中", "待交付", "已完成", "回收中", "完成率", "周转天数", "库存提示", "处理建议"] + let rows = (0..<(rowCount - 1)).map { index in + let available = 8 + index % 18 let pending = 1 + index % 5 - let delivering = 2 + index % 6 - let signed = delivering + 4 - return ["2026-07-13", index.isMultiple(of: 2) ? "华东中心仓" : "华南中心仓", cities[index % cities.count], index.isMultiple(of: 3) ? "优品go" : "青年优品", model, "\(42 + index * 3)", "\(rentable)", "\(index % 4)", "\(6 + index % 8)", "\(pending)", "\(delivering)", "\(max(1, pending - 1))", "\(signed)", "\(index % 3)", String(format: "%.2f%%", Double(signed) / Double(signed + pending) * 100), "\(14 + index % 12)", rentable < 12 ? "补货" : "充足", rentable < 12 ? "优先调拨" : "正常跟进"] + let processing = 2 + index % 6 + let completed = processing + 4 + return [ + "2026-07-01", "演示仓 \(index % 2 + 1)", "区域 \(index % 6 + 1)", + "模拟渠道 \(index % 3 + 1)", String(format: "测试设备 I-%02d", index + 1), + "\(42 + index * 3)", "\(available)", "\(index % 4)", "\(6 + index % 8)", + "\(pending)", "\(processing)", "\(max(1, pending - 1))", "\(completed)", "\(index % 3)", + percentage(completed, of: completed + pending), "\(14 + index % 12)", + available < 12 ? "演示补充" : "演示充足", available < 12 ? "模拟调拨" : "常规跟进" + ] } return denseSnapshot(headers: headers, rows: rows) } static var deviceLedger: OfficeGridSnapshot { - let headers = ["登记日期", "资产编号", "区域", "门店", "商品简称(机型)", "设备状态", "租约状态", "本期租金", "押金", "使用时长", "最近巡检", "电池健康", "屏幕状态", "外观等级", "配件齐全", "回收预计", "风险等级", "处理备注"] - let models = ["iPhone 17", "华为 Mate 80", "vivo X200S", "OPPO Reno16", "一加 15", "小米 17 Pro", "荣耀 Magic8", "iQOO 15", "三星 S26", "REDMI K90", "华为 Pura 80", "OPPO Find X9", "vivo S60", "荣耀 X70", "iPhone 16 Pro", "一加 Ace 6", "小米 Civi 6", "华为 nova 15", "iQOO Neo11", "OPPO K13", "三星 A77", "荣耀 500", "REDMI Note 15", "vivo Y300"] - let rows = models.enumerated().map { index, model in + let headers = ["演示日期", "资产编号", "区域", "演示网点", "设备型号", "设备状态", "流程状态", "本期费用", "保证金", "使用时长", "最近检查", "电池状态", "屏幕状态", "外观等级", "配件状态", "预计回收", "风险等级", "处理备注"] + let rows = (0..<(rowCount - 1)).map { index in let days = 18 + index * 3 - return ["2026-07-13", String(format: "SH-%04d", 2100 + index), index.isMultiple(of: 2) ? "华东" : "华南", index.isMultiple(of: 3) ? "直营店" : "合作门店", model, index.isMultiple(of: 6) ? "待检" : "在库", index.isMultiple(of: 4) ? "即将到期" : "履约中", "¥\(199 + index * 30)", "¥\(699 + index * 50)", "\(days) 天", "2026-07-\(String(format: "%02d", 4 + index % 9))", "\(83 + index % 15)%", index.isMultiple(of: 7) ? "轻微划痕" : "良好", index.isMultiple(of: 5) ? "B+" : "A", index.isMultiple(of: 8) ? "缺充电线" : "齐全", "2026-08-\(String(format: "%02d", 5 + index % 20))", index.isMultiple(of: 6) ? "中" : "低", index.isMultiple(of: 6) ? "安排巡检" : "正常"] + return [ + "2026-07-01", String(format: "DEMO-%04d", 1000 + index), "区域 \(index % 4 + 1)", + "演示网点 \(index % 5 + 1)", String(format: "测试设备 L-%02d", index + 1), + index.isMultiple(of: 6) ? "待检" : "可用", index.isMultiple(of: 4) ? "待处理" : "处理中", + "¥\(100 + index * 10)", "¥\(500 + index * 20)", "\(days) 天", + String(format: "2026-07-%02d", 1 + index % 9), "\(83 + index % 15)%", + index.isMultiple(of: 7) ? "演示瑕疵" : "良好", index.isMultiple(of: 5) ? "B" : "A", + index.isMultiple(of: 8) ? "待补充" : "齐全", String(format: "2026-08-%02d", 1 + index % 20), + index.isMultiple(of: 6) ? "演示中" : "演示低", index.isMultiple(of: 6) ? "模拟检查" : "演示正常" + ] } return denseSnapshot(headers: headers, rows: rows) } @@ -131,7 +132,15 @@ struct OfficeGridSnapshot: Codable, Equatable { return snapshot } - /// Original order-detail templates are replaced by the reference-style operations sheet. + private static func percentage(_ value: Int, of total: Int) -> String { + guard total > 0 else { return "0.00%" } + return String(format: "%.2f%%", Double(value) / Double(total) * 100) + } + + private static func letter(_ offset: Int) -> Character { + Character(UnicodeScalar(65 + offset)!) + } + var isLegacyOperationsTemplate: Bool { let firstHeader = self[OfficeCellCoordinate(row: 0, column: 0)] return firstHeader == "Date" || firstHeader == "日期" diff --git a/Gridnote/Settings/SettingsView.swift b/Gridnote/Settings/SettingsView.swift index 92cada8..ca903bc 100644 --- a/Gridnote/Settings/SettingsView.swift +++ b/Gridnote/Settings/SettingsView.swift @@ -106,7 +106,7 @@ private struct SettingsContent: View { Text(template.disguiseTitle).tag(template) } } - Text("切换后会将当前伪装表格替换为对应的手机租赁行业数据;阅读进度和书签不受影响。") + Text("切换后会将当前表格替换为纯虚构的演示数据;阅读进度和书签不受影响。") .font(.caption) .foregroundStyle(.secondary) Toggle("自动遮蔽公式栏正文", isOn: $viewModel.autoMaskFormulaBar) diff --git a/GridnoteTests/Office/OfficeGridTests.swift b/GridnoteTests/Office/OfficeGridTests.swift index d5ea034..7a70ab0 100644 --- a/GridnoteTests/Office/OfficeGridTests.swift +++ b/GridnoteTests/Office/OfficeGridTests.swift @@ -13,7 +13,7 @@ final class OfficeGridTests: XCTestCase { XCTAssertEqual(try JSONDecoder().decode(OfficeGridSnapshot.self, from: data), snapshot) } - func testDefaultOperationsFillsEveryVisibleCellWithRentalData() { + func testDefaultOperationsFillsEveryVisibleCellWithSyntheticData() { let snapshot = OfficeGridSnapshot.defaultOperations for row in 0..