Compare commits

..

No commits in common. "may2026ui" and "master" have entirely different histories.

59 changed files with 41 additions and 2202 deletions

View File

@ -1,14 +0,0 @@
# 分類調整備忘(已整併)
此文件已整併到通用文件,避免後續骨架調整分散在多份 memo
1. 操作步驟請看:
[operations-memo.md](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/operations-memo.md)
2. 已完成變更請看:
[change-log.md](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/change-log.md)
3. 分類相關 SQL
- [category-restructure-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-restructure-20260503.sql)
- [category-delete-legacy-software-children-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-delete-legacy-software-children-20260503.sql)
- [category-localization-backfill-tw-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-localization-backfill-tw-20260503.sql)

View File

@ -1,142 +0,0 @@
# 變更紀錄(通用)
## 用途
本文件用於記錄「已實作的工程變更」包含程式、SQL、設定與驗證結果。
每次調整請以日期區段追加,避免只留在對話訊息中。
---
## 2026-05-03前台/媒體改造規劃文件建立
### 內容
1. 新增前台翻新與媒體儲存架構規劃文件:
[frontend-media-storage-plan.md](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/frontend-media-storage-plan.md)
2. 規劃重點已明確寫入:
- 新 Theme`Kneo`)方向
- 私有 S3/MinIO + media proxy
- 分階段導入前台與媒體基礎、PictureService 套用、最終遷移)
- 部署檢查清單與風險控制
---
## 2026-05-03本機啟動連接埠修正
### 內容
1. 修正啟動設定避免自動開瀏覽器連到錯誤埠:
[launchSettings.json](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Properties/launchSettings.json)
2. 調整結果:
- `applicationUrl``https://localhost:5001;http://localhost:5000`
- 改為 `http://localhost:5000`
---
## 2026-05-03 ~ 2026-05-05分類結構調整 SQL 與資料落地
### 新增檔案
1. [category-restructure-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-restructure-20260503.sql)
2. [category-delete-legacy-software-children-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-delete-legacy-software-children-20260503.sql)
3. [category-localization-backfill-tw-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-localization-backfill-tw-20260503.sql)
### 重點變更
1. 新分類骨架:
- `Hardware`
- `Software`
- `AI Application``Smart Retail``Medical & Health``Vehicle & Transportation``Smart Manufacture``Smart Home``Future City`
- `Books``Physical Books``E-Books``Certification Books`
2. 中文多語系:
- 透過 `LocalizedProperty` 寫入
- 修正語系代碼匹配包含 `tw`
3. 舊子類刪除策略:
- 由名稱比對改為父節點比對
- 只要是 `Software` 的直屬子類即納入清理
4. 實表名相容:
- SQL 依實際資料表 `Product_Category_Mapping` 修正(非 `ProductCategory`
### 已驗證結果kneo_dev
1. 分類重構 SQL 已成功 `COMMIT`
2. 舊 `Software` 子類已清空(查詢為 0 筆)。
3. `AI Application` 六子類仍存在。
4. 13 個目標分類中文多語系已補齊。
---
## 2026-05-05硬體 PR 欄位實作(商品層)
### 內容
1. 新增產品 PR 欄位預設鍵定義(以 `GenericAttribute` 儲存):
[NopProductPrDefaults.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Core/Domain/Catalog/NopProductPrDefaults.cs)
2. 後台產品模型新增 PR 欄位(含多語系與字級欄位):
[ProductModel.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Models/Catalog/ProductModel.cs)
3. 後台產品編輯頁新增「硬體 PR 區塊」卡片與欄位:
- [_CreateOrUpdate.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Views/Product/_CreateOrUpdate.cshtml)
- [_CreateOrUpdate.HardwarePr.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Views/Product/_CreateOrUpdate.HardwarePr.cshtml)
4. 後台儲存流程已接上:
- `Create/Edit` 會寫入 PR 欄位
- `UpdateLocalesAsync` 會寫入各語言版本欄位key 帶 `LanguageId`
[ProductController.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Controllers/ProductController.cs)
5. 後台載入流程已接上(含多語系讀取):
[ProductModelFactory.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Factories/ProductModelFactory.cs)
6. 前台硬體商品頁已接上 PR 區塊渲染:
- 公開模型新增對應欄位:
[ProductDetailsModel.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Models/Catalog/ProductDetailsModel.cs)
- 產品模型工廠新增語言 fallback 讀取:
[ProductModelFactory.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Factories/ProductModelFactory.cs)
- 硬體模板插入 PR 區塊:
[ProductTemplate.Hardware.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Views/Product/ProductTemplate.Hardware.cshtml)
- PR 區塊 partial含 PR 全文展開 `details/summary`
[_ProductHardwarePrHero.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Views/Product/_ProductHardwarePrHero.cshtml)
### 驗證
1. `dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Debug` 成功0 error
---
## 2026-05-05首頁輪播開關與排序獨立管理
### 內容
1. 新增首頁輪播映射實體:
[HomepageHeroProduct.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Core/Domain/Catalog/HomepageHeroProduct.cs)
2. 新增資料表 mapping builder
[HomepageHeroProductBuilder.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Data/Mapping/Builders/Catalog/HomepageHeroProductBuilder.cs)
3. 新增 migration供既有環境升級時建表
[HomepageHeroProductMigration.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Data/Migrations/UpgradeTo470/HomepageHeroProductMigration.cs)
4. 新增服務層:
- [IHomepageHeroProductService.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Services/Catalog/IHomepageHeroProductService.cs)
- [HomepageHeroProductService.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Libraries/Nop.Services/Catalog/HomepageHeroProductService.cs)
5. 後台商品管理新增首頁輪播管理頁(加入/刪除/啟用/排序):
- [ProductController.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Controllers/ProductController.cs)
- [HomepageHero.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Views/Product/HomepageHero.cshtml)
- [HomepageHeroProductModel.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Models/Catalog/HomepageHeroProductModel.cs)
- [List.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Areas/Admin/Views/Product/List.cshtml)(增加「首頁輪播」入口按鈕)
6. 前台首頁接入輪播資料來源:
- [HomepageHeroProductsViewComponent.cs](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Components/HomepageHeroProductsViewComponent.cs)
- [Default.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Views/Shared/Components/HomepageHeroProducts/Default.cshtml)
- [Index.cshtml](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/src/Presentation/Nop.Web/Views/Home/Index.cshtml)
### 驗證
1. `dotnet build src/Presentation/Nop.Web/Nop.Web.csproj -c Debug` 成功0 error

View File

@ -1,474 +0,0 @@
# Frontend Refresh and Media Storage Plan
## Purpose
This plan defines the first major rebuild track for the storefront and media layer. The goal is to replace the current storefront theme while introducing a private S3-compatible media storage layer that can later take over nopCommerce product pictures and support a controlled production migration.
The current data volume is small, so the plan favors clear boundaries and reversible migration steps over heavy batch infrastructure.
## Fixed Decisions
- The existing storefront theme can be abandoned for the public frontend rebuild.
- A new theme will be created instead of modifying `DefaultClean` in place.
- The first frontend implementation is desktop-first. Responsive behavior can be added later, but HTML and CSS structure should leave room for responsive breakpoints.
- The storefront rebuild is not only visual CSS work. It may require Razor view changes, model/factory changes, controller changes, admin settings, widget changes, and seed/config data.
- Product-related visuals should use existing product picture data where practical instead of being recreated as static theme images.
- Media objects must not expose raw S3, MinIO, or bucket URLs to browsers.
- AWS S3 will be used for development/test and production object storage.
- MinIO will be used in stage as an S3-compatible replacement.
- All browser-facing media URLs must go through the application, using a media proxy.
- CloudFront is intentionally out of scope because DNS is not controlled by the team.
- Existing production migration can use a planned maintenance window of roughly half a day to one day.
## Target Architecture
### Storage
Create an application-level media storage abstraction instead of mounting S3 as a filesystem.
The storage layer should support:
- AWS S3 for test/prod.
- MinIO for stage.
- Private buckets only.
- No public object ACLs.
- Configurable endpoint, bucket, region, access key, secret, and path-style addressing.
Proposed interface:
```csharp
public interface IMediaStorage
{
Task<MediaObjectInfo> PutAsync(MediaPutRequest request, CancellationToken cancellationToken = default);
Task<MediaReadResult> GetAsync(string objectKey, CancellationToken cancellationToken = default);
Task<MediaObjectInfo?> HeadAsync(string objectKey, CancellationToken cancellationToken = default);
Task DeleteAsync(string objectKey, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(string objectKey, CancellationToken cancellationToken = default);
}
```
### Browser-Facing URLs
Media URLs should be stable application URLs, not object storage URLs.
Examples:
```text
/media/assets/{assetId}/{seoName}
/media/pictures/{pictureId}/{size}/{seoName}
/media/pictures/{pictureId}/original/{seoName}
```
Optional compatibility route for existing rich editor HTML:
```text
/images/uploaded/{**path}
```
This route can temporarily proxy legacy uploaded paths while old HTML is migrated.
### Media Proxy
Add a media proxy controller responsible for reading from storage and streaming the response.
Required behavior:
- Stream object content without buffering entire files in memory.
- Return correct `Content-Type`.
- Return `Content-Length` when available.
- Return `ETag` and/or `Last-Modified` when available.
- Support conditional requests with `304 Not Modified`.
- Apply browser cache headers.
- Keep private object storage credentials server-side only.
Later behavior:
- Support HTTP `Range` requests for video and large media.
- Add authorization rules if private customer/vendor media is introduced.
- Add image transformation variants if needed beyond nopCommerce thumbnail behavior.
## Data Model Direction
Use explicit metadata tables rather than relying on physical paths inside HTML or S3 object keys alone.
### Managed Media Assets
Add a new managed media asset model for non-product-picture assets such as homepage visuals, banners, rich editor images, icons, downloadable presentation media, and future marketing media.
Suggested fields:
- `Id`
- `StorageProvider`
- `Bucket`
- `ObjectKey`
- `FileName`
- `SeoFileName`
- `MimeType`
- `FileSize`
- `Width`
- `Height`
- `Checksum`
- `AltText`
- `Title`
- `UsageType`
- `CreatedOnUtc`
- `UpdatedOnUtc`
- `Deleted`
### Product Pictures
Keep nopCommerce product picture concepts intact:
- Keep `Picture`.
- Keep `ProductPicture`.
- Keep existing picture metadata such as MIME type, SEO filename, alt, title, and display order.
Add storage mapping for picture binaries, either with a dedicated `PictureStorage` table or by reusing the managed media asset table with a product-picture usage type.
Suggested picture storage fields:
- `PictureId`
- `StorageProvider`
- `Bucket`
- `ObjectKey`
- `OriginalFileName`
- `MimeType`
- `FileSize`
- `Width`
- `Height`
- `Checksum`
- `CreatedOnUtc`
`PictureBinary` should remain untouched during initial rollout and migration dry runs. It can be cleared only after production validation and rollback risk is acceptable.
## Phase 1: New Frontend Theme and Media Storage Foundation
This is the first major step.
### Scope
- Create the new frontend theme.
- Build the initial `IMediaStorage` abstraction.
- Implement S3-compatible storage for AWS S3 and MinIO.
- Add media proxy routes.
- Add admin-managed media assets for assets currently handled as static files.
- Update new frontend views to use managed media asset URLs instead of hardcoded static files.
- Add or adjust application data needed by the new storefront, such as homepage section configuration, featured product selections, category/application links, and managed media references.
- Adjust public model factories/controllers only when the new frontend requires data that existing models do not expose.
### Theme Work
- Create `Themes/Kneo`.
- Add a new `theme.json`.
- Add new frontend CSS/JS asset structure.
- Override only the required public views and partials.
- Keep `DefaultClean` available as a reference and fallback.
- Build the first version as a desktop-first layout matching the supplied design screenshots.
- Use semantic section/partial boundaries so mobile breakpoints can be added later without replacing the markup.
- Prioritize the main storefront paths:
- Home page
- Header/navigation
- Footer
- Category/listing pages
- Product cards
- Product detail page
- Cart entry points and purchase CTA surfaces
### Media Storage Work
- Add configuration for S3-compatible storage:
- Provider
- Endpoint
- Region
- Bucket
- Access key
- Secret key
- Force path style
- Public proxy base path
- Implement AWS S3/MinIO client code behind `IMediaStorage`.
- Add managed media metadata persistence.
- Add media proxy controller and routes.
- Add admin upload/list/edit/delete flow for managed assets.
- Add rich editor insertion path for new managed media assets.
- Add controlled ways to reference managed media from storefront sections, rather than hardcoding object keys or bucket URLs in Razor views.
### Storefront Data Work
The redesigned home page and other new storefront surfaces may require data that the current theme does not model explicitly.
Possible data/config needs:
- Home hero copy and media references.
- Featured hardware/product list.
- Recommended books/product list.
- Application domain tiles and links.
- Section display order.
- Header navigation entries.
- CTA target URLs.
Implementation options, from simplest to most flexible:
- Use existing nopCommerce categories/products/manufacturers where the content naturally maps to catalog data.
- Use settings for small global values such as hero title, subtitle, and CTA links.
- Use a custom table/model for repeatable home page sections if the content needs admin management.
- Use widgets only when the content needs to be independently pluggable.
Avoid encoding business-editable homepage content directly in CSS or static HTML unless it is clearly temporary.
### Static Asset Conversion
Convert frontend-owned non-CSS media from static files into managed media where practical.
Examples:
- Homepage hero images
- Banner images
- Landing/media blocks
- Editorial image assets
- Future video poster images
CSS-only assets can remain under the theme if they are purely presentational and not expected to change through admin workflows.
### Out of Scope
- Existing `PictureService` replacement.
- Existing product picture migration.
- Bulk rewriting existing product descriptions.
- Deleting `PictureBinary`.
- CloudFront integration.
### Acceptance Criteria
- New theme can be selected and renders core storefront pages.
- New frontend assets can be uploaded to S3/MinIO through the application.
- New frontend views render media through application proxy URLs.
- No browser-visible raw S3 or MinIO URLs.
- Stage can switch between AWS S3-compatible config and MinIO config.
- Existing product pictures still work through current nopCommerce behavior.
- Required home page data can be configured or seeded without editing Razor/CSS for normal content updates.
## Phase 2: Apply Media Storage to Picture Service
### Scope
Replace product picture binary storage/read behavior with the new media storage layer while keeping existing public and admin behavior as stable as possible.
### Work Items
- Extend or replace `IPictureService` implementation.
- Make new product picture uploads write to S3/MinIO.
- Make `GetPictureUrlAsync` return media proxy URLs.
- Make original and generated thumbnail reads flow through the proxy.
- Decide whether thumbnails are:
- generated on demand and stored in S3/MinIO, or
- generated during upload/update and stored as variants.
- Preserve existing product picture metadata and product associations.
- Keep compatibility for pictures still stored in DB during rollout.
### Compatibility Strategy
During rollout, product pictures may exist in two states:
- Legacy DB-backed picture: `PictureBinary.BinaryData` exists and no storage mapping exists.
- New storage-backed picture: storage mapping exists and object is in S3/MinIO.
The picture service should support both until migration is complete.
### Acceptance Criteria
- New product image uploads are stored in S3/MinIO.
- Existing DB-backed product images still render.
- Product image URLs are application proxy URLs.
- Admin product picture add/edit/delete behavior still works.
- Category pages, product detail pages, carts, orders, and search thumbnails render correctly.
## Phase 3: Migration Plan and Migration Tooling
### Scope
Build and test tools to migrate existing DB-backed product pictures and legacy rich editor uploads into the new storage system.
### Product Picture Migration
Migration tool responsibilities:
- Scan `Picture` records.
- Read current binary data from `PictureBinary`.
- Generate deterministic object keys.
- Upload originals to S3/MinIO.
- Create storage mapping records.
- Verify object exists and size/checksum matches.
- Mark migration status.
- Produce a migration report.
Suggested object key format:
```text
pictures/{pictureId}/original/{safeSeoName-or-pictureId}.{extension}
pictures/{pictureId}/thumbs/{size}/{safeSeoName-or-pictureId}.{extension}
```
Thumbs may be generated lazily after migration unless performance testing shows preload is needed.
### Rich Editor Upload Migration
Migration tool responsibilities:
- Scan product descriptions and other rich editor fields for `/images/uploaded/...`.
- Resolve matching local files from the current production filesystem.
- Upload files to managed media storage.
- Create managed media asset records.
- Optionally rewrite HTML to `/media/assets/...`.
- Produce a report of missing local files and rewritten references.
If direct rewrite is risky, keep `/images/uploaded/{**path}` as a legacy proxy route during the transition.
### Production Cutover
Expected maintenance-window flow:
1. Enable maintenance mode.
2. Freeze product/media edits.
3. Run product picture migration.
4. Run rich editor media migration.
5. Validate migration report.
6. Switch production config to storage-backed picture service.
7. Clear application cache.
8. Run smoke tests on critical pages.
9. Disable maintenance mode.
### Rollback Strategy
- Do not delete or clear `PictureBinary` during initial production cutover.
- Keep legacy local uploaded files until product verification is complete.
- Keep DB and filesystem backups from immediately before migration.
- If needed, switch config back to DB-backed picture behavior and legacy static uploaded files.
### Acceptance Criteria
- All existing product pictures have storage mapping records.
- All migrated objects exist in S3/MinIO.
- Product pages render after clearing browser/application cache.
- Missing rich editor files are reported explicitly.
- No raw object storage URLs appear in rendered storefront HTML.
## Risks and Controls
### Web Server Bandwidth
Without CloudFront, all media flows through the application.
Controls:
- Stream responses.
- Use browser caching.
- Use conditional GET.
- Add Range support before serving large videos.
- Keep image sizes reasonable.
### Cache Invalidation
Stable URLs can cause stale browser caches when media is replaced.
Controls:
- Prefer immutable object keys for replaced files.
- Include version or content hash in proxy URLs when needed.
- Use `ETag` and `Last-Modified`.
### Legacy HTML References
Existing rich editor HTML may point to missing local files.
Controls:
- Build a scanner before production migration.
- Report missing files before cutover.
- Keep a compatibility proxy route.
### S3-Compatible Differences
AWS S3 and MinIO differ in endpoint style, path style, metadata, and local TLS behavior.
Controls:
- Keep all provider differences inside `S3MediaStorage`.
- Test against MinIO in stage and AWS S3 in test.
- Avoid provider-specific URLs in persisted HTML.
## Suggested Implementation Order
1. Add configuration and `IMediaStorage`.
2. Add S3/MinIO implementation.
3. Add media metadata tables and migrations.
4. Add media proxy read endpoint.
5. Add admin-managed media upload/list/edit flow.
6. Create `Themes/Kneo`.
7. Convert new frontend non-CSS media to managed assets.
8. Integrate managed assets with rich editor for new content.
9. Add storage-backed picture service compatibility.
10. Build migration scanner/report.
11. Build product picture migration tool.
12. Build rich editor media migration tool.
13. Run dry runs on stage.
14. Execute production migration during maintenance window.
## Deployment Memo
This section tracks deploy-time work that should not be forgotten as the project evolves.
### Phase 1 Deploy Checklist
- Add S3/MinIO configuration values for the target environment.
- Create the target bucket in AWS S3 or MinIO.
- Keep the bucket private.
- Create or provision application credentials with only the required bucket permissions.
- Confirm application config does not expose raw S3/MinIO URLs to rendered HTML.
- Run database migrations for managed media metadata.
- Seed required storefront settings/data:
- active theme set to `Kneo`
- home hero configuration
- featured product/book selections
- application tiles
- navigation entries if they are data-driven
- Upload required non-product homepage media into managed media storage.
- Verify media proxy can read from S3/MinIO in the deployed environment.
- Verify cache headers and conditional GET behavior on media responses.
- Confirm existing product pictures still render through the legacy picture path.
- Clear application cache after theme/settings changes.
- Smoke test:
- home page
- category/listing page
- product detail page
- cart entry point
- admin media upload/list
### Phase 2 Deploy Checklist
- Run database migrations for picture storage mapping.
- Enable storage-backed picture service in the target environment.
- Confirm new product picture uploads write to S3/MinIO.
- Confirm legacy DB-backed product pictures still render.
- Verify generated thumbnail behavior.
- Clear picture/model caches.
- Smoke test:
- product image upload in admin
- product detail gallery
- product card thumbnails
- cart/order thumbnails
### Phase 3 Migration Checklist
- Take database backup.
- Back up legacy local media directories, especially `wwwroot/images/uploaded`.
- Enable maintenance mode.
- Freeze product and media edits.
- Run migration dry-run/report first if possible.
- Run product picture migration.
- Run rich editor media migration.
- Verify migration counts and missing-file report.
- Switch production config to storage-backed picture service.
- Clear application and CDN/browser-relevant caches where applicable.
- Smoke test critical storefront and admin pages.
- Keep `PictureBinary` data and legacy uploaded files until post-cutover validation is complete.
- Disable maintenance mode.

View File

@ -1,48 +0,0 @@
# 操作備忘錄(通用)
## 用途
本文件用於記錄「部署或資料調整時不可漏掉的操作步驟」。
不限定分類調整,後續所有會影響既有資料或上線流程的改動都追加在此。
---
## 2026-05-03 ~ 2026-05-05分類骨架調整與首頁資料前置
### 執行目標
1. 建立新分類骨架:`Hardware``Software``AI Application`6 子類)、`Books`3 子類)
2. 補齊分類中文多語系
3. 處理舊 `Software` 子類別清理
4. 保留可回滾資料
### SQL 檔案
1. [category-restructure-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-restructure-20260503.sql)
2. [category-delete-legacy-software-children-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-delete-legacy-software-children-20260503.sql)
3. [category-localization-backfill-tw-20260503.sql](/Users/chenyanhan/Documents/Projects/Kneo_marketplace/docs/sql/category-localization-backfill-tw-20260503.sql)
### 操作重點
1. 先執行分類重構 SQL再執行舊子類清理 SQL。
2. 語系代碼需支援 `tw`,否則中文多語系不會落地。
3. `Software` 下舊子類刪除邏輯已改為「依父節點刪除」,不再依名稱比對。
4. 刪除前會備份 `Category``Product_Category_Mapping``LocalizedProperty``UrlRecord` 到備份表。
### 手動設定
1. 後台手動調整分類顯示順序:`Hardware = 10``Software = 20`
2. 前台若未即時反映中文名稱,需清除應用程式快取。
### 驗證項目
1. `AI Application` 下應有 6 個子類。
2. `Books` 下應有 3 個子類(英文預設名,中文靠多語系)。
3. `Software` 下子類應為空(新骨架目標)。
4. 13 個目標分類的中文多語系需存在於 `LocalizedProperty`
### 回滾原則
1. 先恢復分類可見性或重新建立父子關係。
2. 若資料已刪除,從備份表回補 `Category``Product_Category_Mapping``LocalizedProperty``UrlRecord`

