Просмотр исходного кода

feat: 完成前端视觉效果优化

- 添加全局渐变背景、Inter字体和平滑滚动
- 优化统计卡片:白色圆角24px,阴影,数字计数动画,悬停上移
- 优化添加按钮:渐变绿,完全圆角,悬停放大,带➕图标
- 优化物品卡片:白色圆角20px,左侧彩色竖条,分类彩色徽章,滑动删除
- 优化底部Tab:固定底部,圆角,选中绿色指示器
- 优化表单:圆角输入框,聚焦绿色阴影,表单项依次淡入
- 优化空状态:居中,插画图标,友好提示
- 添加加载骨架屏:灰色块闪烁动画
- 添加Toast提示:圆角半透明黑底白字
Li 6 месяцев назад
Родитель
Сommit
1aec8c2e38
66 измененных файлов с 5706 добавлено и 1812 удалено
  1. 0 0
      .docgeni/site/.angular/cache/20.3.22/site/.tsbuildinfo
  2. BIN
      .docgeni/site/.angular/cache/20.3.22/site/angular-compiler.db
  3. BIN
      .docgeni/site/.angular/cache/20.3.22/site/angular-compiler.db-lock
  4. 121 0
      .docgeni/site/angular.json
  5. 31 0
      .docgeni/site/karma.conf.js
  6. 15 0
      .docgeni/site/src/app/app.module.ts
  7. 1 0
      .docgeni/site/src/app/content/component-examples.ts
  8. 14 0
      .docgeni/site/src/app/content/components/custom/index.ts
  9. 14 0
      .docgeni/site/src/app/content/config.ts
  10. 28 0
      .docgeni/site/src/app/content/example-loader.ts
  11. 3 0
      .docgeni/site/src/app/content/example-modules.ts
  12. 20 0
      .docgeni/site/src/app/content/index.ts
  13. 274 0
      .docgeni/site/src/app/content/navigations.json
  14. 89 0
      .docgeni/site/src/assets/content/docs/api/item-service.html
  15. 37 0
      .docgeni/site/src/assets/content/docs/api/notification-service.html
  16. 95 0
      .docgeni/site/src/assets/content/docs/guide/features.html
  17. 46 0
      .docgeni/site/src/assets/content/docs/guide/getting-started.html
  18. 528 0
      .docgeni/site/src/assets/content/navigations-en-us.json
  19. 4 0
      .docgeni/site/src/environments/environment.prod.ts
  20. 13 0
      .docgeni/site/src/environments/environment.ts
  21. BIN
      .docgeni/site/src/favicon.ico
  22. 13 0
      .docgeni/site/src/index.html
  23. 7 0
      .docgeni/site/src/main.ts
  24. 3 0
      .docgeni/site/src/styles.scss
  25. 36 0
      .docgeni/site/tsconfig.app.json
  26. 103 2
      angular.json
  27. 6 0
      ionic.config.json
  28. 1586 1600
      package-lock.json
  29. 17 4
      package.json
  30. 63 0
      projects/inventory-assistant/README.md
  31. 7 0
      projects/inventory-assistant/ng-package.json
  32. 12 0
      projects/inventory-assistant/package.json
  33. 3 0
      projects/inventory-assistant/src/lib/inventory-assistant.html
  34. 0 0
      projects/inventory-assistant/src/lib/inventory-assistant.scss
  35. 22 0
      projects/inventory-assistant/src/lib/inventory-assistant.spec.ts
  36. 15 0
      projects/inventory-assistant/src/lib/inventory-assistant.ts
  37. 5 0
      projects/inventory-assistant/src/public-api.ts
  38. 18 0
      projects/inventory-assistant/tsconfig.lib.json
  39. 11 0
      projects/inventory-assistant/tsconfig.lib.prod.json
  40. 15 0
      projects/inventory-assistant/tsconfig.spec.json
  41. 2 1
      src/app/app.config.ts
  42. 0 196
      src/app/app.html
  43. 26 1
      src/app/app.routes.ts
  44. 84 0
      src/app/app.scss
  45. 80 5
      src/app/app.ts
  46. 72 0
      src/app/components/add-item/add-item.component.html
  47. 119 0
      src/app/components/add-item/add-item.component.scss
  48. 104 0
      src/app/components/add-item/add-item.component.ts
  49. 119 0
      src/app/components/home/home.component.html
  50. 497 0
      src/app/components/home/home.component.scss
  51. 121 0
      src/app/components/home/home.component.ts
  52. 49 0
      src/app/components/map/map.component.html
  53. 136 0
      src/app/components/map/map.component.scss
  54. 98 0
      src/app/components/map/map.component.ts
  55. 77 0
      src/app/components/profile/profile.component.html
  56. 184 0
      src/app/components/profile/profile.component.scss
  57. 74 0
      src/app/components/profile/profile.component.ts
  58. 11 0
      src/app/models/item.model.ts
  59. 311 0
      src/app/services/item.service.ts
  60. 66 0
      src/app/services/notification.service.ts
  61. 43 0
      src/app/services/toast.service.ts
  62. 41 0
      src/app/services/user.service.ts
  63. 2 2
      src/main.ts
  64. 94 1
      src/styles.scss
  65. 10 0
      src/theme/variables.css
  66. 11 0
      tsconfig.json

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.docgeni/site/.angular/cache/20.3.22/site/.tsbuildinfo


BIN
.docgeni/site/.angular/cache/20.3.22/site/angular-compiler.db


BIN
.docgeni/site/.angular/cache/20.3.22/site/angular-compiler.db-lock


+ 121 - 0
.docgeni/site/angular.json

@@ -0,0 +1,121 @@
+{
+  "$schema": "../node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "packages",
+  "projects": {
+    "site": {
+      "projectType": "application",
+      "schematics": {
+        "@schematics/angular:component": {
+          "style": "scss"
+        }
+      },
+      "root": "src",
+      "sourceRoot": "src",
+      "prefix": "app",
+      "architect": {
+        "build": {
+          "builder": "@angular/build:application",
+          "options": {
+            "outputPath": {
+              "base": "../../dist/docgeni-site",
+              "browser": ""
+            },
+            "index": "src/index.html",
+            "browser": "src/main.ts",
+            "polyfills": [
+              "zone.js"
+            ],
+            "tsConfig": "tsconfig.app.json",
+            "assets": [
+              {
+                "glob": "**/*",
+                "input": "public",
+                "output": "public"
+              },
+              {
+                "glob": "favicon.ico",
+                "input": "src/",
+                "output": "/"
+              },
+              {
+                "glob": "**/*",
+                "input": "src/assets/",
+                "output": "/assets/"
+              },
+              {
+                "glob": "sitemap.xml",
+                "input": "src/",
+                "output": "/"
+              }
+            ],
+            "styles": [
+              "src/styles.scss"
+            ],
+            "scripts": []
+          },
+          "configurations": {
+            "production": {
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ],
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "namedChunks": false,
+              "extractLicenses": true,
+              "budgets": [
+                {
+                  "type": "initial",
+                  "maximumWarning": "2mb",
+                  "maximumError": "5mb"
+                },
+                {
+                  "type": "anyComponentStyle",
+                  "maximumWarning": "6kb",
+                  "maximumError": "10kb"
+                }
+              ]
+            },
+            "development": {
+              "optimization": false,
+              "extractLicenses": false,
+              "sourceMap": true,
+              "namedChunks": true
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "serve": {
+          "builder": "@angular/build:dev-server",
+          "options": {
+            "buildTarget": "site:build",
+            "port": 4600,
+            "hmr": true
+          },
+          "configurations": {
+            "production": {
+              "buildTarget": "site:build:production"
+            },
+            "development": {
+              "buildTarget": "site:build:development"
+            }
+          },
+          "defaultConfiguration": "development"
+        },
+        "extract-i18n": {
+          "builder": "@angular/build:extract-i18n",
+          "options": {
+            "buildTarget": "site:build"
+          }
+        }
+      }
+    }
+  },
+  "cli": {
+    "analytics": false
+  }
+}

+ 31 - 0
.docgeni/site/karma.conf.js

@@ -0,0 +1,31 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+    config.set({
+        basePath: '',
+        frameworks: ['jasmine'],
+        plugins: [
+            require('karma-jasmine'),
+            require('karma-chrome-launcher'),
+            require('karma-jasmine-html-reporter'),
+            require('karma-coverage-istanbul-reporter'),
+        ],
+        client: {
+            clearContext: false, // leave Jasmine Spec Runner output visible in browser
+        },
+        coverageIstanbulReporter: {
+            dir: require('path').join(__dirname, '../../coverage/site'),
+            reports: ['html', 'lcovonly', 'text-summary'],
+            fixWebpackSourcePaths: true,
+        },
+        reporters: ['progress', 'kjhtml'],
+        port: 9876,
+        colors: true,
+        logLevel: config.LOG_INFO,
+        autoWatch: true,
+        browsers: ['Chrome'],
+        singleRun: false,
+        restartOnFileChange: true,
+    });
+};

+ 15 - 0
.docgeni/site/src/app/app.module.ts

@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { DocgeniTemplateModule } from '@docgeni/template';
+import { DOCGENI_SITE_PROVIDERS, IMPORT_MODULES, RootComponent } from './content/index';
+@NgModule({
+    declarations: [],
+    imports: [BrowserModule, BrowserAnimationsModule, DocgeniTemplateModule, RouterModule.forRoot([]), ...IMPORT_MODULES],
+    providers: [...DOCGENI_SITE_PROVIDERS],
+    bootstrap: [RootComponent],
+})
+export class AppModule {
+    constructor() {}
+}

+ 1 - 0
.docgeni/site/src/app/content/component-examples.ts

@@ -0,0 +1 @@
+export const EXAMPLE_COMPONENTS = {};

+ 14 - 0
.docgeni/site/src/app/content/components/custom/index.ts

@@ -0,0 +1,14 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { addBuiltInComponents } from '@docgeni/template';
+@NgModule({
+  declarations: [  ],
+    providers: [  ],
+    imports: [ CommonModule ],
+    exports: [  ]
+})
+export class CustomComponentsModule {
+  constructor() {
+    addBuiltInComponents([]);
+  }
+}

+ 14 - 0
.docgeni/site/src/app/content/config.ts

@@ -0,0 +1,14 @@
+export const config = {
+    "title": "Docgeni",
+    "description": "",
+    "mode": "lite",
+    "theme": "default",
+    "locales": [
+        {
+            "name": "en-us",
+            "key": "en-us"
+        }
+    ],
+    "defaultLocale": "en-us",
+    "switchTheme": false
+};

+ 28 - 0
.docgeni/site/src/app/content/example-loader.ts

@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { ExampleLoader, ExampleLoadResult } from '@docgeni/template';
+import { EXAMPLE_COMPONENTS } from './component-examples';
+
+@Injectable()
+export class LibExampleLoader extends ExampleLoader {
+
+    override enableIvy = true;
+
+    load(exampleKey: string): Promise<ExampleLoadResult> {
+        const example = (EXAMPLE_COMPONENTS as Record<string, any>)[exampleKey];
+        return new Promise(resolve => {
+            (this as any)[example.module.importSpecifier]().then((result: any) => {
+                resolve({
+                    moduleType: result.EXAMPLES_MODULE,
+                    componentType: result.EXAMPLE_COMPONENTS[exampleKey],
+                    example
+                });
+            });
+        });
+    }
+
+}
+
+export const LIB_EXAMPLE_LOADER_PROVIDER = {
+    provide: ExampleLoader,
+    useClass: LibExampleLoader
+};

+ 3 - 0
.docgeni/site/src/app/content/example-modules.ts

@@ -0,0 +1,3 @@
+
+export const EXAMPLE_MODULES = [
+];

+ 20 - 0
.docgeni/site/src/app/content/index.ts

@@ -0,0 +1,20 @@
+import { config } from './config';
+import { LIB_EXAMPLE_LOADER_PROVIDER } from './example-loader';
+import { CustomComponentsModule } from './components/custom';
+import {
+    CONFIG_TOKEN,
+    DOCGENI_INITIALIZER_PROVIDERS
+} from '@docgeni/template';
+
+export const DOCGENI_SITE_PROVIDERS = [
+    ...DOCGENI_INITIALIZER_PROVIDERS,
+    LIB_EXAMPLE_LOADER_PROVIDER,
+    {
+        provide: CONFIG_TOKEN,
+        useValue: config
+    }
+];
+
+export const IMPORT_MODULES = [ CustomComponentsModule ];
+
+export { RootComponent, DocgeniTemplateModule } from '@docgeni/template';

+ 274 - 0
.docgeni/site/src/app/content/navigations.json

@@ -0,0 +1,274 @@
+{
+  "en-us": [
+    {
+      "id": "api",
+      "path": "api",
+      "channelPath": "api",
+      "title": "Api",
+      "items": [
+        {
+          "id": "item-service",
+          "path": "api/item-service",
+          "channelPath": "api",
+          "title": "Item Service",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "itemservice",
+              "name": "ItemService",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+              "name": "服务说明",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+              "name": "方法列表",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "getallitems()",
+              "name": "getAllItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getitem(id:-string)",
+              "name": "getItem(id: string)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "additem(item:-omit&lt;iitem,-&#39;id&#39;-%7C-&#39;status&#39;&gt;)",
+              "name": "addItem(item: Omit&lt;IItem, &#39;id&#39; | &#39;status&#39;&gt;)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "updateitem(id:-string,-item:-partial%3Ciitem%3E)",
+              "name": "updateItem(id: string, item: Partial<IItem>)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "deleteitem(id:-string)",
+              "name": "deleteItem(id: string)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getexpiringitems()",
+              "name": "getExpiringItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getexpireditems()",
+              "name": "getExpiredItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getstatistics()",
+              "name": "getStatistics()",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/api/item-service.html",
+          "originPath": "docs/api/item-service.md"
+        },
+        {
+          "id": "notification-service",
+          "path": "api/notification-service",
+          "channelPath": "api",
+          "title": "Notification Service",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "notificationservice",
+              "name": "NotificationService",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+              "name": "服务说明",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+              "name": "方法列表",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "requestpermission()",
+              "name": "requestPermission()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "scheduledailycheck()",
+              "name": "scheduleDailyCheck()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "checkexpiringitems()",
+              "name": "checkExpiringItems()",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/api/notification-service.html",
+          "originPath": "docs/api/notification-service.md"
+        }
+      ],
+      "order": 9007199254740991
+    },
+    {
+      "id": "guide",
+      "path": "guide",
+      "channelPath": "guide",
+      "title": "Guide",
+      "items": [
+        {
+          "id": "features",
+          "path": "guide/features",
+          "channelPath": "guide",
+          "title": "Features",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD",
+              "name": "核心功能",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E7%89%A9%E5%93%81%E7%AE%A1%E7%90%86",
+              "name": "物品管理",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%B7%BB%E5%8A%A0%E7%89%A9%E5%93%81",
+              "name": "添加物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E7%BC%96%E8%BE%91%E7%89%A9%E5%93%81",
+              "name": "编辑物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E5%88%A0%E9%99%A4%E7%89%A9%E5%93%81",
+              "name": "删除物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E8%BF%87%E6%9C%9F%E6%8F%90%E9%86%92",
+              "name": "过期提醒",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E7%8A%B6%E6%80%81%E8%AE%A1%E7%AE%97",
+              "name": "状态计算",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E9%80%9A%E7%9F%A5%E6%8F%90%E9%86%92",
+              "name": "通知提醒",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E6%94%B6%E7%BA%B3%E5%9C%B0%E5%9B%BE",
+              "name": "收纳地图",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83",
+              "name": "个人中心",
+              "level": 2,
+              "type": "h2"
+            }
+          ],
+          "contentPath": "docs/guide/features.html",
+          "originPath": "docs/guide/features.md"
+        },
+        {
+          "id": "getting-started",
+          "path": "guide/getting-started",
+          "channelPath": "guide",
+          "title": "Getting Started",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B",
+              "name": "快速开始",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B",
+              "name": "项目简介",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7",
+              "name": "功能特性",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C",
+              "name": "安装和运行",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96",
+              "name": "安装依赖",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E5%90%AF%E5%8A%A8%E5%BC%80%E5%8F%91%E6%9C%8D%E5%8A%A1%E5%99%A8",
+              "name": "启动开发服务器",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE",
+              "name": "构建项目",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/guide/getting-started.html",
+          "originPath": "docs/guide/getting-started.md"
+        }
+      ],
+      "order": 9007199254740991
+    }
+  ]
+}

+ 89 - 0
.docgeni/site/src/assets/content/docs/api/item-service.html

@@ -0,0 +1,89 @@
+
+        <h1 id="itemservice" class="docs-header-link">
+          <span header-link="itemservice"></span>
+          ItemService
+        </h1>
+      
+        <h2 id="%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E" class="docs-header-link">
+          <span header-link="%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E"></span>
+          服务说明
+        </h2>
+      <p>ItemService 是用于管理物品数据的服务,提供了增删改查方法,并使用 localStorage 进行数据持久化。</p>
+
+        <h2 id="%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8" class="docs-header-link">
+          <span header-link="%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8"></span>
+          方法列表
+        </h2>
+      
+        <h3 id="getallitems()" class="docs-header-link">
+          <span header-link="getallitems()"></span>
+          getAllItems()
+        </h3>
+      <p>获取所有物品列表。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem[]</code> - 物品数组</div>
+
+        <h3 id="getitem(id:-string)" class="docs-header-link">
+          <span header-link="getitem(id:-string)"></span>
+          getItem(id: string)
+        </h3>
+      <p>根据 ID 获取物品。</p>
+<div class="dg-paragraph"><strong>参数</strong>:</div>
+<ul>
+<li><code>id</code>:物品 ID</li>
+</ul>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem | null</code> - 物品对象或 null</div>
+
+        <h3 id="additem(item:-omit&lt;iitem,-&#39;id&#39;-%7C-&#39;status&#39;&gt;)" class="docs-header-link">
+          <span header-link="additem(item:-omit&lt;iitem,-&#39;id&#39;-%7C-&#39;status&#39;&gt;)"></span>
+          addItem(item: Omit&lt;IItem, &#39;id&#39; | &#39;status&#39;&gt;)
+        </h3>
+      <p>添加新物品。</p>
+<div class="dg-paragraph"><strong>参数</strong>:</div>
+<ul>
+<li><code>item</code>:物品信息(不包含 id 和 status)</li>
+</ul>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem</code> - 添加的物品对象</div>
+
+        <h3 id="updateitem(id:-string,-item:-partial%3Ciitem%3E)" class="docs-header-link">
+          <span header-link="updateitem(id:-string,-item:-partial%3Ciitem%3E)"></span>
+          updateItem(id: string, item: Partial<IItem>)
+        </h3>
+      <p>更新物品信息。</p>
+<div class="dg-paragraph"><strong>参数</strong>:</div>
+<ul>
+<li><code>id</code>:物品 ID</li>
+<li><code>item</code>:要更新的物品信息</li>
+</ul>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem | null</code> - 更新后的物品对象或 null</div>
+
+        <h3 id="deleteitem(id:-string)" class="docs-header-link">
+          <span header-link="deleteitem(id:-string)"></span>
+          deleteItem(id: string)
+        </h3>
+      <p>删除物品。</p>
+<div class="dg-paragraph"><strong>参数</strong>:</div>
+<ul>
+<li><code>id</code>:物品 ID</li>
+</ul>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>boolean</code> - 删除是否成功</div>
+
+        <h3 id="getexpiringitems()" class="docs-header-link">
+          <span header-link="getexpiringitems()"></span>
+          getExpiringItems()
+        </h3>
+      <p>获取临期物品(距离过期 &lt;= remindDays)。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem[]</code> - 临期物品数组</div>
+
+        <h3 id="getexpireditems()" class="docs-header-link">
+          <span header-link="getexpireditems()"></span>
+          getExpiredItems()
+        </h3>
+      <p>获取已过期物品。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>IItem[]</code> - 过期物品数组</div>
+
+        <h3 id="getstatistics()" class="docs-header-link">
+          <span header-link="getstatistics()"></span>
+          getStatistics()
+        </h3>
+      <p>获取物品统计信息。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>{ total: number; expiring: number; expired: number }</code> - 统计信息</div>

+ 37 - 0
.docgeni/site/src/assets/content/docs/api/notification-service.html

@@ -0,0 +1,37 @@
+
+        <h1 id="notificationservice" class="docs-header-link">
+          <span header-link="notificationservice"></span>
+          NotificationService
+        </h1>
+      
+        <h2 id="%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E" class="docs-header-link">
+          <span header-link="%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E"></span>
+          服务说明
+        </h2>
+      <p>NotificationService 是用于发送浏览器通知的服务,每天上午9点检查临期物品并发送通知。</p>
+
+        <h2 id="%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8" class="docs-header-link">
+          <span header-link="%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8"></span>
+          方法列表
+        </h2>
+      
+        <h3 id="requestpermission()" class="docs-header-link">
+          <span header-link="requestpermission()"></span>
+          requestPermission()
+        </h3>
+      <p>请求浏览器通知权限。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>Promise&lt;boolean&gt;</code> - 是否获得权限</div>
+
+        <h3 id="scheduledailycheck()" class="docs-header-link">
+          <span header-link="scheduledailycheck()"></span>
+          scheduleDailyCheck()
+        </h3>
+      <p>调度每天的检查任务。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>void</code></div>
+
+        <h3 id="checkexpiringitems()" class="docs-header-link">
+          <span header-link="checkexpiringitems()"></span>
+          checkExpiringItems()
+        </h3>
+      <p>检查临期物品并发送通知。</p>
+<div class="dg-paragraph"><strong>返回值</strong>:<code>void</code></div>

+ 95 - 0
.docgeni/site/src/assets/content/docs/guide/features.html

@@ -0,0 +1,95 @@
+
+        <h1 id="%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD" class="docs-header-link">
+          <span header-link="%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD"></span>
+          核心功能
+        </h1>
+      
+        <h2 id="%E7%89%A9%E5%93%81%E7%AE%A1%E7%90%86" class="docs-header-link">
+          <span header-link="%E7%89%A9%E5%93%81%E7%AE%A1%E7%90%86"></span>
+          物品管理
+        </h2>
+      
+        <h3 id="%E6%B7%BB%E5%8A%A0%E7%89%A9%E5%93%81" class="docs-header-link">
+          <span header-link="%E6%B7%BB%E5%8A%A0%E7%89%A9%E5%93%81"></span>
+          添加物品
+        </h3>
+      <ol>
+<li>点击首页右上角的「+ 添加物品」按钮</li>
+<li>填写物品信息:<ul>
+<li>物品名称(必填)</li>
+<li>分类(食品/药品/日用品)</li>
+<li>存放位置</li>
+<li>数量</li>
+<li>生产日期</li>
+<li>过期日期(必填,不能早于生产日期)</li>
+<li>提前提醒天数(默认3天)</li>
+</ul>
+</li>
+<li>点击「保存」按钮</li>
+</ol>
+
+        <h3 id="%E7%BC%96%E8%BE%91%E7%89%A9%E5%93%81" class="docs-header-link">
+          <span header-link="%E7%BC%96%E8%BE%91%E7%89%A9%E5%93%81"></span>
+          编辑物品
+        </h3>
+      <ol>
+<li>在首页找到要编辑的物品</li>
+<li>点击物品卡片上的「编辑」按钮</li>
+<li>修改物品信息</li>
+<li>点击「保存」按钮</li>
+</ol>
+
+        <h3 id="%E5%88%A0%E9%99%A4%E7%89%A9%E5%93%81" class="docs-header-link">
+          <span header-link="%E5%88%A0%E9%99%A4%E7%89%A9%E5%93%81"></span>
+          删除物品
+        </h3>
+      <ol>
+<li>在首页找到要删除的物品</li>
+<li>点击物品卡片上的「删除」按钮</li>
+<li>确认删除操作</li>
+</ol>
+
+        <h2 id="%E8%BF%87%E6%9C%9F%E6%8F%90%E9%86%92" class="docs-header-link">
+          <span header-link="%E8%BF%87%E6%9C%9F%E6%8F%90%E9%86%92"></span>
+          过期提醒
+        </h2>
+      
+        <h3 id="%E7%8A%B6%E6%80%81%E8%AE%A1%E7%AE%97" class="docs-header-link">
+          <span header-link="%E7%8A%B6%E6%80%81%E8%AE%A1%E7%AE%97"></span>
+          状态计算
+        </h3>
+      <ul>
+<li><strong>正常</strong>:距离过期还有超过提醒天数的时间</li>
+<li><strong>临期</strong>:距离过期时间小于等于提醒天数</li>
+<li><strong>过期</strong>:已经过期</li>
+</ul>
+
+        <h3 id="%E9%80%9A%E7%9F%A5%E6%8F%90%E9%86%92" class="docs-header-link">
+          <span header-link="%E9%80%9A%E7%9F%A5%E6%8F%90%E9%86%92"></span>
+          通知提醒
+        </h3>
+      <ul>
+<li>每天上午9点自动检查临期物品</li>
+<li>如果有临期物品,发送浏览器通知</li>
+<li>需要用户授权通知权限</li>
+</ul>
+
+        <h2 id="%E6%94%B6%E7%BA%B3%E5%9C%B0%E5%9B%BE" class="docs-header-link">
+          <span header-link="%E6%94%B6%E7%BA%B3%E5%9C%B0%E5%9B%BE"></span>
+          收纳地图
+        </h2>
+      <ul>
+<li>按位置分组显示物品</li>
+<li>支持搜索功能</li>
+<li>显示每个物品的状态</li>
+</ul>
+
+        <h2 id="%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83" class="docs-header-link">
+          <span header-link="%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83"></span>
+          个人中心
+        </h2>
+      <ul>
+<li>管理个人信息(昵称、性别、年龄)</li>
+<li>检查通知权限</li>
+<li>清空所有数据</li>
+</ul>

+ 46 - 0
.docgeni/site/src/assets/content/docs/guide/getting-started.html

@@ -0,0 +1,46 @@
+
+        <h1 id="%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B" class="docs-header-link">
+          <span header-link="%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B"></span>
+          快速开始
+        </h1>
+      
+        <h2 id="%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B" class="docs-header-link">
+          <span header-link="%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B"></span>
+          项目简介
+        </h2>
+      <p>Home Assistant 是一个家庭物资收纳及过期提醒助手,帮助您管理家庭物品,及时提醒过期物品。</p>
+
+        <h2 id="%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7" class="docs-header-link">
+          <span header-link="%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7"></span>
+          功能特性
+        </h2>
+      <ul>
+<li>物品管理:添加、编辑、删除物品</li>
+<li>过期提醒:自动计算物品状态,提醒临期和过期物品</li>
+<li>收纳地图:按位置分组显示物品</li>
+<li>个人中心:管理个人信息</li>
+<li>搜索功能:快速查找物品</li>
+</ul>
+
+        <h2 id="%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C" class="docs-header-link">
+          <span header-link="%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C"></span>
+          安装和运行
+        </h2>
+      
+        <h3 id="%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96" class="docs-header-link">
+          <span header-link="%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96"></span>
+          安装依赖
+        </h3>
+      <pre class="language-bash"><code class="language-bash"><div><span class="token function">npm</span> <span class="token function">install</span></div></code></pre>
+
+        <h3 id="%E5%90%AF%E5%8A%A8%E5%BC%80%E5%8F%91%E6%9C%8D%E5%8A%A1%E5%99%A8" class="docs-header-link">
+          <span header-link="%E5%90%AF%E5%8A%A8%E5%BC%80%E5%8F%91%E6%9C%8D%E5%8A%A1%E5%99%A8"></span>
+          启动开发服务器
+        </h3>
+      <pre class="language-bash"><code class="language-bash"><div><span class="token function">npm</span> start</div></code></pre>
+
+        <h3 id="%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE" class="docs-header-link">
+          <span header-link="%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE"></span>
+          构建项目
+        </h3>
+      <pre class="language-bash"><code class="language-bash"><div><span class="token function">npm</span> run build</div></code></pre>

+ 528 - 0
.docgeni/site/src/assets/content/navigations-en-us.json

@@ -0,0 +1,528 @@
+{
+  "navs": [
+    {
+      "id": "api",
+      "path": "api",
+      "channelPath": "api",
+      "title": "Api",
+      "items": [
+        {
+          "id": "item-service",
+          "path": "api/item-service",
+          "channelPath": "api",
+          "title": "Item Service",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "itemservice",
+              "name": "ItemService",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+              "name": "服务说明",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+              "name": "方法列表",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "getallitems()",
+              "name": "getAllItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getitem(id:-string)",
+              "name": "getItem(id: string)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "additem(item:-omit&lt;iitem,-&#39;id&#39;-%7C-&#39;status&#39;&gt;)",
+              "name": "addItem(item: Omit&lt;IItem, &#39;id&#39; | &#39;status&#39;&gt;)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "updateitem(id:-string,-item:-partial%3Ciitem%3E)",
+              "name": "updateItem(id: string, item: Partial<IItem>)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "deleteitem(id:-string)",
+              "name": "deleteItem(id: string)",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getexpiringitems()",
+              "name": "getExpiringItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getexpireditems()",
+              "name": "getExpiredItems()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "getstatistics()",
+              "name": "getStatistics()",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/api/item-service.html",
+          "originPath": "docs/api/item-service.md"
+        },
+        {
+          "id": "notification-service",
+          "path": "api/notification-service",
+          "channelPath": "api",
+          "title": "Notification Service",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "notificationservice",
+              "name": "NotificationService",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+              "name": "服务说明",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+              "name": "方法列表",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "requestpermission()",
+              "name": "requestPermission()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "scheduledailycheck()",
+              "name": "scheduleDailyCheck()",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "checkexpiringitems()",
+              "name": "checkExpiringItems()",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/api/notification-service.html",
+          "originPath": "docs/api/notification-service.md"
+        }
+      ],
+      "order": 9007199254740991
+    },
+    {
+      "id": "guide",
+      "path": "guide",
+      "channelPath": "guide",
+      "title": "Guide",
+      "items": [
+        {
+          "id": "features",
+          "path": "guide/features",
+          "channelPath": "guide",
+          "title": "Features",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD",
+              "name": "核心功能",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E7%89%A9%E5%93%81%E7%AE%A1%E7%90%86",
+              "name": "物品管理",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E6%B7%BB%E5%8A%A0%E7%89%A9%E5%93%81",
+              "name": "添加物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E7%BC%96%E8%BE%91%E7%89%A9%E5%93%81",
+              "name": "编辑物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E5%88%A0%E9%99%A4%E7%89%A9%E5%93%81",
+              "name": "删除物品",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E8%BF%87%E6%9C%9F%E6%8F%90%E9%86%92",
+              "name": "过期提醒",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E7%8A%B6%E6%80%81%E8%AE%A1%E7%AE%97",
+              "name": "状态计算",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E9%80%9A%E7%9F%A5%E6%8F%90%E9%86%92",
+              "name": "通知提醒",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E6%94%B6%E7%BA%B3%E5%9C%B0%E5%9B%BE",
+              "name": "收纳地图",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83",
+              "name": "个人中心",
+              "level": 2,
+              "type": "h2"
+            }
+          ],
+          "contentPath": "docs/guide/features.html",
+          "originPath": "docs/guide/features.md"
+        },
+        {
+          "id": "getting-started",
+          "path": "guide/getting-started",
+          "channelPath": "guide",
+          "title": "Getting Started",
+          "order": 9007199254740991,
+          "toc": "content",
+          "headings": [
+            {
+              "id": "%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B",
+              "name": "快速开始",
+              "level": 1,
+              "type": "h1"
+            },
+            {
+              "id": "%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B",
+              "name": "项目简介",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7",
+              "name": "功能特性",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C",
+              "name": "安装和运行",
+              "level": 2,
+              "type": "h2"
+            },
+            {
+              "id": "%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96",
+              "name": "安装依赖",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E5%90%AF%E5%8A%A8%E5%BC%80%E5%8F%91%E6%9C%8D%E5%8A%A1%E5%99%A8",
+              "name": "启动开发服务器",
+              "level": 3,
+              "type": "h3"
+            },
+            {
+              "id": "%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE",
+              "name": "构建项目",
+              "level": 3,
+              "type": "h3"
+            }
+          ],
+          "contentPath": "docs/guide/getting-started.html",
+          "originPath": "docs/guide/getting-started.md"
+        }
+      ],
+      "order": 9007199254740991
+    }
+  ],
+  "docs": [
+    {
+      "id": "item-service",
+      "path": "api/item-service",
+      "channelPath": "api",
+      "title": "Item Service",
+      "order": 9007199254740991,
+      "toc": "content",
+      "headings": [
+        {
+          "id": "itemservice",
+          "name": "ItemService",
+          "level": 1,
+          "type": "h1"
+        },
+        {
+          "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+          "name": "服务说明",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+          "name": "方法列表",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "getallitems()",
+          "name": "getAllItems()",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "getitem(id:-string)",
+          "name": "getItem(id: string)",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "additem(item:-omit&lt;iitem,-&#39;id&#39;-%7C-&#39;status&#39;&gt;)",
+          "name": "addItem(item: Omit&lt;IItem, &#39;id&#39; | &#39;status&#39;&gt;)",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "updateitem(id:-string,-item:-partial%3Ciitem%3E)",
+          "name": "updateItem(id: string, item: Partial<IItem>)",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "deleteitem(id:-string)",
+          "name": "deleteItem(id: string)",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "getexpiringitems()",
+          "name": "getExpiringItems()",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "getexpireditems()",
+          "name": "getExpiredItems()",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "getstatistics()",
+          "name": "getStatistics()",
+          "level": 3,
+          "type": "h3"
+        }
+      ],
+      "contentPath": "docs/api/item-service.html",
+      "originPath": "docs/api/item-service.md"
+    },
+    {
+      "id": "notification-service",
+      "path": "api/notification-service",
+      "channelPath": "api",
+      "title": "Notification Service",
+      "order": 9007199254740991,
+      "toc": "content",
+      "headings": [
+        {
+          "id": "notificationservice",
+          "name": "NotificationService",
+          "level": 1,
+          "type": "h1"
+        },
+        {
+          "id": "%E6%9C%8D%E5%8A%A1%E8%AF%B4%E6%98%8E",
+          "name": "服务说明",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E6%96%B9%E6%B3%95%E5%88%97%E8%A1%A8",
+          "name": "方法列表",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "requestpermission()",
+          "name": "requestPermission()",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "scheduledailycheck()",
+          "name": "scheduleDailyCheck()",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "checkexpiringitems()",
+          "name": "checkExpiringItems()",
+          "level": 3,
+          "type": "h3"
+        }
+      ],
+      "contentPath": "docs/api/notification-service.html",
+      "originPath": "docs/api/notification-service.md"
+    },
+    {
+      "id": "features",
+      "path": "guide/features",
+      "channelPath": "guide",
+      "title": "Features",
+      "order": 9007199254740991,
+      "toc": "content",
+      "headings": [
+        {
+          "id": "%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD",
+          "name": "核心功能",
+          "level": 1,
+          "type": "h1"
+        },
+        {
+          "id": "%E7%89%A9%E5%93%81%E7%AE%A1%E7%90%86",
+          "name": "物品管理",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E6%B7%BB%E5%8A%A0%E7%89%A9%E5%93%81",
+          "name": "添加物品",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E7%BC%96%E8%BE%91%E7%89%A9%E5%93%81",
+          "name": "编辑物品",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E5%88%A0%E9%99%A4%E7%89%A9%E5%93%81",
+          "name": "删除物品",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E8%BF%87%E6%9C%9F%E6%8F%90%E9%86%92",
+          "name": "过期提醒",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E7%8A%B6%E6%80%81%E8%AE%A1%E7%AE%97",
+          "name": "状态计算",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E9%80%9A%E7%9F%A5%E6%8F%90%E9%86%92",
+          "name": "通知提醒",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E6%94%B6%E7%BA%B3%E5%9C%B0%E5%9B%BE",
+          "name": "收纳地图",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83",
+          "name": "个人中心",
+          "level": 2,
+          "type": "h2"
+        }
+      ],
+      "contentPath": "docs/guide/features.html",
+      "originPath": "docs/guide/features.md"
+    },
+    {
+      "id": "getting-started",
+      "path": "guide/getting-started",
+      "channelPath": "guide",
+      "title": "Getting Started",
+      "order": 9007199254740991,
+      "toc": "content",
+      "headings": [
+        {
+          "id": "%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B",
+          "name": "快速开始",
+          "level": 1,
+          "type": "h1"
+        },
+        {
+          "id": "%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B",
+          "name": "项目简介",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E5%8A%9F%E8%83%BD%E7%89%B9%E6%80%A7",
+          "name": "功能特性",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E5%AE%89%E8%A3%85%E5%92%8C%E8%BF%90%E8%A1%8C",
+          "name": "安装和运行",
+          "level": 2,
+          "type": "h2"
+        },
+        {
+          "id": "%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96",
+          "name": "安装依赖",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E5%90%AF%E5%8A%A8%E5%BC%80%E5%8F%91%E6%9C%8D%E5%8A%A1%E5%99%A8",
+          "name": "启动开发服务器",
+          "level": 3,
+          "type": "h3"
+        },
+        {
+          "id": "%E6%9E%84%E5%BB%BA%E9%A1%B9%E7%9B%AE",
+          "name": "构建项目",
+          "level": 3,
+          "type": "h3"
+        }
+      ],
+      "contentPath": "docs/guide/getting-started.html",
+      "originPath": "docs/guide/getting-started.md"
+    }
+  ]
+}