View File

@ -1,14 +0,0 @@
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Homepage hero product mapping
/// </summary>
public partial class HomepageHeroProduct : BaseEntity
{
public int ProductId { get; set; }
public bool IsEnabled { get; set; }
public int DisplayOrder { get; set; }
public DateTime CreatedOnUtc { get; set; }
public DateTime UpdatedOnUtc { get; set; }
}

View File

@ -1,25 +0,0 @@
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Product PR-related attribute defaults
/// </summary>
public static partial class NopProductPrDefaults
{
public static string HeroEnabledAttribute => "Product.Hero.Enabled";
public static string HeroTitleLine1Attribute => "Product.Hero.TitleLine1";
public static string HeroTitleLine2Attribute => "Product.Hero.TitleLine2";
public static string HeroFeatureLine1Attribute => "Product.Hero.FeatureLine1";
public static string HeroFeatureLine2Attribute => "Product.Hero.FeatureLine2";
public static string HeroPrCopyAttribute => "Product.Hero.PrCopy";
public static string HeroTitleLine1FontSizeAttribute => "Product.Hero.TitleLine1FontSize";
public static string HeroTitleLine2FontSizeAttribute => "Product.Hero.TitleLine2FontSize";
public static string HeroFeatureLine1FontSizeAttribute => "Product.Hero.FeatureLine1FontSize";
public static string HeroFeatureLine2FontSizeAttribute => "Product.Hero.FeatureLine2FontSize";
public static string HeroPrCopyFontSizeAttribute => "Product.Hero.PrCopyFontSize";
public static string GetLocalizedKey(string key, int languageId)
{
return $"{key}.{languageId}";
}
}

View File

@ -1,28 +0,0 @@
using FluentMigrator.Builders.Create.Table;
using Nop.Core.Domain.Catalog;
using Nop.Data.Extensions;
namespace Nop.Data.Mapping.Builders.Catalog;
/// <summary>
/// Represents a homepage hero product entity builder
/// </summary>
public partial class HomepageHeroProductBuilder : NopEntityBuilder<HomepageHeroProduct>
{
#region Methods
/// <summary>
/// Apply entity configuration
/// </summary>
/// <param name="table">Create table expression builder</param>
public override void MapEntity(CreateTableExpressionBuilder table)
{
table
.WithColumn(nameof(HomepageHeroProduct.ProductId)).AsInt32().ForeignKey<Product>()
.WithColumn(nameof(HomepageHeroProduct.IsEnabled)).AsBoolean().NotNullable()
.WithColumn(nameof(HomepageHeroProduct.DisplayOrder)).AsInt32().NotNullable();
}
#endregion
}

View File

@ -1,16 +0,0 @@
using FluentMigrator;
using Nop.Core.Domain.Catalog;
using Nop.Data.Extensions;
using Nop.Data.Mapping;
namespace Nop.Data.Migrations.UpgradeTo470;
[NopSchemaMigration("2026-05-05 10:00:00", "Add homepage hero product mapping table")]
public class HomepageHeroProductMigration : ForwardOnlyMigration
{
public override void Up()
{
if (!Schema.Table(nameof(HomepageHeroProduct)).Exists())
Create.TableFor<HomepageHeroProduct>();
}
}

View File

@ -102,8 +102,5 @@ public class SchemaMigration : ForwardOnlyMigration
if (!Schema.Table(newsLetterSubscriptionTableName).Column(nameof(NewsLetterSubscription.LanguageId)).Exists()) if (!Schema.Table(newsLetterSubscriptionTableName).Column(nameof(NewsLetterSubscription.LanguageId)).Exists())
Alter.Table(newsLetterSubscriptionTableName) Alter.Table(newsLetterSubscriptionTableName)
.AddColumn(nameof(NewsLetterSubscription.LanguageId)).AsInt32().NotNullable().SetExistingRowsTo(1); .AddColumn(nameof(NewsLetterSubscription.LanguageId)).AsInt32().NotNullable().SetExistingRowsTo(1);
if (!Schema.Table(nameof(HomepageHeroProduct)).Exists())
Create.TableFor<HomepageHeroProduct>();
} }
} }

View File

@ -1,49 +0,0 @@
using LinqToDB;
using Nop.Core.Domain.Catalog;
using Nop.Data;
namespace Nop.Services.Catalog;
public partial class HomepageHeroProductService : IHomepageHeroProductService
{
private readonly IRepository<HomepageHeroProduct> _homepageHeroProductRepository;
public HomepageHeroProductService(IRepository<HomepageHeroProduct> homepageHeroProductRepository)
{
_homepageHeroProductRepository = homepageHeroProductRepository;
}
public virtual async Task<IList<HomepageHeroProduct>> GetAllAsync(bool onlyEnabled = false)
{
var query = _homepageHeroProductRepository.Table;
if (onlyEnabled)
query = query.Where(x => x.IsEnabled);
return await query.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Id).ToListAsync();
}
public virtual async Task<HomepageHeroProduct> GetByIdAsync(int id)
{
return await _homepageHeroProductRepository.GetByIdAsync(id);
}
public virtual async Task<HomepageHeroProduct> GetByProductIdAsync(int productId)
{
return await _homepageHeroProductRepository.Table.FirstOrDefaultAsync(x => x.ProductId == productId);
}
public virtual async Task InsertAsync(HomepageHeroProduct entity)
{
await _homepageHeroProductRepository.InsertAsync(entity);
}
public virtual async Task UpdateAsync(HomepageHeroProduct entity)
{
await _homepageHeroProductRepository.UpdateAsync(entity);
}
public virtual async Task DeleteAsync(HomepageHeroProduct entity)
{
await _homepageHeroProductRepository.DeleteAsync(entity);
}
}

View File

@ -1,14 +0,0 @@
using Nop.Core.Domain.Catalog;
namespace Nop.Services.Catalog;
public partial interface IHomepageHeroProductService
{
Task<IList<HomepageHeroProduct>> GetAllAsync(bool onlyEnabled = false);
Task<HomepageHeroProduct> GetByIdAsync(int id);
Task<HomepageHeroProduct> GetByProductIdAsync(int productId);
Task InsertAsync(HomepageHeroProduct entity);
Task UpdateAsync(HomepageHeroProduct entity);
Task DeleteAsync(HomepageHeroProduct entity);
}

View File

@ -1283,10 +1283,10 @@ public partial class ProductService : IProductService
//filter by products with tracking inventory //filter by products with tracking inventory
query = query.Where(product => product.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock); query = query.Where(product => product.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock);
//filter by products with stock quantity less than or equal to the notification threshold //filter by products with stock quantity less than the minimum
query = query.Where(product => query = query.Where(product =>
(product.UseMultipleWarehouses ? _productWarehouseInventoryRepository.Table.Where(pwi => pwi.ProductId == product.Id).Sum(pwi => pwi.StockQuantity - pwi.ReservedQuantity) (product.UseMultipleWarehouses ? _productWarehouseInventoryRepository.Table.Where(pwi => pwi.ProductId == product.Id).Sum(pwi => pwi.StockQuantity - pwi.ReservedQuantity)
: product.StockQuantity) <= product.NotifyAdminForQuantityBelow); : product.StockQuantity) <= product.MinStockQuantity);
//ignore deleted products //ignore deleted products
query = query.Where(product => !product.Deleted); query = query.Where(product => !product.Deleted);
@ -1302,7 +1302,7 @@ public partial class ProductService : IProductService
if (loadPublishedOnly.HasValue) if (loadPublishedOnly.HasValue)
query = query.Where(product => product.Published == loadPublishedOnly.Value); query = query.Where(product => product.Published == loadPublishedOnly.Value);
query = query.OrderBy(product => product.NotifyAdminForQuantityBelow).ThenBy(product => product.DisplayOrder).ThenBy(product => product.Id); query = query.OrderBy(product => product.MinStockQuantity).ThenBy(product => product.DisplayOrder).ThenBy(product => product.Id);
return await query.ToPagedListAsync(pageIndex, pageSize, getOnlyTotalCount); return await query.ToPagedListAsync(pageIndex, pageSize, getOnlyTotalCount);
} }
@ -1325,8 +1325,8 @@ public partial class ProductService : IProductService
var combinations = from pac in _productAttributeCombinationRepository.Table var combinations = from pac in _productAttributeCombinationRepository.Table
join p in _productRepository.Table on pac.ProductId equals p.Id join p in _productRepository.Table on pac.ProductId equals p.Id
where where
//filter by combinations with stock quantity less than or equal to the notification threshold //filter by combinations with stock quantity less than the minimum
pac.StockQuantity <= pac.NotifyAdminForQuantityBelow && pac.StockQuantity <= pac.MinStockQuantity &&
//filter by products with tracking inventory by attributes //filter by products with tracking inventory by attributes
p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes &&
//ignore deleted products //ignore deleted products
@ -2847,4 +2847,4 @@ public partial class ProductService : IProductService
#endregion #endregion
#endregion #endregion
} }

View File

@ -145,7 +145,6 @@ public partial class NopStartup : INopStartup
services.AddScoped<IProductAttributeParser, ProductAttributeParser>(); services.AddScoped<IProductAttributeParser, ProductAttributeParser>();
services.AddScoped<IProductAttributeService, ProductAttributeService>(); services.AddScoped<IProductAttributeService, ProductAttributeService>();
services.AddScoped<IProductService, ProductService>(); services.AddScoped<IProductService, ProductService>();
services.AddScoped<IHomepageHeroProductService, HomepageHeroProductService>();
services.AddScoped<ICopyProductService, CopyProductService>(); services.AddScoped<ICopyProductService, CopyProductService>();
services.AddScoped<ISpecificationAttributeService, SpecificationAttributeService>(); services.AddScoped<ISpecificationAttributeService, SpecificationAttributeService>();
services.AddScoped<IProductTemplateService, ProductTemplateService>(); services.AddScoped<IProductTemplateService, ProductTemplateService>();
@ -328,4 +327,4 @@ public partial class NopStartup : INopStartup
/// Gets order of this startup configuration implementation /// Gets order of this startup configuration implementation
/// </summary> /// </summary>
public int Order => 2000; public int Order => 2000;
} }

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"ConnectionString": "Host=192.168.0.130;Database=kneo_dev;Username=kneoweb;Password=kneo!admin", "ConnectionString": "Host=192.168.0.130;Database=kneo;Username=kneoweb;Password=kneo!admin",
"DataProvider": "postgresql", "DataProvider": "postgresql",
"SQLCommandTimeout": null, "SQLCommandTimeout": null,
"WithNoLock": false "WithNoLock": false

View File