+ 4 - 0
.docgeni/site/src/environments/environment.prod.ts

@@ -0,0 +1,4 @@
+export const environment = {
+    production: true,
+    hmr: false,
+};

+ 13 - 0
.docgeni/site/src/environments/environment.ts

@@ -0,0 +1,13 @@
+export const environment = {
+    production: false,
+    hmr: true,
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/plugins/zone-error';  // Included with Angular CLI.

BIN
.docgeni/site/src/favicon.ico


+ 13 - 0
.docgeni/site/src/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <title>Docgeni</title>
+    <base href="/" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link rel="icon" type="image/x-icon" href="favicon.ico" />
+  </head>
+  <body>
+    <dg-root></dg-root>
+  </body>
+</html>

+ 7 - 0
.docgeni/site/src/main.ts

@@ -0,0 +1,7 @@
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic()
+    .bootstrapModule(AppModule)
+    .catch((err) => console.error(err));

+ 3 - 0
.docgeni/site/src/styles.scss

@@ -0,0 +1,3 @@
+/* You can add global styles to this file, and also import other style files */
+
+@import '@docgeni/template/styles/index.css';

+ 36 - 0
.docgeni/site/tsconfig.app.json

@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "outDir": "./dist/out-tsc/site",
+    "sourceMap": true,
+    "declaration": false,
+    "downlevelIteration": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "esModuleInterop": false,
+    "importHelpers": true,
+    "target": "es2022",
+    "useDefineForClassFields": false,
+    "lib": [
+      "es2020",
+      "dom"
+    ],
+    "types": [
+      "node"
+    ],
+    "paths":{
+    }
+  },
+  "files": [
+    "src/main.ts"
+  ],
+  "include": [
+    "src/**/*.d.ts"
+  ],
+  "exclude": [
+    "src/assets",
+    "test.ts"
+  ]
+}

+ 103 - 2
angular.json

@@ -30,7 +30,40 @@
               }
             ],
             "styles": [
-              "src/styles.scss"
+              "src/styles.scss",
+              {
+                "input": "node_modules/@ionic/angular/css/core.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/normalize.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/structure.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/typography.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/display.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/padding.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/float-elements.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/text-alignment.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/text-transformation.css"
+              },
+              {
+                "input": "node_modules/@ionic/angular/css/flex-utils.css"
+              },
+              {
+                "input": "src/theme/variables.css"
+              }
             ]
           },
           "configurations": {
@@ -91,8 +124,76 @@
               "src/styles.scss"
             ]
           }