@ -66,7 +66,6 @@ public partial class ProductController : BaseAdminController
protected readonly IProductModelFactory _productModelFactory; protected readonly IProductModelFactory _productModelFactory;
protected readonly IProductService _productService; protected readonly IProductService _productService;
protected readonly IProductTagService _productTagService; protected readonly IProductTagService _productTagService;
protected readonly IHomepageHeroProductService _homepageHeroProductService;
protected readonly ISettingService _settingService; protected readonly ISettingService _settingService;
protected readonly IShippingService _shippingService; protected readonly IShippingService _shippingService;
protected readonly IShoppingCartService _shoppingCartService; protected readonly IShoppingCartService _shoppingCartService;
@ -110,7 +109,6 @@ public partial class ProductController : BaseAdminController
IProductModelFactory productModelFactory, IProductModelFactory productModelFactory,
IProductService productService, IProductService productService,
IProductTagService productTagService, IProductTagService productTagService,
IHomepageHeroProductService homepageHeroProductService,
ISettingService settingService, ISettingService settingService,
IShippingService shippingService, IShippingService shippingService,
IShoppingCartService shoppingCartService, IShoppingCartService shoppingCartService,
@ -149,7 +147,6 @@ public partial class ProductController : BaseAdminController
_productModelFactory = productModelFactory; _productModelFactory = productModelFactory;
_productService = productService; _productService = productService;
_productTagService = productTagService; _productTagService = productTagService;
_homepageHeroProductService = homepageHeroProductService;
_settingService = settingService; _settingService = settingService;
_shippingService = shippingService; _shippingService = shippingService;
_shoppingCartService = shoppingCartService; _shoppingCartService = shoppingCartService;
@ -198,55 +195,9 @@ public partial class ProductController : BaseAdminController
//search engine name //search engine name
var seName = await _urlRecordService.ValidateSeNameAsync(product, localized.SeName, localized.Name, false); var seName = await _urlRecordService.ValidateSeNameAsync(product, localized.SeName, localized.Name, false);
await _urlRecordService.SaveSlugAsync(product, seName, localized.LanguageId); await _urlRecordService.SaveSlugAsync(product, seName, localized.LanguageId);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine1Attribute, localized.LanguageId),
localized.HeroTitleLine1);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine2Attribute, localized.LanguageId),
localized.HeroTitleLine2);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine1Attribute, localized.LanguageId),
localized.HeroFeatureLine1);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine2Attribute, localized.LanguageId),
localized.HeroFeatureLine2);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroPrCopyAttribute, localized.LanguageId),
localized.HeroPrCopy);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine1FontSizeAttribute, localized.LanguageId),
localized.HeroTitleLine1FontSize);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine2FontSizeAttribute, localized.LanguageId),
localized.HeroTitleLine2FontSize);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine1FontSizeAttribute, localized.LanguageId),
localized.HeroFeatureLine1FontSize);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine2FontSizeAttribute, localized.LanguageId),
localized.HeroFeatureLine2FontSize);
await _genericAttributeService.SaveAttributeAsync(product,
NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroPrCopyFontSizeAttribute, localized.LanguageId),
localized.HeroPrCopyFontSize);
} }
} }
protected virtual async Task SaveHardwarePrAttributesAsync(Product product, ProductModel model)
{
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroEnabledAttribute, model.HeroEnabled);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroTitleLine1Attribute, model.HeroTitleLine1);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroTitleLine2Attribute, model.HeroTitleLine2);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroFeatureLine1Attribute, model.HeroFeatureLine1);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroFeatureLine2Attribute, model.HeroFeatureLine2);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroPrCopyAttribute, model.HeroPrCopy);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroTitleLine1FontSizeAttribute, model.HeroTitleLine1FontSize);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroTitleLine2FontSizeAttribute, model.HeroTitleLine2FontSize);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroFeatureLine1FontSizeAttribute, model.HeroFeatureLine1FontSize);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroFeatureLine2FontSizeAttribute, model.HeroFeatureLine2FontSize);
await _genericAttributeService.SaveAttributeAsync(product, NopProductPrDefaults.HeroPrCopyFontSizeAttribute, model.HeroPrCopyFontSize);
}
protected virtual async Task UpdateLocalesAsync(ProductTag productTag, ProductTagModel model) protected virtual async Task UpdateLocalesAsync(ProductTag productTag, ProductTagModel model)
{ {
foreach (var localized in model.Locales) foreach (var localized in model.Locales)
@ -968,8 +919,6 @@ public partial class ProductController : BaseAdminController
//stores //stores
await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds); await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds);
await SaveHardwarePrAttributesAsync(product, model);
//discounts //discounts
await SaveDiscountMappingsAsync(product, model); await SaveDiscountMappingsAsync(product, model);
@ -1113,8 +1062,6 @@ public partial class ProductController : BaseAdminController
//stores //stores
await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds); await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds);
await SaveHardwarePrAttributesAsync(product, model);
//discounts //discounts
await SaveDiscountMappingsAsync(product, model); await SaveDiscountMappingsAsync(product, model);
@ -1228,108 +1175,6 @@ public partial class ProductController : BaseAdminController
return RedirectToAction("List"); return RedirectToAction("List");
} }
public virtual async Task<IActionResult> HomepageHero()
{
if (!await _permissionService.AuthorizeAsync(StandardPermissionProvider.ManageProducts))
return AccessDeniedView();
var mappings = await _homepageHeroProductService.GetAllAsync();
var model = new HomepageHeroProductListModel();
foreach (var item in mappings)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
if (product == null || product.Deleted)
continue;
model.Items.Add(new HomepageHeroProductModel
{
Id = item.Id,
ProductId = item.ProductId,
ProductName = product.Name,
IsEnabled = item.IsEnabled,
DisplayOrder = item.DisplayOrder
});
}
return View(model);
}
[HttpPost]
public virtual async Task<IActionResult> HomepageHeroAdd(HomepageHeroProductListModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermissionProvider.ManageProducts))
return AccessDeniedView();
if (model.AddProductId <= 0)
{
_notificationService.ErrorNotification("請輸入商品 ID");
return RedirectToAction(nameof(HomepageHero));
}
var product = await _productService.GetProductByIdAsync(model.AddProductId);
if (product == null || product.Deleted)
{
_notificationService.ErrorNotification("找不到商品");
return RedirectToAction(nameof(HomepageHero));
}
var existing = await _homepageHeroProductService.GetByProductIdAsync(model.AddProductId);
if (existing != null)
{
_notificationService.WarningNotification("該商品已在首頁輪播清單中");
return RedirectToAction(nameof(HomepageHero));
}
await _homepageHeroProductService.InsertAsync(new HomepageHeroProduct
{
ProductId = model.AddProductId,
IsEnabled = true,
DisplayOrder = 0,
CreatedOnUtc = DateTime.UtcNow,
UpdatedOnUtc = DateTime.UtcNow
});
_notificationService.SuccessNotification("已加入首頁輪播清單");
return RedirectToAction(nameof(HomepageHero));
}
[HttpPost]
public virtual async Task<IActionResult> HomepageHeroUpdate(HomepageHeroProductListModel model)
{
if (!await _permissionService.AuthorizeAsync(StandardPermissionProvider.ManageProducts))
return AccessDeniedView();
foreach (var item in model.Items)
{
var entity = await _homepageHeroProductService.GetByIdAsync(item.Id);
if (entity == null)
continue;
entity.IsEnabled = item.IsEnabled;
entity.DisplayOrder = item.DisplayOrder;
entity.UpdatedOnUtc = DateTime.UtcNow;
await _homepageHeroProductService.UpdateAsync(entity);
}
_notificationService.SuccessNotification("首頁輪播設定已更新");
return RedirectToAction(nameof(HomepageHero));
}
[HttpPost]
public virtual async Task<IActionResult> HomepageHeroDelete(int id)
{
if (!await _permissionService.AuthorizeAsync(StandardPermissionProvider.ManageProducts))
return AccessDeniedView();
var entity = await _homepageHeroProductService.GetByIdAsync(id);
if (entity != null)
await _homepageHeroProductService.DeleteAsync(entity);
_notificationService.SuccessNotification("已移除輪播商品");
return RedirectToAction(nameof(HomepageHero));
}
[HttpPost] [HttpPost]
public virtual async Task<IActionResult> DeleteSelected(ICollection<int> selectedIds) public virtual async Task<IActionResult> DeleteSelected(ICollection<int> selectedIds)
{ {
@ -3923,4 +3768,4 @@ public partial class ProductController : BaseAdminController
#endregion #endregion
#endregion #endregion
} }

View File

@ -53,7 +53,6 @@ public partial class ProductModelFactory : IProductModelFactory
protected readonly IDateTimeHelper _dateTimeHelper; protected readonly IDateTimeHelper _dateTimeHelper;
protected readonly IDiscountService _discountService; protected readonly IDiscountService _discountService;
protected readonly IDiscountSupportedModelFactory _discountSupportedModelFactory; protected readonly IDiscountSupportedModelFactory _discountSupportedModelFactory;
protected readonly IGenericAttributeService _genericAttributeService;
protected readonly ILocalizationService _localizationService; protected readonly ILocalizationService _localizationService;
protected readonly ILocalizedModelFactory _localizedModelFactory; protected readonly ILocalizedModelFactory _localizedModelFactory;
protected readonly IManufacturerService _manufacturerService; protected readonly IManufacturerService _manufacturerService;
@ -98,7 +97,6 @@ public partial class ProductModelFactory : IProductModelFactory
IDateTimeHelper dateTimeHelper, IDateTimeHelper dateTimeHelper,
IDiscountService discountService, IDiscountService discountService,
IDiscountSupportedModelFactory discountSupportedModelFactory, IDiscountSupportedModelFactory discountSupportedModelFactory,
IGenericAttributeService genericAttributeService,
ILocalizationService localizationService, ILocalizationService localizationService,
ILocalizedModelFactory localizedModelFactory, ILocalizedModelFactory localizedModelFactory,
IManufacturerService manufacturerService, IManufacturerService manufacturerService,
@ -139,7 +137,6 @@ public partial class ProductModelFactory : IProductModelFactory
_dateTimeHelper = dateTimeHelper; _dateTimeHelper = dateTimeHelper;
_discountService = discountService; _discountService = discountService;
_discountSupportedModelFactory = discountSupportedModelFactory; _discountSupportedModelFactory = discountSupportedModelFactory;
_genericAttributeService = genericAttributeService;
_localizationService = localizationService; _localizationService = localizationService;
_localizedModelFactory = localizedModelFactory; _localizedModelFactory = localizedModelFactory;
_manufacturerService = manufacturerService; _manufacturerService = manufacturerService;
@ -814,18 +811,6 @@ public partial class ProductModelFactory : IProductModelFactory
model.CanCreateCombinations = await (await _productAttributeService model.CanCreateCombinations = await (await _productAttributeService
.GetProductAttributeMappingsByProductIdAsync(product.Id)).AnyAwaitAsync(async pam => (await _productAttributeService.GetProductAttributeValuesAsync(pam.Id)).Any()); .GetProductAttributeMappingsByProductIdAsync(product.Id)).AnyAwaitAsync(async pam => (await _productAttributeService.GetProductAttributeValuesAsync(pam.Id)).Any());
model.HeroEnabled = await _genericAttributeService.GetAttributeAsync<bool>(product, NopProductPrDefaults.HeroEnabledAttribute);
model.HeroTitleLine1 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.HeroTitleLine1Attribute);
model.HeroTitleLine2 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.HeroTitleLine2Attribute);
model.HeroFeatureLine1 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.HeroFeatureLine1Attribute);
model.HeroFeatureLine2 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.HeroFeatureLine2Attribute);
model.HeroPrCopy = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.HeroPrCopyAttribute);
model.HeroTitleLine1FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.HeroTitleLine1FontSizeAttribute);
model.HeroTitleLine2FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.HeroTitleLine2FontSizeAttribute);
model.HeroFeatureLine1FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.HeroFeatureLine1FontSizeAttribute);
model.HeroFeatureLine2FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.HeroFeatureLine2FontSizeAttribute);
model.HeroPrCopyFontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.HeroPrCopyFontSizeAttribute);
if (!excludeProperties) if (!excludeProperties)
{ {
model.SelectedCategoryIds = (await _categoryService.GetProductCategoriesByProductIdAsync(product.Id, true)) model.SelectedCategoryIds = (await _categoryService.GetProductCategoriesByProductIdAsync(product.Id, true))
@ -860,16 +845,6 @@ public partial class ProductModelFactory : IProductModelFactory
locale.MetaDescription = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaDescription, languageId, false, false); locale.MetaDescription = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaDescription, languageId, false, false);
locale.MetaTitle = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaTitle, languageId, false, false); locale.MetaTitle = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaTitle, languageId, false, false);
locale.SeName = await _urlRecordService.GetSeNameAsync(product, languageId, false, false); locale.SeName = await _urlRecordService.GetSeNameAsync(product, languageId, false, false);
locale.HeroTitleLine1 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine1Attribute, languageId));
locale.HeroTitleLine2 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine2Attribute, languageId));
locale.HeroFeatureLine1 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine1Attribute, languageId));
locale.HeroFeatureLine2 = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine2Attribute, languageId));
locale.HeroPrCopy = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroPrCopyAttribute, languageId));
locale.HeroTitleLine1FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine1FontSizeAttribute, languageId));
locale.HeroTitleLine2FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroTitleLine2FontSizeAttribute, languageId));
locale.HeroFeatureLine1FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine1FontSizeAttribute, languageId));
locale.HeroFeatureLine2FontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroFeatureLine2FontSizeAttribute, languageId));
locale.HeroPrCopyFontSize = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(NopProductPrDefaults.HeroPrCopyFontSizeAttribute, languageId));
}; };
} }
@ -2430,4 +2405,4 @@ public partial class ProductModelFactory : IProductModelFactory
} }
#endregion #endregion
} }