+        },
+        "ionic-cordova-serve": {
+          "builder": "@ionic/angular-toolkit:cordova-serve",
+          "options": {
+            "cordovaBuildTarget": "home-assistant:ionic-cordova-build",
+            "devServerTarget": "home-assistant:serve"
+          },
+          "configurations": {
+            "production": {
+              "cordovaBuildTarget": "home-assistant:ionic-cordova-build:production",
+              "devServerTarget": "home-assistant:serve:production"
+            }
+          }
+        },
+        "ionic-cordova-build": {
+          "builder": "@ionic/angular-toolkit:cordova-build",
+          "options": {
+            "browserTarget": "home-assistant:build"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "home-assistant:build:production"
+            }
+          }
+        }
+      }
+    },
+    "inventory-assistant": {
+      "projectType": "library",
+      "root": "projects/inventory-assistant",
+      "sourceRoot": "projects/inventory-assistant/src",
+      "prefix": "lib",
+      "architect": {
+        "build": {
+          "builder": "@angular/build:ng-packagr",
+          "configurations": {
+            "production": {
+              "tsConfig": "projects/inventory-assistant/tsconfig.lib.prod.json"
+            },
+            "development": {
+              "tsConfig": "projects/inventory-assistant/tsconfig.lib.json"
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "test": {
+          "builder": "@angular/build:karma",
+          "options": {
+            "tsConfig": "projects/inventory-assistant/tsconfig.spec.json",
+            "polyfills": [
+              "zone.js",
+              "zone.js/testing"
+            ]
+          }
         }
       }
     }
+  },
+  "cli": {
+    "schematicCollections": [
+      "@ionic/angular-toolkit"
+    ],
+    "analytics": "eaf0fa18-d15c-4241-b951-36d45648937f"
+  },
+  "schematics": {
+    "@ionic/angular-toolkit:component": {
+      "styleext": "scss"
+    },
+    "@ionic/angular-toolkit:page": {
+      "styleext": "scss"
+    }
   }
-}
+}

+ 6 - 0
ionic.config.json

@@ -0,0 +1,6 @@
+{
+  "name": "ionic-app",
+  "app_id": "",
+  "type": "angular-standalone",
+  "integrations": {}
+}

Разница между файлами не показана из-за своего большого размера
+ 1586 - 1600
package-lock.json


+ 17 - 4
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "home-assistant",
+  "name": "@liiiiiiiiiiiii/home-assistant",
   "version": "0.0.0",
   "scripts": {
     "ng": "ng",
@@ -20,7 +20,6 @@
       }
     ]
   },
-  "private": true,
   "dependencies": {
     "@angular/common": "^20.2.0",
     "@angular/compiler": "^20.2.0",
@@ -28,6 +27,9 @@
     "@angular/forms": "^20.2.0",
     "@angular/platform-browser": "^20.2.0",
     "@angular/router": "^20.2.0",
+    "@ionic/angular": "^8.8.3",
+    "fmode-ng": "^0.0.246",
+    "ng-fmode-admin": "^0.0.21",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
@@ -36,6 +38,8 @@
     "@angular/build": "^20.2.2",
     "@angular/cli": "^20.2.2",
     "@angular/compiler-cli": "^20.2.0",
+    "@docgeni/cli": "^2.6.0",
+    "@ionic/angular-toolkit": "latest",
     "@types/jasmine": "~5.1.0",
     "jasmine-core": "~5.9.0",
     "karma": "~6.4.0",
@@ -43,6 +47,15 @@
     "karma-coverage": "~2.2.0",
     "karma-jasmine": "~5.1.0",
     "karma-jasmine-html-reporter": "~2.1.0",
+    "ng-packagr": "^20.3.0",
     "typescript": "~5.9.2"
-  }
-}
+  },
+  "description": "This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.2.2.",
+  "main": "index.js",
+  "directories": {
+    "doc": "docs"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

+ 63 - 0
projects/inventory-assistant/README.md

@@ -0,0 +1,63 @@
+# InventoryAssistant
+
+This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
+
+## Code scaffolding
+
+Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
+
+```bash
+ng generate component component-name
+```
+
+For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
+
+```bash
+ng generate --help
+```
+
+## Building
+
+To build the library, run:
+
+```bash
+ng build inventory-assistant
+```
+
+This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
+
+### Publishing the Library
+
+Once the project is built, you can publish your library by following these steps:
+
+1. Navigate to the `dist` directory:
+   ```bash
+   cd dist/inventory-assistant
+   ```
+
+2. Run the `npm publish` command to publish your library to the npm registry:
+   ```bash
+   npm publish
+   ```
+
+## Running unit tests
+
+To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
+
+```bash
+ng test
+```
+
+## Running end-to-end tests
+
+For end-to-end (e2e) testing, run:
+
+```bash
+ng e2e
+```
+
+Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
+
+## Additional Resources
+
+For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

+ 7 - 0
projects/inventory-assistant/ng-package.json

@@ -0,0 +1,7 @@
+{
+  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+  "dest": "../../dist/inventory-assistant",
+  "lib": {
+    "entryFile": "src/public-api.ts"
+  }
+}

+ 12 - 0
projects/inventory-assistant/package.json

@@ -0,0 +1,12 @@
+{
+  "name": "inventory-assistant",
+  "version": "0.0.1",
+  "peerDependencies": {
+    "@angular/common": "^20.3.0",
+    "@angular/core": "^20.3.0"
+  },
+  "dependencies": {
+    "tslib": "^2.3.0"
+  },
+  "sideEffects": false
+}

+ 3 - 0
projects/inventory-assistant/src/lib/inventory-assistant.html

@@ -0,0 +1,3 @@
+<p>
+  inventory-assistant works!
+</p>

+ 0 - 0
projects/inventory-assistant/src/lib/inventory-assistant.scss


+ 22 - 0
projects/inventory-assistant/src/lib/inventory-assistant.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { InventoryAssistant } from './inventory-assistant.';
+
+describe('InventoryAssistant', () => {
+  let component: InventoryAssistant;
+  let fixture: ComponentFixture<InventoryAssistant>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [InventoryAssistant],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(InventoryAssistant);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 15 - 0
projects/inventory-assistant/src/lib/inventory-assistant.ts

@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'lib-inventory-assistant',
+  templateUrl: './inventory-assistant..html',
+  styleUrls: ['./inventory-assistant..scss'],
+  standalone: true,
+})
+export class InventoryAssistant  implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {}
+
+}

+ 5 - 0
projects/inventory-assistant/src/public-api.ts

@@ -0,0 +1,5 @@
+/*
+ * Public API Surface of inventory-assistant
+ */
+
+export * from './lib/inventory-assistant';

+ 18 - 0
projects/inventory-assistant/tsconfig.lib.json

@@ -0,0 +1,18 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../out-tsc/lib",
+    "declaration": true,
+    "declarationMap": true,
+    "inlineSources": true,
+    "types": []
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts"
+  ]
+}

+ 11 - 0
projects/inventory-assistant/tsconfig.lib.prod.json

@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "extends": "./tsconfig.lib.json",
+  "compilerOptions": {
+    "declarationMap": false
+  },
+  "angularCompilerOptions": {
+    "compilationMode": "partial"
+  }
+}

+ 15 - 0
projects/inventory-assistant/tsconfig.spec.json

@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../out-tsc/spec",
+    "types": [
+      "jasmine"
+    ]
+  },
+  "include": [
+    "src/**/*.d.ts",
+    "src/**/*.spec.ts"
+  ]
+}

+ 2 - 1
src/app/app.config.ts

@@ -2,11 +2,12 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChang
 import { provideRouter } from '@angular/router';
 
 import { routes } from './app.routes';
+import { provideIonicAngular } from '@ionic/angular/standalone';
 
 export const appConfig: ApplicationConfig = {
   providers: [
     provideBrowserGlobalErrorListeners(),
     provideZoneChangeDetection({ eventCoalescing: true }),
-    provideRouter(routes)
+    provideRouter(routes), provideIonicAngular({})
   ]
 };

Разница между файлами не показана из-за своего большого размера
+ 0 - 196
src/app/app.html


+ 26 - 1
src/app/app.routes.ts

@@ -1,3 +1,28 @@
 import { Routes } from '@angular/router';
+import { HomeComponent } from './components/home/home.component';
+import { AddItemComponent } from './components/add-item/add-item.component';
+import { MapComponent } from './components/map/map.component';
+import { ProfileComponent } from './components/profile/profile.component';
 
-export const routes: Routes = [];
+export const routes: Routes = [
+  {
+    path: '',
+    component: HomeComponent
+  },
+  {
+    path: 'add',
+    component: AddItemComponent
+  },
+  {
+    path: 'add/:id',
+    component: AddItemComponent
+  },
+  {
+    path: 'map',
+    component: MapComponent
+  },
+  {
+    path: 'profile',
+    component: ProfileComponent
+  }
+];

+ 84 - 0
src/app/app.scss

@@ -0,0 +1,84 @@
+.app-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  overflow: hidden;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
+}
+
+.main-content {
+  flex: 1;
+  padding-bottom: 72px; // 为底部导航栏留出空间
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch; // 为移动端添加平滑滚动
+}
+
+.bottom-nav {
+  position: fixed;
+  bottom: 16px;
+  left: 16px;
+  right: 16px;
+  height: 56px;
+  background-color: white;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  z-index: 1000;
+  border-radius: 28px;
+  padding: 0 16px;
+
+  .nav-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    text-decoration: none;
+    color: #666;
+    padding: 8px 0;
+    flex: 1;
+    transition: all 0.3s ease;
+    border: none;
+    background: none;
+    font-family: inherit;
+    position: relative;
+
+    &.router-link-active {
+      color: #2E7D32;
+      font-weight: 600;
+
+      .nav-icon {
+        transform: scale(1.1);
+      }
+
+      &::after {
+        content: '';
+        position: absolute;
+        top: -8px;
+        width: 4px;
+        height: 4px;
+        background-color: #2E7D32;
+        border-radius: 50%;
+        display: block;
+      }
+    }
+
+    .nav-icon {
+      font-size: 20px;
+      margin-bottom: 4px;
+      transition: transform 0.3s ease;
+    }
+
+    .nav-label {
+      font-size: 12px;
+      transition: all 0.3s ease;
+    }
+
+    &:hover {
+      color: #2E7D32;
+
+      .nav-icon {
+        transform: scale(1.1);
+      }
+    }
+  }
+}

+ 80 - 5
src/app/app.ts

@@ -1,12 +1,87 @@
-import { Component, signal } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+import { Component, OnInit } from '@angular/core';
+import { RouterOutlet, RouterLink } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { NotificationService } from './services/notification.service';
+import { ModalController, IonModal, IonInput, IonButton, IonTextarea, IonHeader, IonTitle, IonContent } from '@ionic/angular/standalone';
 
 @Component({
   selector: 'app-root',
-  imports: [RouterOutlet],
+  standalone: true,
+  imports: [RouterOutlet, RouterLink, IonModal, IonInput, IonButton, IonTextarea, IonHeader, IonTitle, IonContent],
   templateUrl: './app.html',
   styleUrl: './app.scss'
 })
-export class App {
-  protected readonly title = signal('home-assistant');
+export class AppComponent implements OnInit {
+
+  constructor(private notificationService: NotificationService, private modalCtrl: ModalController) {}
+
+  ngOnInit(): void {
+    // 初始化通知服务
+    this.notificationService.requestPermission();
+    this.notificationService.scheduleDailyCheck();
+  }
+
+  async openChatPanel() {
+    try {
+      // 创建智能助手模态框
+      const modal = await this.modalCtrl.create({
+        component: ChatModalComponent,
+        componentProps: {
+          title: '家庭物资收纳智能助手',
+          message: '你好,我是家庭物资收纳智能助手,有什么可以帮你的吗?'
+        }
+      });
+      
+      await modal.present();
+      
+      const { data } = await modal.onWillDismiss();
+      if (data && data.message) {
+        console.log('用户输入:', data.message);
+        // 这里可以添加智能体的逻辑
+        alert('智能助手正在处理你的请求...');
+      }
+    } catch (error) {
+      console.error('打开智能助手面板失败:', error);
+    }
+  }
+}
+
+// 智能助手聊天模态框组件
+@Component({
+  selector: 'app-chat-modal',
+  standalone: true,
+  imports: [IonHeader, IonTitle, IonContent, IonTextarea, IonButton, FormsModule],
+  template: `
+    <ion-header>
+      <ion-title>{{ title }}</ion-title>
+    </ion-header>
+    <ion-content class="ion-padding">
+      <p>{{ message }}</p>
+      <ion-textarea 
+        placeholder="请输入你的问题..." 
+        [(ngModel)]="userInput"
+        rows="4"
+        style="margin-top: 20px; margin-bottom: 20px;"
+      ></ion-textarea>
+      <div style="display: flex; justify-content: space-between;">
+        <ion-button (click)="cancel()">取消</ion-button>
+        <ion-button (click)="confirm()">发送</ion-button>
+      </div>
+    </ion-content>
+  `
+})
+export class ChatModalComponent {
+  title: string = '';
+  message: string = '';
+  userInput: string = '';
+
+  constructor(private modalCtrl: ModalController) {}
+
+  cancel() {
+    return this.modalCtrl.dismiss(null, 'cancel');
+  }
+
+  confirm() {
+    return this.modalCtrl.dismiss({ message: this.userInput }, 'confirm');
+  }
 }

+ 72 - 0
src/app/components/add-item/add-item.component.html

@@ -0,0 +1,72 @@
+<div class="add-item-container">
+  <h2>{{ isEditMode ? '编辑物品' : '添加物品' }}</h2>
+  
+  <form [formGroup]="itemForm" (ngSubmit)="onSubmit()">
+    <div class="form-group">
+      <label for="name">物品名称 *</label>
+      <input type="text" id="name" formControlName="name" placeholder="请输入物品名称">
+      <div *ngIf="itemForm.get('name')?.invalid && itemForm.get('name')?.touched" class="error-message">
+        物品名称不能为空
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="category">分类 *</label>
+      <select id="category" formControlName="category">
+        <option value="food">食品</option>
+        <option value="medicine">药品</option>
+        <option value="daily">日用品</option>
+      </select>
+    </div>
+
+    <div class="form-group">
+      <label for="location">存放位置 *</label>
+      <input type="text" id="location" formControlName="location" placeholder="请输入存放位置">
+      <div *ngIf="itemForm.get('location')?.invalid && itemForm.get('location')?.touched" class="error-message">
+        存放位置不能为空
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="quantity">数量 *</label>
+      <input type="number" id="quantity" formControlName="quantity" min="1">
+      <div *ngIf="itemForm.get('quantity')?.invalid && itemForm.get('quantity')?.touched" class="error-message">
+        数量必须大于0
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="productionDate">生产日期 *</label>
+      <input type="date" id="productionDate" formControlName="productionDate">
+      <div *ngIf="itemForm.get('productionDate')?.invalid && itemForm.get('productionDate')?.touched" class="error-message">
+        请选择生产日期
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="expiryDate">过期日期 *</label>
+      <input type="date" id="expiryDate" formControlName="expiryDate">
+      <div *ngIf="itemForm.get('expiryDate')?.invalid && itemForm.get('expiryDate')?.touched" class="error-message">
+        请选择过期日期
+      </div>
+      <div *ngIf="itemForm.hasError('dateInvalid')" class="error-message">
+        过期日期不能早于生产日期
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="remindDays">提前提醒天数 *</label>
+      <input type="number" id="remindDays" formControlName="remindDays" min="1" value="3">
+      <div *ngIf="itemForm.get('remindDays')?.invalid && itemForm.get('remindDays')?.touched" class="error-message">
+        提醒天数必须大于0
+      </div>
+    </div>
+
+    <div class="form-actions">
+      <button type="button" class="cancel-button" (click)="cancel()">取消</button>
+      <button type="submit" class="save-button" [disabled]="itemForm.invalid">
+        {{ isEditMode ? '保存修改' : '添加物品' }}
+      </button>
+    </div>
+  </form>
+</div>

+ 119 - 0
src/app/components/add-item/add-item.component.scss

@@ -0,0 +1,119 @@
+.add-item-container {
+  padding: 24px 16px;
+  max-width: 500px;
+  margin: 0 auto;
+  min-height: calc(100vh - 72px);
+}
+
+h2 {
+  color: #2E7D32;
+  margin-bottom: 32px;
+  text-align: center;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+form {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  animation: fadeIn 0.5s ease forwards;
+  opacity: 0;
+  transform: translateY(10px);
+
+  label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  input,
+  select {
+    padding: 14px 16px;
+    border: 1px solid #ddd;
+    border-radius: 16px;
+    font-size: 14px;
+    color: #333;
+    transition: all 0.3s ease;
+
+    &:focus {
+      outline: none;
+      border-color: #2E7D32;
+      box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2);
+      transform: translateY(-1px);
+    }
+  }
+
+  .error-message {
+    color: #D32F2F;
+    font-size: 12px;
+    margin-top: 4px;
+  }
+
+  &:nth-child(1) { animation-delay: 0.1s; }
+  &:nth-child(2) { animation-delay: 0.2s; }
+  &:nth-child(3) { animation-delay: 0.3s; }
+  &:nth-child(4) { animation-delay: 0.4s; }
+  &:nth-child(5) { animation-delay: 0.5s; }
+  &:nth-child(6) { animation-delay: 0.6s; }
+  &:nth-child(7) { animation-delay: 0.7s; }
+}
+
+.form-actions {
+  display: flex;
+  gap: 16px;
+  margin-top: 16px;
+  animation: fadeIn 0.5s ease 0.8s forwards;
+  opacity: 0;
+  transform: translateY(10px);
+
+  button {
+    flex: 1;
+    padding: 14px;
+    border: none;
+    border-radius: 16px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+
+    &:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+  }
+
+  .cancel-button {
+    background-color: #f5f5f5;
+    color: #333;
+
+    &:hover:not(:disabled) {
+      background-color: #e0e0e0;
+      transform: translateY(-1px);
+    }
+  }
+
+  .save-button {
+    background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
+
+    &:hover:not(:disabled) {
+      transform: translateY(-1px);
+      box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
+    }
+  }
+}
+
+@keyframes fadeIn {
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}

+ 104 - 0
src/app/components/add-item/add-item.component.ts

@@ -0,0 +1,104 @@
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { ItemService } from '../../services/item.service';
+import { ToastService } from '../../services/toast.service';
+
+@Component({
+  selector: 'app-add-item',
+  standalone: true,
+  imports: [ReactiveFormsModule, CommonModule],
+  templateUrl: './add-item.component.html',
+  styleUrl: './add-item.component.scss'
+})
+export class AddItemComponent implements OnInit {
+  itemForm: FormGroup;
+  isEditMode = false;
+  itemId: string | null = null;
+
+  constructor(
+    private fb: FormBuilder,
+    private route: ActivatedRoute,
+    private router: Router,
+    private itemService: ItemService,
+    private toastService: ToastService
+  ) {
+    this.itemForm = this.fb.group({
+      name: ['', Validators.required],
+      category: ['food', Validators.required],
+      location: ['', Validators.required],
+      quantity: [1, [Validators.required, Validators.min(1)]],
+      productionDate: ['', Validators.required],
+      expiryDate: ['', Validators.required],
+      remindDays: [3, [Validators.required, Validators.min(1)]]
+    }, {
+      validator: this.dateValidator
+    });
+  }
+
+  dateValidator(formGroup: FormGroup) {
+    const productionDate = formGroup.get('productionDate')?.value;
+    const expiryDate = formGroup.get('expiryDate')?.value;
+    
+    if (productionDate && expiryDate) {
+      if (new Date(expiryDate) < new Date(productionDate)) {
+        return { dateInvalid: true };
+      }
+    }
+    return null;
+  }
+
+  ngOnInit(): void {
+    this.itemId = this.route.snapshot.paramMap.get('id');
+    if (this.itemId) {
+      this.isEditMode = true;
+      this.loadItem();
+    }
+  }
+
+  loadItem() {
+    if (this.itemId) {
+      const item = this.itemService.getItemById(this.itemId);
+      if (item) {
+        this.itemForm.patchValue({
+          name: item.name,
+          category: item.category,
+          location: item.location,
+          quantity: item.quantity,
+          productionDate: new Date(item.productionDate).toISOString().split('T')[0],
+          expiryDate: new Date(item.expiryDate).toISOString().split('T')[0],
+          remindDays: item.remindDays
+        });
+      }
+    }
+  }
+
+  onSubmit() {
+    if (this.itemForm.valid) {
+      const formValue = this.itemForm.value;
+      
+      if (this.isEditMode && this.itemId) {
+        this.itemService.updateItem(this.itemId, {
+          ...formValue,
+          productionDate: new Date(formValue.productionDate),
+          expiryDate: new Date(formValue.expiryDate)
+        });
+        this.toastService.show('物品已更新');
+      } else {
+        this.itemService.addItem({
+          ...formValue,
+          productionDate: new Date(formValue.productionDate),
+          expiryDate: new Date(formValue.expiryDate)
+        });
+        this.toastService.show('物品已添加');
+      }
+      
+      this.router.navigate(['/']);
+    }
+  }
+
+  cancel() {
+    this.router.navigate(['/']);
+  }
+}

+ 119 - 0
src/app/components/home/home.component.html

@@ -0,0 +1,119 @@
+<div class="home-container">
+  <!-- 加载骨架屏 -->
+  <div *ngIf="loading" class="skeleton-container">
+    <div class="skeleton-statistics">
+      <div class="skeleton-stat-item">
+        <div class="skeleton-stat-value"></div>
+        <div class="skeleton-stat-label"></div>
+      </div>
+      <div class="skeleton-stat-item">
+        <div class="skeleton-stat-value"></div>
+        <div class="skeleton-stat-label"></div>
+      </div>
+      <div class="skeleton-stat-item">
+        <div class="skeleton-stat-value"></div>
+        <div class="skeleton-stat-label"></div>
+      </div>
+    </div>
+    
+    <div class="skeleton-header-actions">
+      <div class="skeleton-search"></div>
+      <div class="skeleton-add-button"></div>
+    </div>
+    
+    <div class="skeleton-items-list">
+      <div class="skeleton-item-card" *ngFor="let i of [1, 2, 3]">
+        <div class="skeleton-item-content">
+          <div class="skeleton-item-header">
+            <div class="skeleton-item-title"></div>
+            <div class="skeleton-category-badge"></div>
+          </div>
+          <div class="skeleton-item-details">
+            <div class="skeleton-detail-line"></div>
+            <div class="skeleton-detail-line"></div>
+            <div class="skeleton-detail-line"></div>
+          </div>
+        </div>
+        <div class="skeleton-item-actions">
+          <div class="skeleton-action-button"></div>
+          <div class="skeleton-action-button"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 实际内容 -->
+  <div *ngIf="!loading">
+    <!-- 统计摘要 -->
+    <div class="statistics">
+      <div class="stat-item">
+        <span class="stat-value">{{ statistics.total }}</span>
+        <span class="stat-label">总计</span>
+      </div>
+      <div class="stat-item">
+        <span class="stat-value expiring">{{ statistics.expiring }}</span>
+        <span class="stat-label">临期</span>
+      </div>
+      <div class="stat-item">
+        <span class="stat-value expired">{{ statistics.expired }}</span>
+        <span class="stat-label">过期</span>
+      </div>
+    </div>
+
+    <!-- 搜索框和操作按钮 -->
+    <div class="header-actions">
+      <div class="search-container">
+        <input 
+          type="text" 
+          class="search-input" 
+          placeholder="搜索物品名称、位置或分类" 
+          [(ngModel)]="searchKeyword" 
+          (input)="onSearchChange()"
+        >
+        <span class="search-icon">🔍</span>
+      </div>
+      <button class="add-button" (click)="addItem()">
+        ➕ 添加物品
+      </button>
+    </div>
+
+    <!-- 物品列表 -->
+    <div class="items-list">
+      <div 
+        *ngFor="let item of filteredItems" 
+        class="item-card" 
+        [class.expiring]="item.status === 'expiring'" 
+        [class.expired]="item.status === 'expired'"
+      >
+        <div class="item-content">
+          <div class="item-header">
+            <div class="item-title">
+              <span *ngIf="getStatusIcon(item.status)" class="status-icon" [class]="getStatusIconClass(item.status)">{{ getStatusIcon(item.status) }}</span>
+              <h3>{{ item.name }}</h3>
+            </div>
+            <span class="category-badge" [class]="item.category">{{ getCategoryLabel(item.category) }}</span>
+          </div>
+          <div class="item-details">
+            <p><strong>存放位置:</strong>{{ item.location }}</p>
+            <p><strong>数量:</strong>{{ item.quantity }}</p>
+            <p class="days-remaining">
+              <strong>
+                {{ getDaysRemaining(item.expiryDate) >= 0 ? '还剩 ' + getDaysRemaining(item.expiryDate) + ' 天' : '已过期 ' + getAbs(getDaysRemaining(item.expiryDate)) + ' 天' }}
+              </strong>
+            </p>
+          </div>
+        </div>
+        <div class="item-actions">
+          <button class="action-button edit" (click)="editItem(item.id)">编辑</button>
+          <button class="action-button delete" (click)="deleteItem(item.id)">删除</button>
+        </div>
+      </div>
+      
+      <div *ngIf="filteredItems.length === 0" class="empty-state">
+        <div class="empty-icon">📦</div>
+        <p>{{ searchKeyword ? '没有找到匹配的物品' : '还没有添加任何物品' }}</p>
+        <button *ngIf="!searchKeyword" class="add-button" (click)="addItem()">➕ 添加第一个物品</button>
+      </div>
+    </div>
+  </div>
+</div>

+ 497 - 0
src/app/components/home/home.component.scss

@@ -0,0 +1,497 @@
+.home-container {
+  padding: 16px;
+  max-width: 500px;
+  margin: 0 auto;
+  width: 100%;
+}
+
+.statistics {
+  display: flex;
+  justify-content: space-around;
+  margin-bottom: 24px;
+  background-color: white;
+  border-radius: 24px;
+  padding: 24px 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+
+  &:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
+  }
+
+  .stat-item {
+    text-align: center;
+    flex: 1;
+    padding: 0 12px;
+    transition: transform 0.3s ease;
+
+    &:hover {
+      transform: translateY(-2px);
+    }
+
+    .stat-value {
+      display: block;
+      font-size: 28px;
+      font-weight: 700;
+      color: #2E7D32;
+      animation: countUp 1s ease-out;
+
+      &.expiring {
+        color: #FFA000;
+      }
+
+      &.expired {
+        color: #D32F2F;
+      }
+    }
+
+    .stat-label {
+      font-size: 14px;
+      color: #666;
+      margin-top: 4px;
+      display: block;
+    }
+  }
+}
+
+@keyframes countUp {
+  from {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.header-actions {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 16px;
+  align-items: center;
+
+  .search-container {
+    flex: 1;
+    position: relative;
+
+    .search-input {
+      width: 100%;
+      padding: 10px 40px 10px 16px;
+      border: 1px solid #ddd;
+      border-radius: 24px;
+      font-size: 14px;
+      outline: none;
+
+      &:focus {
+        border-color: #2E7D32;
+        box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2);
+      }
+    }
+
+    .search-icon {
+      position: absolute;
+      right: 12px;
+      top: 50%;
+      transform: translateY(-50%);
+      color: #999;
+      font-size: 16px;
+    }
+  }
+
+  .add-button {
+    background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
+    color: white;
+    border: none;
+    border-radius: 50px;
+    padding: 12px 24px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.3s ease;
+
+    &:hover {
+      transform: scale(1.05);
+      box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
+    }
+  }
+}
+
+.items-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.item-card {
+  background-color: white;
+  border-radius: 20px;
+  padding: 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  position: relative;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 4px;
+    background-color: #2E7D32;
+    transition: all 0.3s ease;
+  }
+
+  &.expiring {
+    background-color: #FFF8E1;
+
+    &::before {
+      background-color: #FFA000;
+    }
+  }
+
+  &.expired {
+    background-color: #FFEBEE;
+
+    &::before {
+      background-color: #D32F2F;
+    }
+  }
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
+  }
+
+  .item-content {
+    flex: 1;
+    padding-left: 12px;
+  }
+
+  .item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+
+    .item-title {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .status-icon {
+        font-size: 16px;
+        animation: pulse 2s infinite;
+
+        &.expired-icon {
+          color: #D32F2F;
+        }
+      }
+
+      h3 {
+        margin: 0;
+        font-size: 16px;
+        font-weight: 600;
+        color: #333;
+      }
+    }
+
+    .category-badge {
+      padding: 4px 12px;
+      border-radius: 16px;
+      font-size: 12px;
+      font-weight: 600;
+      transition: all 0.3s ease;
+
+      &.food {
+        background-color: #E8F5E8;
+        color: #2E7D32;
+      }
+
+      &.medicine {
+        background-color: #E3F2FD;
+        color: #1976D2;
+      }
+
+      &.daily {
+        background-color: #FFF3E0;
+        color: #FF9800;
+      }
+    }
+  }
+
+  .item-details {
+    font-size: 14px;
+    color: #666;
+
+    p {
+      margin: 4px 0;
+    }
+
+    .days-remaining {
+      margin-top: 8px;
+      font-size: 12px;
+      font-weight: 500;
+    }
+  }
+
+  .item-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    margin-left: 16px;
+    opacity: 0;
+    transform: translateX(10px);
+    transition: all 0.3s ease;
+
+    .action-button {
+      border: none;
+      border-radius: 8px;
+      padding: 8px 16px;
+      font-size: 12px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.3s ease;
+
+      &.edit {
+        background-color: #E3F2FD;
+        color: #1976D2;
+
+        &:hover {
+          background-color: #BBDEFB;
+        }
+      }
+
+      &.delete {
+        background-color: #FFEBEE;
+        color: #D32F2F;
+
+        &:hover {
+          background-color: #FFCDD2;
+        }
+      }
+    }
+  }
+
+  &:hover .item-actions {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 64px 16px;
+  background-color: white;
+  border-radius: 20px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  text-align: center;
+  margin-top: 32px;
+
+  .empty-icon {
+    font-size: 48px;
+    margin-bottom: 16px;
+    color: #2E7D32;
+    opacity: 0.8;
+  }
+
+  p {
+    color: #666;
+    margin-bottom: 24px;
+    font-size: 16px;
+    line-height: 1.5;
+  }
+
+  .add-button {
+    margin-top: 8px;
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.1);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+
+/* 骨架屏样式 */
+.skeleton-container {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.skeleton-statistics {
+  display: flex;
+  justify-content: space-around;
+  background-color: white;
+  border-radius: 24px;
+  padding: 24px 16px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+
+  .skeleton-stat-item {
+    text-align: center;
+    flex: 1;
+    padding: 0 12px;
+
+    .skeleton-stat-value {
+      width: 40px;
+      height: 28px;
+      background-color: #e0e0e0;
+      border-radius: 4px;
+      margin: 0 auto 8px;
+      animation: skeleton-loading 1.5s ease-in-out infinite;
+    }
+
+    .skeleton-stat-label {
+      width: 30px;
+      height: 14px;
+      background-color: #e0e0e0;
+      border-radius: 4px;
+      margin: 0 auto;
+      animation: skeleton-loading 1.5s ease-in-out infinite 0.2s;
+    }
+  }
+}
+
+.skeleton-header-actions {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+
+  .skeleton-search {
+    flex: 1;
+    height: 40px;
+    background-color: #e0e0e0;
+    border-radius: 24px;
+    animation: skeleton-loading 1.5s ease-in-out infinite;
+  }
+
+  .skeleton-add-button {
+    width: 100px;
+    height: 40px;
+    background-color: #e0e0e0;
+    border-radius: 24px;
+    animation: skeleton-loading 1.5s ease-in-out infinite 0.2s;
+  }
+}
+
+.skeleton-items-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+
+  .skeleton-item-card {
+    background-color: white;
+    border-radius: 20px;
+    padding: 16px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    position: relative;
+    overflow: hidden;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      width: 4px;
+      background-color: #e0e0e0;
+    }
+
+    .skeleton-item-content {
+      flex: 1;
+      padding-left: 12px;
+    }
+
+    .skeleton-item-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 12px;
+
+      .skeleton-item-title {
+        width: 120px;
+        height: 16px;
+        background-color: #e0e0e0;
+        border-radius: 4px;
+        animation: skeleton-loading 1.5s ease-in-out infinite;
+      }
+
+      .skeleton-category-badge {
+        width: 60px;
+        height: 24px;
+        background-color: #e0e0e0;
+        border-radius: 16px;
+        animation: skeleton-loading 1.5s ease-in-out infinite 0.2s;
+      }
+    }
+
+    .skeleton-item-details {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+
+      .skeleton-detail-line {
+        height: 14px;
+        background-color: #e0e0e0;
+        border-radius: 4px;
+        animation: skeleton-loading 1.5s ease-in-out infinite 0.3s;
+
+        &:nth-child(2) {
+          width: 80%;
+          animation-delay: 0.4s;
+        }
+
+        &:nth-child(3) {
+          width: 60%;
+          animation-delay: 0.5s;
+        }
+      }
+    }
+
+    .skeleton-item-actions {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+      margin-left: 16px;
+
+      .skeleton-action-button {
+        width: 60px;
+        height: 32px;
+        background-color: #e0e0e0;
+        border-radius: 8px;
+        animation: skeleton-loading 1.5s ease-in-out infinite 0.6s;
+      }
+    }
+  }
+}
+
+@keyframes skeleton-loading {
+  0%, 100% {
+    opacity: 0.6;
+  }
+  50% {
+    opacity: 0.3;
+  }
+}