View File

@ -1,23 +0,0 @@
using Nop.Web.Framework.Models;
namespace Nop.Web.Areas.Admin.Models.Catalog;
public partial record HomepageHeroProductModel : BaseNopEntityModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public bool IsEnabled { get; set; }
public int DisplayOrder { get; set; }
}
public partial record HomepageHeroProductListModel : BaseNopModel
{
public HomepageHeroProductListModel()
{
Items = new List<HomepageHeroProductModel>();
}
public IList<HomepageHeroProductModel> Items { get; set; }
public int AddProductId { get; set; }
}

View File

@ -113,24 +113,6 @@ public partial record ProductModel : BaseNopEntityModel,
[NopResourceDisplayName("Admin.Catalog.Products.Fields.ShowOnHomepage")] [NopResourceDisplayName("Admin.Catalog.Products.Fields.ShowOnHomepage")]
public bool ShowOnHomepage { get; set; } public bool ShowOnHomepage { get; set; }
//hardware PR
public bool HeroEnabled { get; set; }
public string HeroTitleLine1 { get; set; }
public string HeroTitleLine2 { get; set; }
public string HeroFeatureLine1 { get; set; }
public string HeroFeatureLine2 { get; set; }
public string HeroPrCopy { get; set; }
[UIHint("Int32Nullable")]
public int? HeroTitleLine1FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroTitleLine2FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroFeatureLine1FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroFeatureLine2FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroPrCopyFontSize { get; set; }
[NopResourceDisplayName("Admin.Catalog.Products.Fields.MetaKeywords")] [NopResourceDisplayName("Admin.Catalog.Products.Fields.MetaKeywords")]
public string MetaKeywords { get; set; } public string MetaKeywords { get; set; }
@ -523,21 +505,4 @@ public partial record ProductLocalizedModel : ILocalizedLocaleModel
[NopResourceDisplayName("Admin.Catalog.Products.Fields.SeName")] [NopResourceDisplayName("Admin.Catalog.Products.Fields.SeName")]
public string SeName { get; set; } public string SeName { get; set; }
}
//hardware PR
public string HeroTitleLine1 { get; set; }
public string HeroTitleLine2 { get; set; }
public string HeroFeatureLine1 { get; set; }
public string HeroFeatureLine2 { get; set; }
public string HeroPrCopy { get; set; }
[UIHint("Int32Nullable")]
public int? HeroTitleLine1FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroTitleLine2FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroFeatureLine1FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroFeatureLine2FontSize { get; set; }
[UIHint("Int32Nullable")]
public int? HeroPrCopyFontSize { get; set; }
}

View File

@ -1,79 +0,0 @@
@model HomepageHeroProductListModel
@{
ViewBag.PageTitle = "首頁輪播商品";
NopHtml.SetActiveMenuItemSystemName("Products");
}
<form asp-action="HomepageHeroUpdate" method="post">
<section class="content">
<div class="container-fluid">
<div class="card card-default">
<div class="card-header">
<h3 class="card-title">新增輪播商品</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label asp-for="AddProductId">商品 ID</label>
</div>
<div class="col-md-6">
<nop-editor asp-for="AddProductId" />
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary" formaction="@Url.Action("HomepageHeroAdd", "Product")">加入</button>
</div>
</div>
</div>
</div>
<div class="card card-default">
<div class="card-header">
<h3 class="card-title">輪播清單</h3>
</div>
<div class="card-body p-0">
<table class="table table-bordered table-striped mb-0">
<thead>
<tr>
<th style="width:80px;">ID</th>
<th>商品</th>
<th style="width:120px;">啟用</th>
<th style="width:140px;">順序</th>
<th style="width:120px;">操作</th>
</tr>
</thead>
<tbody>
@for (var i = 0; i < Model.Items.Count; i++)
{
<tr>
<td>
@Model.Items[i].ProductId
<input type="hidden" asp-for="@Model.Items[i].Id" />
<input type="hidden" asp-for="@Model.Items[i].ProductId" />
<input type="hidden" asp-for="@Model.Items[i].ProductName" />
</td>
<td>
<a asp-controller="Product" asp-action="Edit" asp-route-id="@Model.Items[i].ProductId">@Model.Items[i].ProductName</a>
</td>
<td>
<nop-editor asp-for="@Model.Items[i].IsEnabled" />
</td>
<td>
<nop-editor asp-for="@Model.Items[i].DisplayOrder" />
</td>
<td>
<button type="submit" class="btn btn-danger btn-sm" formaction="@Url.Action("HomepageHeroDelete", "Product", new { id = Model.Items[i].Id })">刪除</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">儲存排序/啟用狀態</button>
</div>
</div>
</div>
</section>
</form>

View File

@ -26,10 +26,6 @@
<i class="fas fa-plus-square"></i> <i class="fas fa-plus-square"></i>
@T("Admin.Common.AddNew") @T("Admin.Common.AddNew")
</a> </a>
<a asp-action="HomepageHero" class="btn btn-info">
<i class="fas fa-images"></i>
首頁輪播
</a>
<button asp-action="DownloadCatalogPDF" type="submit" name="download-catalog-pdf" class="btn bg-purple"> <button asp-action="DownloadCatalogPDF" type="submit" name="download-catalog-pdf" class="btn bg-purple">
<i class="far fa-file-pdf"></i> <i class="far fa-file-pdf"></i>
@T("Admin.Catalog.Products.List.DownloadPDF") @T("Admin.Catalog.Products.List.DownloadPDF")
@ -467,4 +463,4 @@
}); });
}); });
</script> </script>
<nop-alert asp-alert-id="exportExcelSelected" /> <nop-alert asp-alert-id="exportExcelSelected" />

View File

@ -1,101 +0,0 @@
@model ProductModel
<div class="card-body">
<div class="cards-group">
<div class="form-group row">
<div class="col-md-3">
<label for="@Html.IdFor(model => model.HeroEnabled)">啟用硬體 PR 區塊</label>
</div>
<div class="col-md-9">
<nop-editor asp-for="HeroEnabled" />
</div>
</div>
@(await Html.LocalizedEditorAsync<ProductModel, ProductLocalizedModel>("product-hardware-pr-localized",
@<div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroTitleLine1)">左上標題(第一行)</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroTitleLine1" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroTitleLine2)">左上標題(第二行)</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroTitleLine2" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroFeatureLine1)">右上特性(第一行)</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroFeatureLine1" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroFeatureLine2)">右上特性(第二行)</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroFeatureLine2" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroPrCopy)">右下 PR 文案</label></div>
<div class="col-md-9"><nop-textarea asp-for="@Model.Locales[item].HeroPrCopy"></nop-textarea></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroTitleLine1FontSize)">左上第一行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroTitleLine1FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroTitleLine2FontSize)">左上第二行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroTitleLine2FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroFeatureLine1FontSize)">右上第一行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroFeatureLine1FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroFeatureLine2FontSize)">右上第二行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroFeatureLine2FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.Locales[item].HeroPrCopyFontSize)">PR 文案字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="@Model.Locales[item].HeroPrCopyFontSize" /></div>
</div>
<input type="hidden" asp-for="@Model.Locales[item].LanguageId" />
</div>,
@<div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroTitleLine1)">左上標題(第一行)</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroTitleLine1" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroTitleLine2)">左上標題(第二行)</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroTitleLine2" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroFeatureLine1)">右上特性(第一行)</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroFeatureLine1" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroFeatureLine2)">右上特性(第二行)</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroFeatureLine2" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroPrCopy)">右下 PR 文案</label></div>
<div class="col-md-9"><nop-textarea asp-for="HeroPrCopy"></nop-textarea></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroTitleLine1FontSize)">左上第一行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroTitleLine1FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroTitleLine2FontSize)">左上第二行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroTitleLine2FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroFeatureLine1FontSize)">右上第一行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroFeatureLine1FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroFeatureLine2FontSize)">右上第二行字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroFeatureLine2FontSize" /></div>
</div>
<div class="form-group row">
<div class="col-md-3"><label for="@Html.IdFor(model => model.HeroPrCopyFontSize)">PR 文案字級px</label></div>
<div class="col-md-9"><nop-editor asp-for="HeroPrCopyFontSize" /></div>
</div>
</div>))
</div>
</div>

View File

@ -80,7 +80,6 @@
<nop-card asp-name="product-shipping" asp-icon="fas fa-truck" asp-title="@T("Admin.Catalog.Products.Shipping")" asp-hide-block-attribute-name="@hideShippingBlockAttributeName" asp-hide="@hideShippingBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Shipping", Model)</nop-card> <nop-card asp-name="product-shipping" asp-icon="fas fa-truck" asp-title="@T("Admin.Catalog.Products.Shipping")" asp-hide-block-attribute-name="@hideShippingBlockAttributeName" asp-hide="@hideShippingBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Shipping", Model)</nop-card>
<nop-card asp-name="product-inventory" asp-icon="fas fa-sitemap" asp-title="@T("Admin.Catalog.Products.Inventory")" asp-hide-block-attribute-name="@hideInventoryBlockAttributeName" asp-hide="@hideInventoryBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Inventory", Model)</nop-card> <nop-card asp-name="product-inventory" asp-icon="fas fa-sitemap" asp-title="@T("Admin.Catalog.Products.Inventory")" asp-hide-block-attribute-name="@hideInventoryBlockAttributeName" asp-hide="@hideInventoryBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Inventory", Model)</nop-card>
<nop-card asp-name="product-multimedia" asp-icon="fas fa-photo-video" asp-title="@T("Admin.Catalog.Products.Multimedia")" asp-hide-block-attribute-name="@hideMultimediaBlockAttributeName" asp-hide="@hideMultimediaBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Multimedia", Model)</nop-card> <nop-card asp-name="product-multimedia" asp-icon="fas fa-photo-video" asp-title="@T("Admin.Catalog.Products.Multimedia")" asp-hide-block-attribute-name="@hideMultimediaBlockAttributeName" asp-hide="@hideMultimediaBlock" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.Multimedia", Model)</nop-card>
<nop-card asp-name="product-hardware-pr" asp-icon="fas fa-bullhorn" asp-title="硬體 PR 區塊" asp-hide-block-attribute-name="ProductPage.HideHardwarePrBlock" asp-hide="false" asp-advanced="false">@await Html.PartialAsync("_CreateOrUpdate.HardwarePr", Model)</nop-card>
<nop-card asp-name="product-product-attributes" asp-icon="fas fa-paperclip" asp-title="@T("Admin.Catalog.Products.ProductAttributes")" asp-hide-block-attribute-name="@hideProductAttributesBlockAttributeName" asp-hide="@hideProductAttributesBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.ProductAttributes)">@await Html.PartialAsync("_CreateOrUpdate.ProductAttributes", Model)</nop-card> <nop-card asp-name="product-product-attributes" asp-icon="fas fa-paperclip" asp-title="@T("Admin.Catalog.Products.ProductAttributes")" asp-hide-block-attribute-name="@hideProductAttributesBlockAttributeName" asp-hide="@hideProductAttributesBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.ProductAttributes)">@await Html.PartialAsync("_CreateOrUpdate.ProductAttributes", Model)</nop-card>
<nop-card asp-name="product-specification-attributes" asp-icon="fas fa-cog" asp-title="@T("Admin.Catalog.Attributes.SpecificationAttributes")" asp-hide-block-attribute-name="@hideSpecificationAttributeBlockAttributeName" asp-hide="@hideSpecificationAttributeBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.SpecificationAttributes)">@await Html.PartialAsync("_CreateOrUpdate.SpecificationAttributes", Model)</nop-card> <nop-card asp-name="product-specification-attributes" asp-icon="fas fa-cog" asp-title="@T("Admin.Catalog.Attributes.SpecificationAttributes")" asp-hide-block-attribute-name="@hideSpecificationAttributeBlockAttributeName" asp-hide="@hideSpecificationAttributeBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.SpecificationAttributes)">@await Html.PartialAsync("_CreateOrUpdate.SpecificationAttributes", Model)</nop-card>
<nop-card asp-name="product-giftcard" asp-icon="fas fa-gift" asp-title="@T("Admin.Catalog.Products.GiftCard")" asp-hide-block-attribute-name="@hideGiftCardBlockAttributeName" asp-hide="@hideGiftCardBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.IsGiftCard)">@await Html.PartialAsync("_CreateOrUpdate.GiftCard", Model)</nop-card> <nop-card asp-name="product-giftcard" asp-icon="fas fa-gift" asp-title="@T("Admin.Catalog.Products.GiftCard")" asp-hide-block-attribute-name="@hideGiftCardBlockAttributeName" asp-hide="@hideGiftCardBlock" asp-advanced="@(!Model.ProductEditorSettingsModel.IsGiftCard)">@await Html.PartialAsync("_CreateOrUpdate.GiftCard", Model)</nop-card>
@ -101,4 +100,4 @@
</nop-cards> </nop-cards>
</div> </div>
</div> </div>
</section> </section>