+ 121 - 0
src/app/components/home/home.component.ts

@@ -0,0 +1,121 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IItem } from '../../models/item.model';
+import { ItemService } from '../../services/item.service';
+import { ToastService } from '../../services/toast.service';
+
+@Component({
+  selector: 'app-home',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './home.component.html',
+  styleUrl: './home.component.scss'
+})
+export class HomeComponent implements OnInit {
+  items: IItem[] = [];
+  filteredItems: IItem[] = [];
+  searchKeyword: string = '';
+  loading: boolean = true;
+  statistics: { total: number; expiring: number; expired: number } = { total: 0, expiring: 0, expired: 0 };
+
+  constructor(private itemService: ItemService, private router: Router, private toastService: ToastService) {}
+
+  ngOnInit(): void {
+    this.loadItems();
+  }
+
+  loadItems() {
+    console.log('开始加载物品数据');
+    this.loading = true;
+    // 模拟加载延迟,以便显示骨架屏
+    setTimeout(() => {
+      // 从 ItemService 获取数据
+      this.items = this.itemService.getAllItems();
+      console.log('从服务获取的物品数据:', this.items);
+      // 排序:过期 > 临期 > 正常
+      this.items.sort((a, b) => {
+        const statusOrder = { expired: 0, expiring: 1, normal: 2 };
+        const statusDiff = statusOrder[a.status] - statusOrder[b.status];
+        if (statusDiff !== 0) {
+          return statusDiff;
+        }
+        // 同状态下按过期时间从近到远排序
+        return new Date(a.expiryDate).getTime() - new Date(b.expiryDate).getTime();
+      });
+      console.log('排序后的物品数据:', this.items);
+      // 计算统计数据
+      this.statistics = this.itemService.getStatistics();
+      console.log('统计数据:', this.statistics);
+      this.filterItems();
+      this.loading = false;
+    }, 800);
+  }
+
+  filterItems() {
+    console.log('开始过滤物品,搜索关键词:', this.searchKeyword);
+    if (!this.searchKeyword.trim()) {
+      this.filteredItems = this.items;
+    } else {
+      const keyword = this.searchKeyword.toLowerCase().trim();
+      this.filteredItems = this.items.filter(item => 
+        item.name.toLowerCase().includes(keyword) ||
+        item.location.toLowerCase().includes(keyword) ||
+        this.getCategoryLabel(item.category).toLowerCase().includes(keyword)
+      );
+    }
+    console.log('过滤后的物品数据:', this.filteredItems);
+  }
+
+  onSearchChange() {
+    this.filterItems();
+  }
+
+  getDaysRemaining(expiryDate: Date): number {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    const expiry = new Date(expiryDate);
+    expiry.setHours(0, 0, 0, 0);
+    const timeDiff = expiry.getTime() - today.getTime();
+    return Math.ceil(timeDiff / (1000 * 3600 * 24));
+  }
+
+  getCategoryLabel(category: string): string {
+    const labels = {
+      food: '食品',
+      medicine: '药品',
+      daily: '日用品'
+    };
+    return labels[category as keyof typeof labels] || category;
+  }
+
+  deleteItem(id: string) {
+    this.itemService.deleteItem(id);
+    this.loadItems();
+    this.toastService.show('物品已删除');
+  }
+
+  editItem(id: string) {
+    this.router.navigate(['/add', id]);
+  }
+
+  addItem() {
+    this.router.navigate(['/add']);
+  }
+
+  getAbs(value: number): number {
+    return Math.abs(value);
+  }
+
+  getStatusIcon(status: string): string {
+    if (status === 'expired') {
+      return '⚠️';
+    }
+    return '';
+  }
+
+  getStatusIconClass(status: string): string {
+    return status === 'expired' ? 'expired-icon' : '';
+  }
+}

+ 49 - 0
src/app/components/map/map.component.html

@@ -0,0 +1,49 @@
+<div class="map-container">
+  <h2>收纳地图</h2>
+  
+  <!-- 搜索框 -->
+  <div class="search-container">
+    <input 
+      type="text" 
+      class="search-input" 
+      placeholder="搜索物品名称、位置或分类" 
+      [(ngModel)]="searchKeyword" 
+      (input)="onSearchChange()"
+    >
+    <span class="search-icon">🔍</span>
+  </div>
+  
+  <div class="map-content">
+    <div *ngIf="filteredLocationNames.length === 0" class="empty-state">
+      <p>{{ searchKeyword ? '没有找到匹配的物品' : '还没有添加任何物品' }}</p>
+      <p>{{ searchKeyword ? '请尝试其他关键词' : '添加物品后会按位置显示在这里' }}</p>
+    </div>
+    
+    <div *ngFor="let location of filteredLocationNames" class="location-section">
+      <h3 class="location-name">{{ location }}</h3>
+      <div class="items-list">
+        <div 
+          *ngFor="let item of filteredLocations[location]" 
+          class="item-card" 
+          [class.expiring]="item.status === 'expiring'" 
+          [class.expired]="item.status === 'expired'"
+        >
+          <div class="item-content">
+            <div class="item-header">
+              <h4>{{ item.name }}</h4>
+              <span class="category-badge">{{ getCategoryLabel(item.category) }}</span>
+            </div>
+            <div class="item-details">
+              <p><strong>数量:</strong>{{ item.quantity }}</p>
+              <p class="days-remaining">
+                <strong>
+                  {{ getDaysRemaining(item.expiryDate) >= 0 ? '还剩 ' + getDaysRemaining(item.expiryDate) + ' 天' : '已过期 ' + getAbs(getDaysRemaining(item.expiryDate)) + ' 天' }}
+                </strong>
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 136 - 0
src/app/components/map/map.component.scss

@@ -0,0 +1,136 @@
+.map-container {
+  padding: 16px;
+  max-width: 500px;
+  margin: 0 auto;
+  width: 100%;
+}
+
+h2 {
+  color: #2E7D32;
+  margin-bottom: 24px;
+  text-align: center;
+}
+
+.search-container {
+  position: relative;
+  margin-bottom: 24px;
+
+  .search-input {
+    width: 100%;
+    padding: 10px 40px 10px 16px;
+    border: 1px solid #ddd;
+    border-radius: 24px;
+    font-size: 14px;
+    color: #333;
+    outline: none;
+
+    &:focus {
+      border-color: #2E7D32;
+      box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2);
+    }
+  }
+
+  .search-icon {
+    position: absolute;
+    right: 12px;
+    top: 50%;
+    transform: translateY(-50%);
+    color: #999;
+    font-size: 16px;
+  }
+}
+
+.map-content {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 48px 16px;
+  background-color: #f5f5f5;
+  border-radius: 12px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  p {
+    color: #666;
+    margin: 8px 0;
+  }
+}
+
+.location-section {
+  background-color: #f5f5f5;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  .location-name {
+    color: #2E7D32;
+    margin: 0 0 16px 0;
+    font-size: 18px;
+    border-bottom: 2px solid #E8F5E8;
+    padding-bottom: 8px;
+  }
+
+  .items-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .item-card {
+    background-color: white;
+    border-radius: 8px;
+    padding: 12px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    transition: all 0.3s ease;
+
+    &.expiring {
+      background-color: #FFF8E1;
+    }
+
+    &.expired {
+      background-color: #FFEBEE;
+    }
+
+    .item-content {
+      .item-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 8px;
+
+        h4 {
+          margin: 0;
+          font-size: 14px;
+          font-weight: bold;
+          color: #333;
+        }
+
+        .category-badge {
+          background-color: #E8F5E8;
+          color: #2E7D32;
+          padding: 2px 6px;
+          border-radius: 10px;
+          font-size: 10px;
+          font-weight: bold;
+        }
+      }
+
+      .item-details {
+        font-size: 12px;
+        color: #666;
+
+        p {
+          margin: 4px 0;
+        }
+
+        .days-remaining {
+          margin-top: 6px;
+          font-size: 11px;
+        }
+      }
+    }
+  }
+}

+ 98 - 0
src/app/components/map/map.component.ts

@@ -0,0 +1,98 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ItemService } from '../../services/item.service';
+import { IItem } from '../../models/item.model';
+
+@Component({
+  selector: 'app-map',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './map.component.html',
+  styleUrl: './map.component.scss'
+})
+export class MapComponent implements OnInit {
+  locations: { [key: string]: IItem[] } = {};
+  filteredLocations: { [key: string]: IItem[] } = {};
+  locationNames: string[] = [];
+  filteredLocationNames: string[] = [];
+  searchKeyword: string = '';
+
+  constructor(private itemService: ItemService) {}
+
+  ngOnInit(): void {
+    this.loadLocations();
+  }
+
+  loadLocations() {
+    const items = this.itemService.getAllItems();
+    this.locations = {};
+
+    // 按位置分组物品
+    items.forEach(item => {
+      if (!this.locations[item.location]) {
+        this.locations[item.location] = [];
+      }
+      this.locations[item.location].push(item);
+    });
+
+    // 获取所有位置名称
+    this.locationNames = Object.keys(this.locations);
+    this.filterLocations();
+  }
+
+  filterLocations() {
+    if (!this.searchKeyword.trim()) {
+      this.filteredLocations = { ...this.locations };
+      this.filteredLocationNames = [...this.locationNames];
+    } else {
+      const keyword = this.searchKeyword.toLowerCase().trim();
+      this.filteredLocations = {};
+      
+      // 过滤位置和物品
+      this.locationNames.forEach(location => {
+        const filteredItems = this.locations[location].filter(item => 
+          item.name.toLowerCase().includes(keyword) ||
+          item.location.toLowerCase().includes(keyword) ||
+          this.getCategoryLabel(item.category).toLowerCase().includes(keyword)
+        );
+        
+        if (filteredItems.length > 0) {
+          this.filteredLocations[location] = filteredItems;
+        }
+      });
+      
+      this.filteredLocationNames = Object.keys(this.filteredLocations);
+    }
+  }
+
+  onSearchChange() {
+    this.filterLocations();
+  }
+
+  getCategoryLabel(category: string): string {
+    const labels = {
+      food: '食品',
+      medicine: '药品',
+      daily: '日用品'
+    };
+    return labels[category as keyof typeof labels] || category;
+  }
+
+  getStatusClass(status: string): string {
+    return status === 'expiring' ? 'expiring' : status === 'expired' ? 'expired' : '';
+  }
+
+  getDaysRemaining(expiryDate: Date): number {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    const expiry = new Date(expiryDate);
+    expiry.setHours(0, 0, 0, 0);
+    const timeDiff = expiry.getTime() - today.getTime();
+    return Math.ceil(timeDiff / (1000 * 3600 * 24));
+  }
+
+  getAbs(value: number): number {
+    return Math.abs(value);
+  }
+}

+ 77 - 0
src/app/components/profile/profile.component.html

@@ -0,0 +1,77 @@
+<div class="profile-container">
+  <h2>我的</h2>
+  <div class="profile-content">
+    <!-- 个人信息卡片 -->
+    <div class="profile-section">
+      <h3>个人信息</h3>
+      
+      <div *ngIf="!isEditing">
+        <div class="info-item">
+          <span class="info-label">昵称:</span>
+          <span class="info-value">{{ userInfo?.nickname || '未设置' }}</span>
+        </div>
+        <div class="info-item">
+          <span class="info-label">性别:</span>
+          <span class="info-value">{{ userInfo?.gender === 'male' ? '男' : userInfo?.gender === 'female' ? '女' : '其他' }}</span>
+        </div>
+        <div class="info-item">
+          <span class="info-label">年龄:</span>
+          <span class="info-value">{{ userInfo?.age || '未设置' }}</span>
+        </div>
+        <button class="edit-button" (click)="startEditing()">编辑信息</button>
+      </div>
+      
+      <form *ngIf="isEditing" [formGroup]="userForm" (ngSubmit)="saveUserInfo()">
+        <div class="form-group">
+          <label for="nickname">昵称 *</label>
+          <input type="text" id="nickname" formControlName="nickname" placeholder="请输入昵称">
+          <div *ngIf="userForm.get('nickname')?.invalid && userForm.get('nickname')?.touched" class="error-message">
+            昵称不能为空
+          </div>
+        </div>
+        
+        <div class="form-group">
+          <label for="gender">性别 *</label>
+          <select id="gender" formControlName="gender">
+            <option value="male">男</option>
+            <option value="female">女</option>
+            <option value="other">其他</option>
+          </select>
+        </div>
+        
+        <div class="form-group">
+          <label for="age">年龄 *</label>
+          <input type="number" id="age" formControlName="age" min="0" max="150" placeholder="请输入年龄">
+          <div *ngIf="userForm.get('age')?.invalid && userForm.get('age')?.touched" class="error-message">
+            请输入有效的年龄
+          </div>
+        </div>
+        
+        <div class="form-actions">
+          <button type="button" class="cancel-button" (click)="cancelEditing()">取消</button>
+          <button type="submit" class="save-button" [disabled]="userForm.invalid">保存</button>
+        </div>
+      </form>
+    </div>
+
+    <!-- 应用信息 -->
+    <div class="profile-section">
+      <h3>关于应用</h3>
+      <p>家庭收纳助手 v1.0.0</p>
+      <p>帮助您管理家庭物资,及时提醒过期物品</p>
+    </div>
+
+    <!-- 设置 -->
+    <div class="profile-section">
+      <h3>设置</h3>
+      <div class="setting-item">
+        <span>通知权限</span>
+        <button class="setting-button" (click)="checkNotificationPermission()">检查权限</button>
+      </div>
+      <div class="setting-item">
+        <span>清空数据</span>
+        <button class="setting-button danger" (click)="clearAllData()">清空</button>
+      </div>
+    </div>
+  </div>
+</div>

+ 184 - 0
src/app/components/profile/profile.component.scss

@@ -0,0 +1,184 @@
+.profile-container {
+  padding: 16px;
+  max-width: 500px;
+  margin: 0 auto;
+  width: 100%;
+}
+
+h2 {
+  color: #2E7D32;
+  margin-bottom: 24px;
+  text-align: center;
+}
+
+.profile-content {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.profile-section {
+  background-color: #f5f5f5;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  h3 {
+    color: #2E7D32;
+    margin: 0 0 16px 0;
+  }
+
+  p {
+    color: #666;
+    margin: 8px 0;
+    font-size: 14px;
+  }
+
+  .info-item {
+    display: flex;
+    justify-content: space-between;
+    padding: 12px 0;
+    border-bottom: 1px solid #e0e0e0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .info-label {
+      color: #666;
+      font-size: 14px;
+    }
+
+    .info-value {
+      color: #333;
+      font-size: 14px;
+      font-weight: 500;
+    }
+  }
+
+  .edit-button {
+    margin-top: 16px;
+    width: 100%;
+    background-color: #2E7D32;
+    color: white;
+    border: none;
+    border-radius: 8px;
+    padding: 10px;
+    font-size: 14px;
+    font-weight: bold;
+    cursor: pointer;
+
+    &:hover {
+      background-color: #1B5E20;
+    }
+  }
+
+  .form-group {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    margin-bottom: 16px;
+
+    label {
+      font-size: 14px;
+      font-weight: bold;
+      color: #333;
+    }
+
+    input,
+    select {
+      padding: 10px;
+      border: 1px solid #ddd;
+      border-radius: 8px;
+      font-size: 14px;
+      color: #333;
+
+      &:focus {
+        outline: none;
+        border-color: #2E7D32;
+        box-shadow: 0 0 0 2px rgba(46, 125, 50, 0.2);
+      }
+    }
+
+    .error-message {
+      color: #D32F2F;
+      font-size: 12px;
+      margin-top: 4px;
+    }
+  }
+
+  .form-actions {
+    display: flex;
+    gap: 12px;
+    margin-top: 8px;
+
+    button {
+      flex: 1;
+      padding: 10px;
+      border: none;
+      border-radius: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      cursor: pointer;
+
+      &:disabled {
+        opacity: 0.6;
+        cursor: not-allowed;
+      }
+    }
+
+    .cancel-button {
+      background-color: #f5f5f5;
+      color: #333;
+
+      &:hover:not(:disabled) {
+        background-color: #e0e0e0;
+      }
+    }
+
+    .save-button {
+      background-color: #2E7D32;
+      color: white;
+
+      &:hover:not(:disabled) {
+        background-color: #1B5E20;
+      }
+    }
+  }
+}
+
+.setting-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 0;
+  border-bottom: 1px solid #e0e0e0;
+
+  &:last-child {
+    border-bottom: none;
+  }
+
+  span {
+    color: #333;
+    font-size: 14px;
+  }
+
+  .setting-button {
+    background-color: #E8F5E8;
+    color: #2E7D32;
+    border: none;
+    border-radius: 8px;
+    padding: 6px 12px;
+    font-size: 12px;
+    cursor: pointer;
+
+    &.danger {
+      background-color: #FFEBEE;
+      color: #D32F2F;
+    }
+
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+}

+ 74 - 0
src/app/components/profile/profile.component.ts

@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { UserService, UserInfo } from '../../services/user.service';
+import { NotificationService } from '../../services/notification.service';
+import { ItemService } from '../../services/item.service';
+
+@Component({
+  selector: 'app-profile',
+  standalone: true,
+  imports: [CommonModule, ReactiveFormsModule],
+  templateUrl: './profile.component.html',
+  styleUrl: './profile.component.scss'
+})
+export class ProfileComponent implements OnInit {
+  userInfo: UserInfo | null = null;
+  isEditing = false;
+  userForm: FormGroup;
+
+  constructor(
+    private userService: UserService,
+    private notificationService: NotificationService,
+    private itemService: ItemService,
+    private fb: FormBuilder
+  ) {
+    this.userForm = this.fb.group({
+      nickname: ['', Validators.required],
+      gender: ['other', Validators.required],
+      age: [0, [Validators.required, Validators.min(0), Validators.max(150)]]
+    });
+  }
+
+  ngOnInit(): void {
+    this.loadUserInfo();
+  }
+
+  loadUserInfo() {
+    this.userInfo = this.userService.getUserInfo();
+    if (this.userInfo) {
+      this.userForm.patchValue(this.userInfo);
+    }
+  }
+
+  startEditing() {
+    this.isEditing = true;
+  }
+
+  saveUserInfo() {
+    if (this.userForm.valid) {
+      this.userService.saveUserInfo(this.userForm.value);
+      this.userInfo = this.userForm.value;
+      this.isEditing = false;
+    }
+  }
+
+  cancelEditing() {
+    this.isEditing = false;
+    this.loadUserInfo();
+  }
+
+  checkNotificationPermission() {
+    this.notificationService.requestPermission().then(granted => {
+      alert(granted ? '通知权限已开启' : '通知权限已拒绝');
+    });
+  }
+
+  clearAllData() {
+    if (confirm('确定要清空所有数据吗?此操作不可恢复!')) {
+      localStorage.clear();
+      alert('数据已清空');
+      window.location.reload();
+    }
+  }
+}

+ 11 - 0
src/app/models/item.model.ts

@@ -0,0 +1,11 @@
+export interface IItem {
+  id: string;
+  name: string;
+  category: 'food' | 'medicine' | 'daily';
+  location: string;
+  quantity: number;
+  productionDate: Date;
+  expiryDate: Date;
+  remindDays: number;
+  status: 'normal' | 'expiring' | 'expired';
+}

+ 311 - 0
src/app/services/item.service.ts