View File

@ -1,92 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Core;
using Nop.Services.Security;
using Nop.Services.Stores;
using Nop.Core.Domain.Catalog;
using Nop.Services.Catalog;
using Nop.Web.Factories;
using Nop.Web.Framework.Components;
namespace Nop.Web.Components;
public partial class HomepageHeroProductsViewComponent : NopViewComponent
{
protected readonly IHomepageHeroProductService _homepageHeroProductService;
protected readonly IAclService _aclService;
protected readonly IProductService _productService;
protected readonly IProductModelFactory _productModelFactory;
protected readonly IStoreMappingService _storeMappingService;
protected readonly IStoreContext _storeContext;
public HomepageHeroProductsViewComponent(
IHomepageHeroProductService homepageHeroProductService,
IAclService aclService,
IProductService productService,
IProductModelFactory productModelFactory,
IStoreMappingService storeMappingService,
IStoreContext storeContext)
{
_homepageHeroProductService = homepageHeroProductService;
_aclService = aclService;
_productService = productService;
_productModelFactory = productModelFactory;
_storeMappingService = storeMappingService;
_storeContext = storeContext;
}
public virtual async Task<IViewComponentResult> InvokeAsync()
{
var mappings = await _homepageHeroProductService.GetAllAsync(onlyEnabled: true);
var models = new List<Models.Catalog.ProductDetailsModel>();
foreach (var mapping in mappings.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Id))
{
var product = await _productService.GetProductByIdAsync(mapping.ProductId);
if (product == null || product.Deleted || !product.Published)
continue;
if (!await _aclService.AuthorizeAsync(product) || !await _storeMappingService.AuthorizeAsync(product))
continue;
if (!_productService.ProductIsAvailable(product))
continue;
var detailsModel = await _productModelFactory.PrepareProductDetailsModelAsync(product);
if (!detailsModel.HeroEnabled)
continue;
models.Add(detailsModel);
}
// fallback: if no explicit homepage hero mapping, use products that have HeroEnabled.
if (!models.Any())
{
var store = await _storeContext.GetCurrentStoreAsync();
var candidates = await _productService.SearchProductsAsync(
pageIndex: 0,
pageSize: 50,
storeId: store.Id,
visibleIndividuallyOnly: true,
showHidden: false,
orderBy: ProductSortingEnum.Position);
foreach (var product in candidates)
{
if (product == null || product.Deleted || !product.Published)
continue;
if (!await _aclService.AuthorizeAsync(product) || !await _storeMappingService.AuthorizeAsync(product))
continue;
if (!_productService.ProductIsAvailable(product))
continue;
var detailsModel = await _productModelFactory.PrepareProductDetailsModelAsync(product);
if (!detailsModel.HeroEnabled)
continue;
models.Add(detailsModel);
if (models.Count >= 4)
break;
}
}
return View(models);
}
}

View File

@ -1379,9 +1379,6 @@ public partial class ProductModelFactory : IProductModelFactory
SeName = await _urlRecordService.GetSeNameAsync(product), SeName = await _urlRecordService.GetSeNameAsync(product),
Sku = product.Sku, Sku = product.Sku,
ProductType = product.ProductType, ProductType = product.ProductType,
StockQuantity = product.StockQuantity,
MinStockQuantity = product.MinStockQuantity,
NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow,
MarkAsNew = product.MarkAsNew && MarkAsNew = product.MarkAsNew &&
(!product.MarkAsNewStartDateTimeUtc.HasValue || product.MarkAsNewStartDateTimeUtc.Value < DateTime.UtcNow) && (!product.MarkAsNewStartDateTimeUtc.HasValue || product.MarkAsNewStartDateTimeUtc.Value < DateTime.UtcNow) &&
(!product.MarkAsNewEndDateTimeUtc.HasValue || product.MarkAsNewEndDateTimeUtc.Value > DateTime.UtcNow) (!product.MarkAsNewEndDateTimeUtc.HasValue || product.MarkAsNewEndDateTimeUtc.Value > DateTime.UtcNow)
@ -1472,24 +1469,6 @@ public partial class ProductModelFactory : IProductModelFactory
return result; return result;
} }
protected virtual async Task<string> GetLocalizedHeroValueAsync(Product product, string key, int languageId)
{
var localized = await _genericAttributeService.GetAttributeAsync<string>(product, NopProductPrDefaults.GetLocalizedKey(key, languageId));
if (!string.IsNullOrWhiteSpace(localized))
return localized;
return await _genericAttributeService.GetAttributeAsync<string>(product, key);
}
protected virtual async Task<int?> GetLocalizedHeroFontSizeAsync(Product product, string key, int languageId)
{
var localized = await _genericAttributeService.GetAttributeAsync<int?>(product, NopProductPrDefaults.GetLocalizedKey(key, languageId));
if (localized.HasValue)
return localized;
return await _genericAttributeService.GetAttributeAsync<int?>(product, key);
}
/// <summary> /// <summary>
/// Prepare the product details model /// Prepare the product details model
/// </summary> /// </summary>
@ -1745,19 +1724,6 @@ public partial class ProductModelFactory : IProductModelFactory
model.JsonLd = JsonConvert.SerializeObject(jsonLdModel, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); model.JsonLd = JsonConvert.SerializeObject(jsonLdModel, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
} }
var languageId = (await _workContext.GetWorkingLanguageAsync()).Id;
model.HeroEnabled = await _genericAttributeService.GetAttributeAsync<bool>(product, NopProductPrDefaults.HeroEnabledAttribute);
model.HeroTitleLine1 = await GetLocalizedHeroValueAsync(product, NopProductPrDefaults.HeroTitleLine1Attribute, languageId);
model.HeroTitleLine2 = await GetLocalizedHeroValueAsync(product, NopProductPrDefaults.HeroTitleLine2Attribute, languageId);
model.HeroFeatureLine1 = await GetLocalizedHeroValueAsync(product, NopProductPrDefaults.HeroFeatureLine1Attribute, languageId);
model.HeroFeatureLine2 = await GetLocalizedHeroValueAsync(product, NopProductPrDefaults.HeroFeatureLine2Attribute, languageId);
model.HeroPrCopy = await GetLocalizedHeroValueAsync(product, NopProductPrDefaults.HeroPrCopyAttribute, languageId);
model.HeroTitleLine1FontSize = await GetLocalizedHeroFontSizeAsync(product, NopProductPrDefaults.HeroTitleLine1FontSizeAttribute, languageId);
model.HeroTitleLine2FontSize = await GetLocalizedHeroFontSizeAsync(product, NopProductPrDefaults.HeroTitleLine2FontSizeAttribute, languageId);
model.HeroFeatureLine1FontSize = await GetLocalizedHeroFontSizeAsync(product, NopProductPrDefaults.HeroFeatureLine1FontSizeAttribute, languageId);
model.HeroFeatureLine2FontSize = await GetLocalizedHeroFontSizeAsync(product, NopProductPrDefaults.HeroFeatureLine2FontSizeAttribute, languageId);
model.HeroPrCopyFontSize = await GetLocalizedHeroFontSizeAsync(product, NopProductPrDefaults.HeroPrCopyFontSizeAttribute, languageId);
return model; return model;
} }
@ -2044,4 +2010,4 @@ public partial class ProductModelFactory : IProductModelFactory
} }
#endregion #endregion
} }

View File

@ -125,19 +125,6 @@ public partial record ProductDetailsModel : BaseNopEntityModel
public bool AllowAddingOnlyExistingAttributeCombinations { get; set; } public bool AllowAddingOnlyExistingAttributeCombinations { get; set; }
//hardware PR
public bool HeroEnabled { get; set; }
public string HeroTitleLine1 { get; set; }
public string HeroTitleLine2 { get; set; }
public string HeroFeatureLine1 { get; set; }
public string HeroFeatureLine2 { get; set; }
public string HeroPrCopy { get; set; }
public int? HeroTitleLine1FontSize { get; set; }
public int? HeroTitleLine2FontSize { get; set; }
public int? HeroFeatureLine1FontSize { get; set; }
public int? HeroFeatureLine2FontSize { get; set; }
public int? HeroPrCopyFontSize { get; set; }
#region Nested Classes #region Nested Classes
public partial record ProductBreadcrumbModel : BaseNopModel public partial record ProductBreadcrumbModel : BaseNopModel
@ -351,4 +338,4 @@ public partial record ProductDetailsModel : BaseNopEntityModel
} }
#endregion #endregion
} }

View File

@ -27,9 +27,6 @@ public partial record ProductOverviewModel : BaseNopEntityModel
public ProductType ProductType { get; set; } public ProductType ProductType { get; set; }
public bool MarkAsNew { get; set; } public bool MarkAsNew { get; set; }
public int StockQuantity { get; set; }
public int MinStockQuantity { get; set; }
public int NotifyAdminForQuantityBelow { get; set; }
//price //price
public ProductPriceModel ProductPrice { get; set; } public ProductPriceModel ProductPrice { get; set; }
@ -72,4 +69,4 @@ public partial record ProductOverviewModel : BaseNopEntityModel
} }
#endregion #endregion
} }

View File

@ -1,555 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Alata&display=swap');
.html-home-page .header,
.html-home-page .header-menu,
.html-home-page .footer,
.html-home-page .side-2 {
display: none;
}
.home-page .master-wrapper-content {
width: 1280px;
max-width: 1280px;
margin: 0 auto;
}
.kneo-home {
width: 1280px;
margin: 0 auto;
padding: 0 0 40px;
}
.kneo-home-header,
.kneo-home-header * {
font-family: "Alata", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
}
.kneo-home-header {
position: relative;
height: 96px;
background: linear-gradient(90deg, #0c3b84 0%, #2f97d4 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 56px 0 54px;
margin-top: 16px;
overflow: hidden;
}
.kneo-home-header::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.45);
box-shadow: 6px 6px 12px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
.kneo-home-header__brand {
position: relative;
z-index: 1;
font-size: 36px;
font-weight: 400;
line-height: 1;
color: #07111a;
letter-spacing: 0.8px;
}
.kneo-home-header__nav {
position: relative;
z-index: 1;
display: flex;
gap: 42px;
}
.kneo-home-header__nav a {
color: #07111a;
font-size: 16px;
font-weight: 400;
line-height: 1;
letter-spacing: 0.8px;
white-space: nowrap;
}
.kneo-home__section {
}
.kneo-home__section--hero {
position: relative;
margin-bottom: 0;
background: #ffffff;
overflow: hidden;
padding: 20px 20px 0 20px;
}
.kneo-home__section--hero::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 55%;
background: linear-gradient(90deg, #0c3b84 0%, #2f97d4 100%);
z-index: 0;
}
.kneo-home__section-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 4px 10px;
margin-bottom: 8px;
}
.kneo-home__section-head h2 {
margin: 0;
font-size: 20px;
font-weight: 500;
letter-spacing: 6px;
display: inline-flex;
align-items: center;
gap: 20px;
}
.kneo-home__section-head h2::before,
.kneo-home__section-head h2::after {
content: "";
display: inline-block;
width: 220px;
height: 10px;
color: currentColor;
}
.kneo-home__section-head h2::before {
background:
linear-gradient(currentColor, currentColor) left center / 200px 1px no-repeat,
radial-gradient(circle at center, currentColor 0 5px, transparent 5px) right center / 10px 10px no-repeat;
}
.kneo-home__section-head h2::after {
background:
radial-gradient(circle at center, currentColor 0 5px, transparent 5px) left center / 10px 10px no-repeat,
linear-gradient(currentColor, currentColor) right center / 200px 1px no-repeat;
}
.kneo-home__section-head a,
.kneo-home__section-tail a {
color: #2f4150;
font-size: 12px;
letter-spacing: 1px;
}
.homepage-hero-products {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/hero/hero-background.png');
background-repeat: no-repeat;
background-size: 955px 632px;
min-height: 600px;
padding: 0 40px 30px;
border-radius: 0;
position: relative;
overflow: hidden;
z-index: 1;
}
.homepage-hero-products::before {
content: none;
}
.homepage-hero-products > * {
position: relative;
z-index: 1;
}
.homepage-hero-products--empty {
color: #fff;
}
.homepage-hero-products__empty-title {
font-size: 92px;
font-weight: 600;
line-height: 1;
}
.homepage-hero-products__empty-subtitle {
font-size: 72px;
line-height: 1.1;
margin-top: 10px;
}
.homepage-hero-products__empty-copy {
font-size: 28px;
line-height: 1.4;
margin-top: 26px;
max-width: 760px;
}
.homepage-hero-products__dots {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 14px;
}
.homepage-hero-products__dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.45);
}
.homepage-hero-products__dot.is-active {
background: #ffffff;
}
.kneo-home__section--hardware {
background: url('/Themes/DefaultClean/Kneo/Content/images/home/hw-back-band.png') no-repeat top center;
background-size: 1228px 377px;
min-height: 300px;
padding: 44px 48px 24px 70px;
}
.kneo-home__section--book {
background: url('/Themes/DefaultClean/Kneo/Content/images/home/book-back-band.png') no-repeat top center;
background-size: 1190px 360px;
min-height: 360px;
padding: 24px 48px 24px 70px;
}
.kneo-home__section--application {
background: url('/Themes/DefaultClean/Kneo/Content/images/home/app-back-band.png') no-repeat top center;
background-size: 1200px 328px;
min-height: 328px;
padding: 24px 48px 24px 70px;
}
.kneo-home__hardware-body .product-grid {
}
.kneo-home__hardware-body .item-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.kneo-home-hw-card {
position: relative;
display: flex;
flex-direction: column;
width: 256px;
height: 256px;
background: url('/Themes/DefaultClean/Kneo/Content/images/common/hw-product-back.png') no-repeat center top;
background-size: 100% 100%;
padding: 24px 30px;
}
.kneo-home-hw-card:hover {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/hw-product-back-hover.png');
}
.kneo-home-hw-card__hit {
position: absolute;
inset: 0;
z-index: 1;
}
.kneo-home-hw-card__stock-icon {
width: 81px;
height: 28px;
position: relative;
z-index: 3;
margin-left: auto;
background-repeat: no-repeat;
background-size: contain;
}
.kneo-home-hw-card.in-stock .kneo-home-hw-card__stock-icon {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/in-stock.png');
}
.kneo-home-hw-card.low-stock .kneo-home-hw-card__stock-icon {
width: 98px;
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/low-stock.png');
}
.kneo-home-hw-card.out-stock .kneo-home-hw-card__stock-icon {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/out-stock.png');
}
.kneo-home-hw-card.in-stock:hover .kneo-home-hw-card__stock-icon {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/in-stock-hover.png');
}
.kneo-home-hw-card.low-stock:hover .kneo-home-hw-card__stock-icon {
width: 98px;
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/low-stock-hover.png');
}
.kneo-home-hw-card.out-stock:hover .kneo-home-hw-card__stock-icon {
background-image: url('/Themes/DefaultClean/Kneo/Content/images/common/out-stock-hover.png');
}
.kneo-home-hw-card__picture {
height: 172px;
width: 194px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 2;
}
.kneo-home-hw-card__picture img {
max-width: 172px;
max-height: 172px;
object-fit: contain;
}
.kneo-home-hw-card__text {
color: #ffffff;
z-index: 3;
display: grid;
grid-template-columns: 96px 1fr;
column-gap: 6px;
row-gap: 2px;
margin-top: auto;
}
.kneo-home-hw-card:hover .kneo-home-hw-card__text {
color: #333333;
}
.kneo-home-hw-card__price {
font-size: 12px;
line-height: 1.2;
grid-column: 1;
grid-row: 1;
align-self: start;
letter-spacing: 2.4px;
}
.kneo-home-hw-card__tag {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
background: #ffffff;
color: #000000;
width: fit-content;
min-width: 81px;
height: 25px;
padding: 2px 6px;
grid-column: 1;
grid-row: 2;
align-self: end;
letter-spacing: 2.4px;
}
.kneo-home-hw-card__name {
margin: 0;
min-height: 0;
font-size: 16px;
line-height: 1.2;
grid-column: 2;
grid-row: 1 / span 2;
align-self: end;
max-height: calc(1.2em * 2);
overflow: hidden;
display: -webkit-box;
margin-bottom: 2px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.kneo-home-hw-card__name a {
}
.kneo-home__hardware-body .product-grid {
margin-bottom: 36px;
}
.kneo-home__section-tail {
display: flex;
justify-content: flex-end;
padding-right: 108px;
}
.kneo-home__tail-link {
display: inline-flex;
align-items: center;
gap: 12px;
white-space: nowrap;
}
.kneo-home__tail-link span {
display: inline-block;
}
.kneo-home__section--book .kneo-home__section-head h2 {
color: #00a4d7;
}
.kneo-home__book-body {
height: 286px;
margin-bottom: 12px;
}
.kneo-home__placeholder {
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #5f6e7a;
background: rgba(255, 255, 255, 0.55);
}
.kneo-home__section--application {
margin-top: -36px;
}
.kneo-home__section--application .kneo-home__section-head h2 {
color: #16404f;
}
.kneo-home__apps {
width: 1140px;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
padding-left: 12px;
margin-bottom: 22px;
}
.kneo-home__app {
position: relative;
width: 170px;
height: 184px;
display: block;
}
.kneo-home__app-img {
position: absolute;
inset: 0;
width: 170px;
height: 184px;
display: block;
}
.kneo-home__app-img--default {
opacity: 1;
}
.kneo-home__app-img--hover {
opacity: 0;
}
.kneo-home__app:hover .kneo-home__app-img--default {
opacity: 0;
}
.kneo-home__app:hover .kneo-home__app-img--hover {
opacity: 1;
}
.kneo-home__section--footer {
min-height: 120px;
}
.home-page .kneo-product-hero-pr__main {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
grid-template-rows: auto auto;
column-gap: 26px;
row-gap: 20px;
align-items: start;
}
.home-page .kneo-product-hero-pr__title {
grid-column: 1;
grid-row: 1;
color: #ffffff;
padding-top: 6px;
}
.home-page .kneo-product-hero-pr__line1 {
font-size: 96px;
line-height: 1;
font-weight: 600;
}
.home-page .kneo-product-hero-pr__line2 {
font-size: 82px;
line-height: 1.06;
margin-top: 10px;
font-weight: 500;
}
.home-page .kneo-product-hero-pr__picture {
grid-column: 1;
grid-row: 2;
background: transparent;
border-radius: 0;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin-top: 28px;
margin-left: -22px;
}
.home-page .kneo-product-hero-pr__picture img {
max-width: 108%;
max-height: 370px;
object-fit: contain;
}
.home-page .kneo-product-hero-pr__feature {
position: absolute;
right: 78px;
top: 40px;
background: url('/Themes/DefaultClean/Kneo/Content/images/hero/hero-effecient.png') no-repeat center;
background-size: cover;
border-radius: 20px;
width: 292px;
min-height: 275px;
color: #ffffff;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
padding: 0 20px;
z-index: 2;
}
.home-page .kneo-product-hero-pr__feature-line1 {
font-size: 82px;
line-height: 0.95;
font-weight: 600;
}
.home-page .kneo-product-hero-pr__feature-line2 {
font-size: 58px;
line-height: 1.05;
font-weight: 500;
}
.home-page .kneo-product-hero-pr__copy {
position: absolute;
right: 8px;
top: 364px;
font-size: 24px;
line-height: 2;
letter-spacing: 2.5px;
color: #183041;
padding: 0;
width: 243px;
word-break: break-word;
writing-mode: horizontal-tb;
text-orientation: mixed;
}
.home-page .kneo-product-hero-pr__copy-more summary {
cursor: pointer;
color: #0f5e9a;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -12,7 +12,6 @@
//add main CSS file //add main CSS file
NopHtml.AppendCssFileParts($"~/Themes/{themeName}/Content/css/styles{(supportRtl ? ".rtl" : "")}.css"); NopHtml.AppendCssFileParts($"~/Themes/{themeName}/Content/css/styles{(supportRtl ? ".rtl" : "")}.css");
NopHtml.AppendCssFileParts($"~/Themes/{themeName}/Kneo/Content/css/home.css");
//add swiper css file //add swiper css file
if (catalogSettings.DisplayAllPicturesOnCatalogPages) if (catalogSettings.DisplayAllPicturesOnCatalogPages)
@ -22,4 +21,4 @@
//add jQuery UI css file //add jQuery UI css file
NopHtml.AppendCssFileParts("~/lib_npm/jquery-ui-dist/jquery-ui.min.css"); NopHtml.AppendCssFileParts("~/lib_npm/jquery-ui-dist/jquery-ui.min.css");
} }

View File

@ -28,119 +28,18 @@
} }
<div class="page home-page"> <div class="page home-page">
<div class="page-body"> <div class="page-body">
<section class="kneo-home kneo-home--1280"> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageTop })
<div class="kneo-home-header"> @await Component.InvokeAsync(typeof(TopicBlockViewComponent), new { systemName = "HomepageText" })
<div class="kneo-home-header__brand">KNEO PLATFORM</div> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeCategories })
<nav class="kneo-home-header__nav"> @await Component.InvokeAsync(typeof(HomepageCategoriesViewComponent))
<a href="@Url.RouteUrl("Category", new { SeName = "hardware" })">硬體</a> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeProducts })
<a href="@Url.RouteUrl("Category", new { SeName = "software" })">軟體</a> @await Component.InvokeAsync(typeof(HomepageProductsViewComponent))
<a href="@Url.RouteUrl("Category", new { SeName = "ai-application" })">AI應用範圍</a> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeBestSellers })
<a href="@Url.RouteUrl("Category", new { SeName = "books" })">書城</a> @await Component.InvokeAsync(typeof(HomepageBestSellersViewComponent))
<a href="@Url.RouteUrl("KneronLogin")">會員登入</a> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeNews })
</nav> @await Component.InvokeAsync(typeof(HomepageNewsViewComponent))
</div> @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforePoll })
@await Component.InvokeAsync(typeof(HomepagePollsViewComponent))
<div class="kneo-home__section kneo-home__section--hero">
@await Component.InvokeAsync(typeof(HomepageHeroProductsViewComponent))
</div>
<div class="kneo-home__section kneo-home__section--hardware">
<div class="kneo-home__hardware-body">
@await Component.InvokeAsync(typeof(HomepageProductsViewComponent))
</div>
<div class="kneo-home__section-tail">
<a class="kneo-home__tail-link" href="/hardware">
<svg width="9" height="11" viewBox="0 0 9 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7_3)">
<path d="M0.10614 10.8135L8.60081 5.32684L0.10614 0.186401" stroke="#1A1A1A" stroke-width="0.8" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_7_3">
<rect width="9" height="11" fill="white"/>
</clipPath>
</defs>
</svg>
<span>SEE ALL SUPPLIMENTS</span>
</a>
</div>
</div>
<div class="kneo-home__section kneo-home__section--book">
<div class="kneo-home__section-head">
<h2>推薦書籍</h2>
</div>
<div class="kneo-home__book-body">
<div class="kneo-home__placeholder">書城輪播區(待接書籍首頁資料)</div>
</div>
<div class="kneo-home__section-tail">
<a class="kneo-home__tail-link" href="/books">
<svg width="9" height="11" viewBox="0 0 9 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7_3)">
<path d="M0.10614 10.8135L8.60081 5.32684L0.10614 0.186401" stroke="#1A1A1A" stroke-width="0.8" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_7_3">
<rect width="9" height="11" fill="white"/>
</clipPath>
</defs>
</svg>
<span>SEE ALL BOOKS</span>
</a>
</div>
</div>
<div class="kneo-home__section kneo-home__section--application">
<div class="kneo-home__section-head">
<h2>應用領域</h2>
</div>
<div class="kneo-home__apps">
<a class="kneo-home__app" href="#" aria-label="Smart Retail">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-retail.png" alt="Smart Retail" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-retail-hover.png" alt="" aria-hidden="true" />
</a>
<a class="kneo-home__app" href="#" aria-label="Medical & Health">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/medical-health.png" alt="Medical & Health" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/medical-health-hover.png" alt="" aria-hidden="true" />
</a>
<a class="kneo-home__app" href="#" aria-label="Vehicle & Transportation">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/vehicle-transportation.png" alt="Vehicle & Transportation" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/vehicle-transportation-hover.png" alt="" aria-hidden="true" />
</a>
<a class="kneo-home__app" href="#" aria-label="Smart Manufacture">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-manufacture.png" alt="Smart Manufacture" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-manufacture-hover.png" alt="" aria-hidden="true" />
</a>
<a class="kneo-home__app" href="#" aria-label="Smart Home">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-home.png" alt="Smart Home" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/smart-home-hover.png" alt="" aria-hidden="true" />
</a>
<a class="kneo-home__app" href="#" aria-label="Future City">
<img class="kneo-home__app-img kneo-home__app-img--default" src="~/Themes/DefaultClean/Kneo/Content/images/home/future-city.png" alt="Future City" />
<img class="kneo-home__app-img kneo-home__app-img--hover" src="~/Themes/DefaultClean/Kneo/Content/images/home/future-city-hover.png" alt="" aria-hidden="true" />
</a>
</div>
<div class="kneo-home__section-tail">
<a class="kneo-home__tail-link" href="/ai-application">
<svg width="9" height="11" viewBox="0 0 9 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7_3)">
<path d="M0.10614 10.8135L8.60081 5.32684L0.10614 0.186401" stroke="#1A1A1A" stroke-width="0.8" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_7_3">
<rect width="9" height="11" fill="white"/>
</clipPath>
</defs>
</svg>
<span>SEE ALL APPLICATION</span>
</a>
</div>
</div>
<div class="kneo-home__section kneo-home__section--footer">
<div class="kneo-home__placeholder">Footer 區塊(待設計)</div>
</div>
</section>
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBottom }) @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBottom })
</div> </div>
</div> </div>

View File

@ -67,7 +67,6 @@
</script> </script>
} }
<div data-productid="@Model.Id"> <div data-productid="@Model.Id">
@await Html.PartialAsync("_ProductHardwarePrHero", Model)
<div class="product-essential"> <div class="product-essential">
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.ProductDetailsEssentialTop, additionalData = Model }) @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.ProductDetailsEssentialTop, additionalData = Model })
<div class="gallery"> <div class="gallery">
@ -175,4 +174,4 @@
} }
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.ProductDetailsBottom, additionalData = Model }) @await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.ProductDetailsBottom, additionalData = Model })
</div> </div>
</div> </div>

View File

@ -1,59 +0,0 @@
@model ProductDetailsModel
@{
var hasHeroContent = !string.IsNullOrWhiteSpace(Model.HeroTitleLine1)
|| !string.IsNullOrWhiteSpace(Model.HeroTitleLine2)
|| !string.IsNullOrWhiteSpace(Model.HeroFeatureLine1)
|| !string.IsNullOrWhiteSpace(Model.HeroFeatureLine2)
|| !string.IsNullOrWhiteSpace(Model.HeroPrCopy);
var canShow = Model.HeroEnabled && hasHeroContent;
}
@if (canShow)
{
<section class="kneo-product-hero-pr">
<div class="kneo-product-hero-pr__main">
<div class="kneo-product-hero-pr__title">
@if (!string.IsNullOrWhiteSpace(Model.HeroTitleLine1))
{
<div class="kneo-product-hero-pr__line1" style="@(Model.HeroTitleLine1FontSize.HasValue ? $"font-size:{Model.HeroTitleLine1FontSize.Value}px;" : null)">@Model.HeroTitleLine1</div>
}
@if (!string.IsNullOrWhiteSpace(Model.HeroTitleLine2))
{
<div class="kneo-product-hero-pr__line2" style="@(Model.HeroTitleLine2FontSize.HasValue ? $"font-size:{Model.HeroTitleLine2FontSize.Value}px;" : null)">@Model.HeroTitleLine2</div>
}
</div>
<div class="kneo-product-hero-pr__picture">
<img src="@Model.DefaultPictureModel.ImageUrl" alt="@Model.DefaultPictureModel.AlternateText" />
</div>
<div class="kneo-product-hero-pr__feature">
@if (!string.IsNullOrWhiteSpace(Model.HeroFeatureLine1))
{
<div class="kneo-product-hero-pr__feature-line1" style="@(Model.HeroFeatureLine1FontSize.HasValue ? $"font-size:{Model.HeroFeatureLine1FontSize.Value}px;" : null)">@Model.HeroFeatureLine1</div>
}
@if (!string.IsNullOrWhiteSpace(Model.HeroFeatureLine2))
{
<div class="kneo-product-hero-pr__feature-line2" style="@(Model.HeroFeatureLine2FontSize.HasValue ? $"font-size:{Model.HeroFeatureLine2FontSize.Value}px;" : null)">@Model.HeroFeatureLine2</div>
}
</div>
<div class="kneo-product-hero-pr__copy" style="@(Model.HeroPrCopyFontSize.HasValue ? $"font-size:{Model.HeroPrCopyFontSize.Value}px;" : null)">
@if (!string.IsNullOrWhiteSpace(Model.HeroPrCopy))
{
var longCopy = Model.HeroPrCopy.Length > 120;
if (longCopy)
{
<div class="kneo-product-hero-pr__copy-preview">@Model.HeroPrCopy.Substring(0, 120)...</div>
<details class="kneo-product-hero-pr__copy-more">
<summary>顯示全文</summary>
<div>@Model.HeroPrCopy</div>
</details>
}
else
{
<div>@Model.HeroPrCopy</div>
}
}
</div>
</div>
</section>
}

View File

@ -1,28 +0,0 @@
@model IList<ProductDetailsModel>
@if (Model?.Any() == true)
{
var first = Model.First();
<div class="homepage-hero-products">
<div class="homepage-hero-products__current">
@await Html.PartialAsync("~/Views/Product/_ProductHardwarePrHero.cshtml", first)
</div>
@if (Model.Count > 1)
{
<div class="homepage-hero-products__dots">
@for (var i = 0; i < Model.Count; i++)
{
<span class="homepage-hero-products__dot @(i == 0 ? "is-active" : null)"></span>
}
</div>
}
</div>
}
else
{
<div class="homepage-hero-products homepage-hero-products--empty">
<div class="homepage-hero-products__empty-title">AI Camera</div>
<div class="homepage-hero-products__empty-subtitle">智慧影像識別設備</div>
<div class="homepage-hero-products__empty-copy">首頁 Hero 尚未設定產品,請至後台「首頁輪播」選擇商品。</div>
</div>
}

View File

@ -1,40 +1,17 @@
@model IList<ProductOverviewModel> @model IList<ProductOverviewModel>
@using System.Linq
@using Nop.Core.Domain.Catalog
@if (Model.Count > 0) @if (Model.Count > 0)
{ {
var homeItems = Model.Take(4).ToList(); <div class="product-grid home-page-product-grid">
<div class="product-grid home-page-product-grid kneo-home-hw-grid"> <div class="title">
<strong>@T("Homepage.Products")</strong>
</div>
<div class="item-grid"> <div class="item-grid">
@foreach (var item in homeItems) @foreach (var item in Model)
{ {
var statusClass = "in-stock"; <div class="item-box">
if (item.StockQuantity <= 0) @await Html.PartialAsync("_ProductBox", item)
{ </div>
statusClass = "out-stock";
}
else if (item.NotifyAdminForQuantityBelow > 0 && item.StockQuantity <= item.NotifyAdminForQuantityBelow)
{
statusClass = "low-stock";
}
var productUrl = Url.RouteUrl<Product>(new { SeName = item.SeName });
var picture = item.PictureModels.FirstOrDefault();
<article class="kneo-home-hw-card @statusClass">
<a class="kneo-home-hw-card__hit" href="@productUrl" aria-label="@item.Name"></a>
<div class="kneo-home-hw-card__stock-icon"></div>
<div class="kneo-home-hw-card__picture">
@if (picture != null)
{
<img src="@picture.ImageUrl" alt="@picture.AlternateText" />
}
</div>
<div class="kneo-home-hw-card__text">
<div class="kneo-home-hw-card__price">@item.ProductPrice.Price</div>
<div class="kneo-home-hw-card__tag">加速硬體</div>
<div class="kneo-home-hw-card__name"><a href="@productUrl">@item.Name</a></div>
</div>
</article>
} }
</div> </div>
</div> </div>
} }