@@ -0,0 +1,311 @@
+import { Injectable } from '@angular/core';
+import { IItem } from '../models/item.model';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ItemService {
+  private storageKey = 'home-assistant-items';
+
+  constructor() {
+    // 初始化测试数据
+    this.initTestData();
+  }
+
+  private initTestData(): void {
+    console.log('开始初始化测试数据');
+    try {
+      // 强制清空 localStorage 并重新初始化测试数据
+      if (typeof localStorage !== 'undefined') {
+        localStorage.removeItem(this.storageKey);
+        console.log('已清空 localStorage');
+        
+        const testItems: IItem[] = [
+          // 过期物品
+          {
+            id: '1',
+            name: '感冒药',
+            category: 'medicine',
+            location: '卧室抽屉 - 药箱',
+            quantity: 1,
+            productionDate: new Date('2025-01-01'),
+            expiryDate: new Date('2026-01-01'),
+            remindDays: 30,
+            status: 'expired'
+          },
+          {
+            id: '2',
+            name: '过期面包',
+            category: 'food',
+            location: '厨房储物柜 - 上层',
+            quantity: 1,
+            productionDate: new Date('2026-03-01'),
+            expiryDate: new Date('2026-03-10'),
+            remindDays: 3,
+            status: 'expired'
+          },
+          {
+            id: '3',
+            name: '旧牙膏',
+            category: 'daily',
+            location: '浴室洗漱台 - 左侧',
+            quantity: 1,
+            productionDate: new Date('2025-04-01'),
+            expiryDate: new Date('2026-04-01'),
+            remindDays: 30,
+            status: 'expired'
+          },
+          
+          // 临期物品
+          {
+            id: '4',
+            name: '面包',
+            category: 'food',
+            location: '厨房餐桌 - 面包篮',
+            quantity: 1,
+            productionDate: new Date('2026-03-30'),
+            expiryDate: new Date('2026-04-03'),
+            remindDays: 3,
+            status: 'expiring'
+          },
+          {
+            id: '5',
+            name: '鸡蛋',
+            category: 'food',
+            location: '冰箱冷藏室 - 蛋架',
+            quantity: 12,
+            productionDate: new Date('2026-03-28'),
+            expiryDate: new Date('2026-04-04'),
+            remindDays: 3,
+            status: 'expiring'
+          },
+          {
+            id: '6',
+            name: '酸奶',
+            category: 'food',
+            location: '冰箱冷藏室 - 下层',
+            quantity: 4,
+            productionDate: new Date('2026-03-29'),
+            expiryDate: new Date('2026-04-05'),
+            remindDays: 3,
+            status: 'expiring'
+          },
+          
+          // 正常物品(绿色系)
+          {
+            id: '7',
+            name: '牛奶',
+            category: 'food',
+            location: '冰箱冷藏室 - 门架',
+            quantity: 2,
+            productionDate: new Date('2026-03-15'),
+            expiryDate: new Date('2026-04-05'),
+            remindDays: 3,
+            status: 'normal'
+          },
+          {
+            id: '8',
+            name: '洗发水',
+            category: 'daily',
+            location: '浴室淋浴间 - 置物架',
+            quantity: 1,
+            productionDate: new Date('2026-01-01'),
+            expiryDate: new Date('2027-01-01'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '9',
+            name: '牙膏',
+            category: 'daily',
+            location: '浴室洗漱台 - 右侧',
+            quantity: 2,
+            productionDate: new Date('2026-02-01'),
+            expiryDate: new Date('2027-02-01'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '10',
+            name: '饼干',
+            category: 'food',
+            location: '厨房储物柜 - 中层',
+            quantity: 3,
+            productionDate: new Date('2026-02-15'),
+            expiryDate: new Date('2026-08-15'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '11',
+            name: '维生素C',
+            category: 'medicine',
+            location: '卧室抽屉 - 药箱',
+            quantity: 1,
+            productionDate: new Date('2025-12-01'),
+            expiryDate: new Date('2026-12-01'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '12',
+            name: '洗手液',
+            category: 'daily',
+            location: '厨房水槽 - 右侧',
+            quantity: 2,
+            productionDate: new Date('2026-01-15'),
+            expiryDate: new Date('2027-01-15'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '13',
+            name: '水果',
+            category: 'food',
+            location: '冰箱冷藏室 - 水果盒',
+            quantity: 5,
+            productionDate: new Date('2026-04-01'),
+            expiryDate: new Date('2026-04-06'),
+            remindDays: 3,
+            status: 'normal'
+          },
+          {
+            id: '14',
+            name: '洗洁精',
+            category: 'daily',
+            location: '厨房水槽 - 左侧',
+            quantity: 1,
+            productionDate: new Date('2026-01-01'),
+            expiryDate: new Date('2027-01-01'),
+            remindDays: 30,
+            status: 'normal'
+          },
+          {
+            id: '15',
+            name: '沐浴露',
+            category: 'daily',
+            location: '浴室淋浴间 - 置物架',
+            quantity: 1,
+            productionDate: new Date('2026-02-01'),
+            expiryDate: new Date('2027-02-01'),
+            remindDays: 30,
+            status: 'normal'
+          }
+        ];
+        this.saveItems(testItems);
+        console.log('测试数据已初始化:', testItems);
+      } else {
+        console.log('localStorage 不可用,可能在非浏览器环境');
+      }
+    } catch (error) {
+      console.error('初始化测试数据失败:', error);
+    }
+  }
+
+  private calculateStatus(item: IItem): 'normal' | 'expiring' | 'expired' {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    
+    const expiryDate = new Date(item.expiryDate);
+    expiryDate.setHours(0, 0, 0, 0);
+    
+    const timeDiff = expiryDate.getTime() - today.getTime();
+    const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
+    
+    if (daysDiff < 0) {
+      return 'expired';
+    } else if (daysDiff <= item.remindDays) {
+      return 'expiring';
+    } else {
+      return 'normal';
+    }
+  }
+
+  private getItems(): IItem[] {
+    const itemsJson = localStorage.getItem(this.storageKey);
+    return itemsJson ? JSON.parse(itemsJson) : [];
+  }
+
+  private saveItems(items: IItem[]): void {
+    localStorage.setItem(this.storageKey, JSON.stringify(items));
+  }
+
+  getAllItems(): IItem[] {
+    const items = this.getItems();
+    // 自动更新状态并按过期时间排序
+    return items
+      .map(item => ({
+        ...item,
+        status: this.calculateStatus(item)
+      }))
+      .sort((a, b) => new Date(a.expiryDate).getTime() - new Date(b.expiryDate).getTime());
+  }
+
+  getItemById(id: string): IItem | undefined {
+    const items = this.getItems();
+    const item = items.find(item => item.id === id);
+    if (item) {
+      return {
+        ...item,
+        status: this.calculateStatus(item)
+      };
+    }
+    return undefined;
+  }
+
+  addItem(item: Omit<IItem, 'id' | 'status'>): IItem {
+    const newItem: IItem = {
+      ...item,
+      id: Date.now().toString(),
+      status: this.calculateStatus({ ...item, id: '', status: 'normal' })
+    };
+    const items = this.getItems();
+    items.push(newItem);
+    this.saveItems(items);
+    return newItem;
+  }
+
+  updateItem(id: string, updatedItem: Partial<IItem>): IItem | undefined {
+    const items = this.getItems();
+    const index = items.findIndex(item => item.id === id);
+    if (index !== -1) {
+      const item = {
+        ...items[index],
+        ...updatedItem,
+        id
+      };
+      item.status = this.calculateStatus(item);
+      items[index] = item;
+      this.saveItems(items);
+      return item;
+    }
+    return undefined;
+  }
+
+  deleteItem(id: string): boolean {
+    const items = this.getItems();
+    const filteredItems = items.filter(item => item.id !== id);
+    if (filteredItems.length !== items.length) {
+      this.saveItems(filteredItems);
+      return true;
+    }
+    return false;
+  }
+
+  getExpiringItems(): IItem[] {
+    return this.getAllItems().filter(item => item.status === 'expiring');
+  }
+
+  getExpiredItems(): IItem[] {
+    return this.getAllItems().filter(item => item.status === 'expired');
+  }
+
+  getStatistics() {
+    const items = this.getAllItems();
+    return {
+      total: items.length,
+      expiring: items.filter(item => item.status === 'expiring').length,
+      expired: items.filter(item => item.status === 'expired').length
+    };
+  }
+}

+ 66 - 0
src/app/services/notification.service.ts

@@ -0,0 +1,66 @@
+import { Injectable, OnInit } from '@angular/core';
+import { ItemService } from './item.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class NotificationService {
+
+  constructor(private itemService: ItemService) {}
+
+  async requestPermission(): Promise<boolean> {
+    if (!('Notification' in window)) {
+      console.log('This browser does not support notifications');
+      return false;
+    }
+
+    if (Notification.permission === 'granted') {
+      return true;
+    }
+
+    if (Notification.permission !== 'denied') {
+      const permission = await Notification.requestPermission();
+      return permission === 'granted';
+    }
+
+    return false;
+  }
+
+  async checkAndSendNotifications() {
+    const hasPermission = await this.requestPermission();
+    if (!hasPermission) {
+      return;
+    }
+
+    const expiringItems = this.itemService.getExpiringItems();
+    if (expiringItems.length > 0) {
+      const message = `您有 ${expiringItems.length} 件物品即将过期,请及时处理!`;
+      new Notification('家庭收纳助手提醒', {
+        body: message,
+        icon: 'favicon.ico'
+      });
+    }
+  }
+
+  scheduleDailyCheck() {
+    // 每天上午9点检查
+    this.checkAndSendNotifications();
+    
+    // 计算下一次检查的时间
+    const now = new Date();
+    const nextCheck = new Date(now);
+    nextCheck.setHours(9, 0, 0, 0);
+    
+    if (nextCheck <= now) {
+      // 今天已经过了9点,设置为明天9点
+      nextCheck.setDate(nextCheck.getDate() + 1);
+    }
+    
+    const timeUntilNextCheck = nextCheck.getTime() - now.getTime();
+    
+    // 定时执行
+    setTimeout(() => {
+      this.scheduleDailyCheck();
+    }, timeUntilNextCheck);
+  }
+}

+ 43 - 0
src/app/services/toast.service.ts

@@ -0,0 +1,43 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ToastService {
+  private toastElement: HTMLDivElement | null = null;
+
+  show(message: string, duration: number = 3000): void {
+    // 移除现有的Toast
+    this.hide();
+
+    // 创建Toast元素
+    this.toastElement = document.createElement('div');
+    this.toastElement.className = 'toast';
+    this.toastElement.textContent = message;
+    document.body.appendChild(this.toastElement);
+
+    // 添加显示动画
+    setTimeout(() => {
+      if (this.toastElement) {
+        this.toastElement.classList.add('show');
+      }
+    }, 10);
+
+    // 自动隐藏
+    setTimeout(() => {
+      this.hide();
+    }, duration);
+  }
+
+  hide(): void {
+    if (this.toastElement) {
+      this.toastElement.classList.remove('show');
+      setTimeout(() => {
+        if (this.toastElement && this.toastElement.parentNode) {
+          this.toastElement.parentNode.removeChild(this.toastElement);
+          this.toastElement = null;
+        }
+      }, 300);
+    }
+  }
+}

+ 41 - 0
src/app/services/user.service.ts

@@ -0,0 +1,41 @@
+import { Injectable } from '@angular/core';
+
+export interface UserInfo {
+  nickname: string;
+  gender: 'male' | 'female' | 'other';
+  age: number;
+  avatar?: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UserService {
+  private storageKey = 'home-assistant-user';
+
+  constructor() {}
+
+  getUserInfo(): UserInfo | null {
+    const userJson = localStorage.getItem(this.storageKey);
+    return userJson ? JSON.parse(userJson) : null;
+  }
+
+  saveUserInfo(userInfo: UserInfo): void {
+    localStorage.setItem(this.storageKey, JSON.stringify(userInfo));
+  }
+
+  updateUserInfo(updatedInfo: Partial<UserInfo>): UserInfo {
+    const currentInfo = this.getUserInfo() || {
+      nickname: '用户',
+      gender: 'other',
+      age: 0
+    };
+    const newInfo = { ...currentInfo, ...updatedInfo };
+    this.saveUserInfo(newInfo);
+    return newInfo;
+  }
+
+  resetUserInfo(): void {
+    localStorage.removeItem(this.storageKey);
+  }
+}

+ 2 - 2
src/main.ts

@@ -1,6 +1,6 @@
 import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app';
 import { appConfig } from './app/app.config';
-import { App } from './app/app';
 
-bootstrapApplication(App, appConfig)
+bootstrapApplication(AppComponent, appConfig)
   .catch((err) => console.error(err));

+ 94 - 1
src/styles.scss

@@ -1 +1,94 @@
-/* You can add global styles to this file, and also import other style files */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+/* 全局样式重置 */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  transition: all 0.2s ease;
+}
+
+body {
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  font-size: 16px;
+  line-height: 1.5;
+  color: #333;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
+  min-height: 100vh;
+  overflow-x: hidden;
+  scroll-behavior: smooth;
+}
+
+/* 主色调 */
+:root {
+  --primary-color: #2E7D32;
+  --primary-dark: #1B5E20;
+  --primary-light: #4CAF50;
+  --expiring-color: #FFF8E1;
+  --expired-color: #FFEBEE;
+  --text-primary: #333;
+  --text-secondary: #666;
+  --background-light: #f8f8f8;
+  --white: #ffffff;
+  --border-radius: 12px;
+  --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* 通用按钮样式 */
+button {
+  font-family: inherit;
+  cursor: pointer;
+}
+
+/* 通用输入框样式 */
+input, select {
+  font-family: inherit;
+}
+
+/* 响应式设计 */
+@media (max-width: 500px) {
+  body {
+    font-size: 14px;
+  }
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+  width: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #2E7D32;
+  border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #1B5E20;
+}
+
+/* Toast 样式 */
+.toast {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  background-color: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 12px 20px;
+  border-radius: 20px;
+  font-size: 14px;
+  font-weight: 500;
+  z-index: 9999;
+  opacity: 0;
+  transform: translateX(100%);
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+.toast.show {
+  opacity: 1;
+  transform: translateX(0);
+}

+ 10 - 0
src/theme/variables.css

@@ -0,0 +1,10 @@
+/**
+ * Ionic Dark Theme
+ * -----------------------------------------------------
+ * For more info, please see:
+ * https://ionicframework.com/docs/theming/dark-mode
+ */
+
+/* @import "@ionic/angular/css/palettes/dark.always.css"; */
+/* @import "@ionic/angular/css/palettes/dark.class.css"; */
+@import "@ionic/angular/css/palettes/dark.system.css";

+ 11 - 0
tsconfig.json

@@ -5,6 +5,11 @@
   "compilerOptions": {
     "strict": true,
     "noImplicitOverride": true,
+    "paths": {
+      "inventory-assistant": [
+        "./dist/inventory-assistant"
+      ]
+    },
     "noPropertyAccessFromIndexSignature": true,
     "noImplicitReturns": true,
     "noFallthroughCasesInSwitch": true,
@@ -29,6 +34,12 @@
     },
     {
       "path": "./tsconfig.spec.json"
+    },
+    {
+      "path": "./projects/inventory-assistant/tsconfig.lib.json"
+    },
+    {
+      "path": "./projects/inventory-assistant/tsconfig.spec.json"
     }
   ]
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов