Skip to content

feat:add card list menu#136

Merged
kagol merged 18 commits intoopentiny:devfrom
zhaoxiaofeng876:zhaoxiaofeng-card
Nov 28, 2025
Merged

feat:add card list menu#136
kagol merged 18 commits intoopentiny:devfrom
zhaoxiaofeng876:zhaoxiaofeng-card

Conversation

@zhaoxiaofeng876
Copy link
Contributor

@zhaoxiaofeng876 zhaoxiaofeng876 commented Nov 26, 2025

PR

效果图:

1920px 分辨率:

image

1366px 分辨率:

image

手机端效果:

image

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Card List UI for service applications: paginated, searchable, classification filters, responsive layout, icons, and pager behavior
    • Frontend route, image component and API integration to load application data
    • Backend module, service, DTOs, seed data and mock endpoints to provide application records
    • English/Chinese translations for Card List
  • Bug Fixes

    • Improved application initialization flow and startup logging

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 26, 2025

Walkthrough

Introduces a Card List feature across frontend and backend: new Application TypeORM model and models export; NestJS module/controller/service/DTO and seed/mock data with AppModule seeding; menu and locale entries; TinyVue API, route, components, and TypeScript config updates.

Changes

Cohort / File(s) Summary
Models & Barrel Export
template/nestJs/libs/models/src/application.ts, template/nestJs/libs/models/src/index.ts
Adds TagEnum and Application TypeORM entity (id, name, description, icon, tag, classify); re-exports ./application.
Backend Module / Controller / Service
template/nestJs/src/application/application.module.ts, template/nestJs/src/application/application.controller.ts, template/nestJs/src/application/application.service.ts
Adds ApplicationModule (TypeOrmModule.forFeature([Application])), ApplicationController GET endpoint, and ApplicationService with findAllApplication (keyword search, optional classify filter, pagination, safe tag parsing) and createApplication (duplicate check, init-mode handling).
DTO & Seed Data
template/nestJs/src/application/dto/pagination-query.dto.ts, template/nestJs/src/application/init/data.ts
Adds PaginationQueryDto (page, limit, classify, keywords with class-transformer/validator) and applicationData seed array of application objects.
AppModule Integration
template/nestJs/src/app.module.ts
Registers ApplicationModule, injects ApplicationService, and onModuleInit iterates applicationData calling createApplication for each item with error handling.
Mock Data & Composition
template/nestJs/src/mock/data/application.ts, template/nestJs/src/mock/data/index.ts
Adds mock generator and GET /api/application handler with pagination; imports and spreads application mocks into mock data index.
Menu Data & Service Mapping
template/nestJs/src/menu/init/menuData.ts, template/nestJs/src/menu/menu.service.ts, template/springboot/src/main/resources/MenuData.json
Appends Card menu entry (path card, component list/card-list/index, locale menu.list.cardList) and updates a parentId mapping in menu service; mirrors menu addition in SpringBoot MenuData.json.
Localization (NestJS & TinyVue)
template/nestJs/locales.json, template/tinyvue/src/locales.json
Adds English and Chinese translation keys for card list UI: menu.list.cardList, cardList.title, cardList.options.*, and cardList.search.placeholder.
TinyVue API & Router
template/tinyvue/src/api/card.ts, template/tinyvue/src/router/routes/modules/list.ts
Adds QueryTaskParams and getServicesList GET call to /application; adds card route with lazy-loaded card-list view and roles [RoleType.admin, RoleType.user].
TinyVue Components & Config
template/tinyvue/src/views/list/card-list/index.vue, template/tinyvue/src/views/list/card-list/components/image.vue, template/tinyvue/tsconfig.json, template/tinyvue/package.json
Adds card-list view with filters, search, pagination, resize-aware pager and async data loading; adds image component for 32×32 icons; adds resize-observer-browser to tsconfig types and devDependencies.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant FE as TinyVue CardList
    participant API as NestJS Controller
    participant Svc as ApplicationService
    participant DB as Database

    User->>FE: Open Card List view
    FE->>FE: onMounted → fetchData()
    FE->>API: GET /api/application?page=&limit=&classify=&keywords=
    API->>Svc: findAllApplication(dto)
    Svc->>Svc: build query (keywords, classify, pagination)
    Svc->>DB: execute paginated query
    DB-->>Svc: rows + total
    Svc->>Svc: parse tag JSON safely
    Svc-->>API: { total, data }
    API-->>FE: response
    FE->>FE: update cards UI, stop loading
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect ApplicationService: query construction (LIKE patterns, classify filter), pagination offset calculation, and safe JSON tag parsing.
  • Verify createApplication behavior during AppModule seeding (idempotence and error/exit handling).
  • Check DTO decorators and defaults match frontend requests.
  • Validate mock route pagination logic and response wrapping.
  • Confirm frontend asset URL resolution in image component and ResizeObserver typing/config.

Poem

🐰 I nibble seeds and shuffle rows,
Tiny cards and icon glows,
Backend hums, the frontend sings,
Data hops on gentle springs,
A rabbit cheers — the feature grows.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add card list menu' accurately and concisely describes the primary change - adding a card list menu feature to the project.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebf1c7f and eeb18f6.

📒 Files selected for processing (1)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • template/tinyvue/src/views/list/card-list/index.vue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
template/nestJs/src/menu/menu.service.ts (1)

114-141: Refactor hardcoded array indices to use a more maintainable approach.

The handleMenuParentId method relies on hardcoded array indices to establish parent-child relationships. This approach is fragile—adding, removing, or reordering menu items will break these mappings and cause runtime errors.

Consider refactoring to match menus by a stable identifier (e.g., name, path, or locale) rather than array position.

Example refactor using name-based matching:

 async handleMenuParentId(menuId: number[]) {
   const menu = await this.menu.find();
+  const menuMap = new Map(menu.map(m => [m.name, m]));
+  const menuIdMap = new Map(menu.map((m, idx) => [m.name, menuId[idx]]));
+  
   if (menu) {
-    menu[1].parentId = menuId[0];
-    menu[2].parentId = menuId[0];
-    menu[4].parentId = menuId[3];
+    menuMap.get('Home').parentId = menuIdMap.get('Board');
+    menuMap.get('Work').parentId = menuIdMap.get('Board');
+    menuMap.get('Table').parentId = menuIdMap.get('List');
+    menuMap.get('Card').parentId = menuIdMap.get('List');
     // ... continue pattern for remaining entries
   }
🧹 Nitpick comments (10)
template/springboot/src/main/resources/MenuData.json (1)

49-58: Minor inconsistency: explicit parentId: null in nested structure.

The Card entry includes an explicit "parentId": null field, while its sibling menu item Table (lines 40-47) omits the parentId field entirely. In a nested JSON structure, the parent relationship is implicit, making the explicit parentId field redundant.

For consistency, consider either removing the parentId field or adding it to all sibling menu items.

Apply this diff to remove the redundant field:

     {
       "name": "Card",
       "order": 2,
-      "parentId": null,
       "menuType": "normal",
       "icon": "",
       "component": "list/card-list/index",
       "path": "card",
       "locale": "menu.list.cardList"
     }
template/nestJs/src/application/init/data.ts (1)

6-6: Consider using actual JSON for the tag field instead of string-encoded JSON.

The tag field stores JSON arrays as strings (e.g., '[{ "type": "", "value": "..." }]'). While this may be intentional for database storage, using actual arrays would improve type safety and eliminate the need for runtime parsing.

If database constraints require string storage, consider defining a proper type/interface for tag items and documenting why string encoding is necessary.

Example with proper typing:

interface TagItem {
  type: '' | 'success' | 'info' | 'danger' | 'warning';
  value: string;
}

export const applicationData = [
  {
    name: 'Tiny Design 设计体系',
    description: '...',
    tag: [{ type: '', value: '机会点定义' }, { type: 'danger', value: '交互设计' }] as TagItem[],
    // ... or convert to JSON string in service layer before persisting
  },
  // ...
];

Also applies to: 14-14, 21-21, 29-29, 37-37, 45-45, 53-53, 60-60, 68-68, 76-76, 84-84

template/tinyvue/src/api/card.ts (1)

9-13: Consider adding response type and error handling.

The function lacks a return type annotation and doesn't handle potential API errors. Consider typing the response for better type safety.

-export function getServicesList(params: QueryTaskParmas) {
+export interface ApplicationResponse {
+  data: Array<{
+    id: string;
+    name: string;
+    description: string;
+    icon: string;
+    tag: Array<{ type: string; value: string }>;
+  }>;
+  total: number;
+}
+
+export function getServicesList(params: QueryTaskParams): Promise<{ data: ApplicationResponse }> {
   return axios.get(`${import.meta.env.VITE_BASE_API}/application`, {
     params,
   });
 }
template/nestJs/src/mock/data/application.ts (1)

21-22: Misleading variable name treeData.

The variable treeData stores a flat paginated slice, not tree-structured data. Consider renaming to paginatedData or pageData for clarity.

-let treeData = [];
+let paginatedData = [];
template/tinyvue/src/views/list/card-list/components/image.vue (1)

5-5: Unnecessary import of defineProps.

In <script setup>, defineProps is a compiler macro and doesn't need to be explicitly imported from 'vue'.

-  import { computed, defineProps } from 'vue';
+  import { computed } from 'vue';
template/nestJs/libs/models/src/application.ts (1)

11-25: Consider adding column constraints and documentation.

The entity columns lack length constraints, which may cause issues with different databases or allow unexpectedly long values.

 @Entity('application')
 export class Application {
   @PrimaryGeneratedColumn()
   id: number;
-  @Column()
+  @Column({ length: 255 })
   name: string;
-  @Column()
+  @Column({ type: 'text' })
   description: string;
-  @Column()
+  @Column({ length: 255 })
   icon: string;
-  @Column()
+  @Column({ type: 'text' })
   tag: string;
-  @Column()
+  @Column({ length: 50 })
   classify: string;
 }
template/tinyvue/src/views/list/card-list/index.vue (1)

80-88: Use const instead of let for ref declarations.

Refs should be declared with const because the ref wrapper object itself is never reassigned—only its .value property is mutated.

-  let cards = ref([]);
-  let cardLoadingState = ref(null);
+  const cards = ref([]);
+  const cardLoadingState = ref<any>(null);

-  let pager = ref({
+  const pager = ref({
     currentPage: 1,
     pageSize: 10,
     total: 2,
     pageSizes: [10, 20, 50],
   });
template/nestJs/src/application/application.service.ts (2)

49-50: Add type annotation for dto parameter.

The dto parameter lacks a type, reducing type safety and IDE support. Consider creating a CreateApplicationDto class.

-  async createApplication(dto, isInit: boolean) {
+  async createApplication(dto: CreateApplicationDto, isInit: boolean) {

30-32: Handle undefined/null classify value.

Currently, if classify is undefined or null, the condition classify !== 'all' is true, and the query will filter by an undefined value, potentially causing unexpected results.

-    if (classify !== 'all') {
+    if (classify && classify !== 'all') {
       queryBuilder.andWhere('application.classify= :classify', { classify });
     }
template/nestJs/src/application/dto/pagination-query.dto.ts (1)

11-16: Default limit of 1 is too low for pagination.

A default of 1 item per page will result in excessive API calls and poor user experience. Consider a more practical default like 10 or 20.

   @IsOptional()
   @Type(() => Number)
   @IsInt()
   @Min(1)
   @Max(100)
-  limit: number = 1;
+  limit: number = 10;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9e423e and c7f3ef7.

📒 Files selected for processing (19)
  • template/nestJs/libs/models/src/application.ts (1 hunks)
  • template/nestJs/libs/models/src/index.ts (1 hunks)
  • template/nestJs/locales.json (3 hunks)
  • template/nestJs/src/app.module.ts (5 hunks)
  • template/nestJs/src/application/application.controller.ts (1 hunks)
  • template/nestJs/src/application/application.module.ts (1 hunks)
  • template/nestJs/src/application/application.service.ts (1 hunks)
  • template/nestJs/src/application/dto/pagination-query.dto.ts (1 hunks)
  • template/nestJs/src/application/init/data.ts (1 hunks)
  • template/nestJs/src/menu/init/menuData.ts (1 hunks)
  • template/nestJs/src/menu/menu.service.ts (1 hunks)
  • template/nestJs/src/mock/data/application.ts (1 hunks)
  • template/nestJs/src/mock/data/index.ts (1 hunks)
  • template/springboot/src/main/resources/MenuData.json (1 hunks)
  • template/tinyvue/src/api/card.ts (1 hunks)
  • template/tinyvue/src/locales.json (3 hunks)
  • template/tinyvue/src/router/routes/modules/list.ts (1 hunks)
  • template/tinyvue/src/views/list/card-list/components/image.vue (1 hunks)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
template/nestJs/src/application/application.module.ts (1)
template/nestJs/src/app.module.ts (1)
  • Module (42-233)
template/nestJs/src/application/application.service.ts (2)
template/nestJs/src/.generate/i18n.generated.ts (1)
  • I18nTranslations (7-58)
template/nestJs/src/application/dto/pagination-query.dto.ts (1)
  • PaginationQueryDto (4-23)
template/nestJs/src/app.module.ts (1)
template/nestJs/src/application/init/data.ts (1)
  • applicaitonData (1-88)
template/nestJs/src/application/application.controller.ts (1)
template/nestJs/src/application/dto/pagination-query.dto.ts (1)
  • PaginationQueryDto (4-23)
🔇 Additional comments (8)
template/nestJs/libs/models/src/index.ts (1)

7-7: LGTM!

The barrel export follows the established pattern and properly exposes the Application model module.

template/nestJs/src/menu/init/menuData.ts (1)

282-291: LGTM!

The new Card menu entry follows the established pattern for menu initialization. The parentId: null value is intentional and will be updated by handleMenuParentId in the menu service.

template/nestJs/src/application/application.module.ts (1)

1-13: LGTM!

The module follows NestJS best practices: properly registers the TypeORM repository for the Application entity, wires the controller and service, and exports the service for consumption by other modules (e.g., AppModule).

template/nestJs/src/application/application.controller.ts (1)

1-13: LGTM!

The controller follows NestJS conventions: properly injects the service, uses DTO for query parameter validation, and delegates to the service layer. The implementation is clean and straightforward.

template/nestJs/src/mock/data/index.ts (1)

6-15: LGTM!

The application mock data is properly integrated following the existing pattern used for other mock data modules.

template/nestJs/src/app.module.ts (1)

183-193: Application initialization follows established patterns.

The application initialization logic correctly mirrors the existing menu initialization pattern with proper error handling and exit on failure.

template/tinyvue/src/views/list/card-list/index.vue (1)

42-50: LGTM on pagination implementation.

The pagination component is properly wired with current page, total, page size, and handlers for page/size changes.

template/nestJs/libs/models/src/application.ts (1)

3-9: I'm unable to clone the repository to perform verification in the sandbox environment. However, based on the information provided in the review comment and code snippet, I can assess what we know:

The review comment states:

  • TagEnum is defined (lines 3-9)
  • The tag field on line 22 is typed as string, not using the enum
  • Init data shows tag stores a JSON-encoded array of objects

Without access to the full codebase, I cannot definitively verify whether TagEnum is used elsewhere or confirm the actual implementation details. This requires manual verification by examining the codebase directly.


TagEnum usage should be verified and either utilized or removed.

The enum is defined but the tag field is typed as string rather than using TagEnum. Verify whether TagEnum is referenced elsewhere in the codebase; if unused, remove it. If intended for type safety on the tag field, refactor the field definition to use the enum type appropriately.

Comment on lines 38 to 40
import { ApplicationModule } from './application/application.module';
import { ApplicationService } from './application/application.service';
import { applicaitonData } from './application/init/data';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typos in variable and import names.

There are consistent typos: applicaiton should be application, and applicaitonData should be applicationData. These typos reduce code readability and may cause confusion.

 import { ApplicationModule } from './application/application.module';
 import { ApplicationService } from './application/application.service';
-import { applicaitonData } from './application/init/data';
+import { applicationData } from './application/init/data';

Also update the constructor parameter on line 90:

-    private applicaiton: ApplicationService
+    private application: ApplicationService

And the usage on line 186:

-        await this.applicaiton.createApplication(item, isInit);
+        await this.application.createApplication(item, isInit);

Note: This also requires renaming the export in template/nestJs/src/application/init/data.ts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In template/nestJs/src/app.module.ts around lines 38-40 (and update references
at line 90 and line 186), fix the consistent typos: rename the imported
identifier and file export from "applicaitonData" to "applicationData" and the
import path segment "applicaiton" to "application"; update the constructor
parameter at line 90 to use the correctly spelled name and update the usage at
line 186 to reference applicationData; also open
template/nestJs/src/application/init/data.ts and rename its exported identifier
from applicaitonData to applicationData to match the corrected imports/usages.

Comment on lines +51 to +62
const applicationInfo = this.applicationRep.findOne({
where: { name },
});
if (isInit === true && (await applicationInfo)) {
return applicationInfo;
}
if ((await applicationInfo) && isInit === false) {
throw new HttpException(
`exception.applicationInfo.exists:${name}`,
HttpStatus.BAD_REQUEST
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing await on findOne causes broken duplicate detection.

Line 51 is missing await, so applicationInfo holds a Promise, not the resolved entity. The subsequent checks on lines 54 and 57 will always evaluate the Promise object as truthy, breaking the duplicate detection logic entirely.

Apply this diff to fix:

-    const applicationInfo = this.applicationRep.findOne({
+    const applicationInfo = await this.applicationRep.findOne({
       where: { name },
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const applicationInfo = this.applicationRep.findOne({
where: { name },
});
if (isInit === true && (await applicationInfo)) {
return applicationInfo;
}
if ((await applicationInfo) && isInit === false) {
throw new HttpException(
`exception.applicationInfo.exists:${name}`,
HttpStatus.BAD_REQUEST
);
}
const applicationInfo = await this.applicationRep.findOne({
where: { name },
});
if (isInit === true && (await applicationInfo)) {
return applicationInfo;
}
if ((await applicationInfo) && isInit === false) {
throw new HttpException(
`exception.applicationInfo.exists:${name}`,
HttpStatus.BAD_REQUEST
);
}
🤖 Prompt for AI Agents
In template/nestJs/src/application/application.service.ts around lines 51 to 62,
the call to this.applicationRep.findOne is missing await so applicationInfo
becomes a Promise and truthy checks always succeed; change the code to await the
findOne call once (e.g., const applicationInfo = await
this.applicationRep.findOne(...)) and then use that resolved value in the
subsequent if checks so duplicate detection and the isInit logic work correctly.

Comment on lines 1 to 3
<template>
<img :src="url" class="img" />
</template>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add alt attribute for accessibility.

The <img> element is missing an alt attribute, which is required for screen readers and accessibility compliance.

-<template>
-  <img :src="url" class="img" />
-</template>
+<template>
+  <img :src="url" class="img" alt="" />
+</template>

Consider accepting an alt prop if a meaningful description is available.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<template>
<img :src="url" class="img" />
</template>
<template>
<img :src="url" class="img" alt="" />
</template>
🤖 Prompt for AI Agents
In template/tinyvue/src/views/list/card-list/components/image.vue around lines 1
to 3, the <img> tag is missing an alt attribute; update the component to accept
an alt prop (string) and bind it to the img's alt attribute, providing a
sensible default ('' for purely decorative images) and document/validate the
prop type; if the image is decorative, ensure the default is an empty string or
set aria-hidden appropriately.

<div>
<Breadcrumb :items="['menu.list', 'menu.list.cardList']" />
<div class="content">
<div class="header mb-4">{{ $t('cardList.tiltle') }}</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in localization key: "tiltle" should be "title".

The localization key cardList.tiltle contains a typo. While it works if the key exists in locales.json, this should be corrected for consistency.

This requires updating both the template and the locales.json file:

-      <div class="header mb-4">{{ $t('cardList.tiltle') }}</div>
+      <div class="header mb-4">{{ $t('cardList.title') }}</div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="header mb-4">{{ $t('cardList.tiltle') }}</div>
<div class="header mb-4">{{ $t('cardList.title') }}</div>
🤖 Prompt for AI Agents
In template/tinyvue/src/views/list/card-list/index.vue around line 5 the
template uses the misspelled localization key cardList.tiltle; update this key
to cardList.title and then update the corresponding entry in the locales.json
file(s) (and any other locale files) to rename cardList.tiltle to cardList.title
so the translation key matches across template and locale files.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
template/tinyvue/src/views/list/card-list/index.vue (1)

130-130: Add optional chaining to prevent potential runtime error.

If cardLoadingState.value is null or undefined, calling .close() will throw a runtime error. Use optional chaining to safely handle this case.

Apply this diff to add safe navigation:

     } finally {
-      cardLoadingState.value.close();
+      cardLoadingState.value?.close();
     }
🧹 Nitpick comments (1)
template/tinyvue/src/views/list/card-list/components/image.vue (1)

1-3: LGTM! Accessibility improved.

The alt attribute has been added. For decorative images, alt="" is appropriate.

For enhanced accessibility, consider accepting an optional alt prop if meaningful descriptions are available for different images:

 <template>
-  <img :src="url" class="img" alt="" />
+  <img :src="url" class="img" :alt="alt" />
 </template>

Then add the prop definition:

 const props = defineProps({
   src: {
     type: String,
     default: 'card-list-application-default.png',
   },
+  alt: {
+    type: String,
+    default: '',
+  },
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f3ef7 and 3989b60.

📒 Files selected for processing (8)
  • template/nestJs/locales.json (3 hunks)
  • template/nestJs/src/app.module.ts (5 hunks)
  • template/nestJs/src/application/application.service.ts (1 hunks)
  • template/nestJs/src/application/init/data.ts (1 hunks)
  • template/tinyvue/src/api/card.ts (1 hunks)
  • template/tinyvue/src/locales.json (3 hunks)
  • template/tinyvue/src/views/list/card-list/components/image.vue (1 hunks)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • template/nestJs/src/application/application.service.ts
  • template/tinyvue/src/locales.json
  • template/nestJs/src/application/init/data.ts
🧰 Additional context used
🧬 Code graph analysis (1)
template/nestJs/src/app.module.ts (1)
template/nestJs/src/application/init/data.ts (1)
  • applicationData (1-88)
🔇 Additional comments (12)
template/nestJs/locales.json (1)

177-182: Typo fixed from previous review and localization keys are well-structured.

The previous review flagged a typo (cardList.tiltlecardList.title), which has been corrected in both enUS (Line 178) and zhCN (Line 781) sections. All new locale keys follow consistent naming conventions, and the translations are clear and coherent across both languages. The structure supports the Card List feature with proper menu entry, title, filtering options, and search placeholder text.

Also applies to: 780-785

template/nestJs/src/app.module.ts (5)

38-40: LGTM!

The import statements are correctly structured and follow NestJS conventions.


50-50: LGTM!

ApplicationModule is correctly added to the module imports array.


89-90: LGTM!

ApplicationService is properly injected via constructor, following NestJS dependency injection patterns.


127-134: LGTM!

The reformatting of the user permissions array improves readability without changing functionality.


184-193: LGTM!

The application initialization logic is well-structured and follows the same reliable pattern used for menu initialization, with appropriate error handling and logging.

template/tinyvue/src/api/card.ts (1)

1-13: LGTM! Previous issues resolved.

The interface and function are now correctly implemented. The typo in the interface name and parameter mismatch have been fixed.

template/tinyvue/src/views/list/card-list/components/image.vue (1)

4-17: LGTM! Asset path correctly resolved.

The asset path now uses Vite's new URL() with import.meta.url, which correctly handles asset resolution in both development and production builds. Previous issue with static paths is resolved.

template/tinyvue/src/views/list/card-list/index.vue (4)

1-53: LGTM! Template implementation is clean.

The localization key typo has been corrected, and the template properly uses TinyVue components for filtering, card display, and pagination.


54-92: LGTM! Imports and state initialization are correct.

The component setup is well-structured with appropriate imports and reactive state management.


94-111: LGTM! Event handlers properly update state.

The handlers correctly update pagination and filter state before triggering data refresh.


134-155: LGTM! Styles are well-structured.

The scoped styles appropriately use CSS custom properties and provide clean layout definitions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
template/tinyvue/src/views/list/card-list/index.vue (5)

14-18: Inconsistent locale key usage.

Line 16 uses searchTable.form.placeholder instead of the Card List-specific key cardList.search.placeholder mentioned in the locales. This creates an unnecessary dependency on the searchTable module.

Apply this diff to use the Card List-specific locale key:

         <tiny-search
           v-model="filterDataModel.keywords"
-          :placeholder="$t('searchTable.form.placeholder')"
+          :placeholder="$t('cardList.search.placeholder')"
           @change="handleRefresh"
         ></tiny-search>

28-30: Use consistent utility class naming.

Lines 28 and 30 use mt2 which doesn't follow standard utility naming conventions like mt-2. This may cause styling issues if the class is undefined.

Apply this diff to use standard utility class naming:

-        <div class="header mt2 mb-2">{{ card.name }}</div>
+        <div class="header mt-2 mb-2">{{ card.name }}</div>
         <div class="line-clamp-2">{{ card.description }}</div>
-        <div class="mt2">
+        <div class="mt-2">

94-101: Reset pagination when filters change.

When users change filters (classification or search keywords), the current page should reset to 1 for better UX. Otherwise, users might land on an empty page if the filtered results have fewer pages.

Apply this diff to reset the current page:

   function handleRefresh() {
+    pager.value.currentPage = 1;
     fetchData();
   }

   function classifyChange(val) {
     filterDataModel.classify = val;
+    pager.value.currentPage = 1;
     fetchData();
   }

108-111: Consider resetting pagination when page size changes.

When the page size changes, resetting the current page to 1 provides a more predictable user experience, especially if the new page size results in fewer total pages.

Apply this diff:

   function sizeChange(size) {
     pager.value.pageSize = size;
+    pager.value.currentPage = 1;
     fetchData();
   }

86-86: Arbitrary initial total value.

The total is initialized to 2, which seems arbitrary. Consider initializing it to 0 for consistency, as it will be updated with the actual count after the first API call.

Apply this diff:

   let pager = ref({
     currentPage: 1,
     pageSize: 10,
-    total: 2,
+    total: 0,
     pageSizes: [10, 20, 50],
   });
template/nestJs/src/app.module.ts (1)

183-193: Application seeding mirrors menu seeding; consider small DRY/logging improvements.

The new application seeding block correctly mirrors the existing menuData seeding pattern and uses the shared isInit flag and error handling, so behavior looks good. If you’d like to refine it later, you could extract a small helper to seed arbitrary datasets (menu/application) and optionally include the failing item’s identifier (e.g., name) in the logged error to speed up troubleshooting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3989b60 and 49045e3.

📒 Files selected for processing (2)
  • template/nestJs/src/app.module.ts (5 hunks)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
template/nestJs/src/app.module.ts (1)
template/nestJs/src/application/init/data.ts (1)
  • applicationData (1-88)
🔇 Additional comments (3)
template/tinyvue/src/views/list/card-list/index.vue (1)

27-27: Unable to complete automated verification due to repository access issues.

The verification script cannot access the repository to examine the import statement in template/tinyvue/src/views/list/card-list/index.vue or confirm whether the Image component file exists at the expected location. Manual verification is required to:

  1. Check the import statement for the Image component in the index.vue file
  2. Verify the component file exists at the imported path
  3. Confirm the import path is correct and the file is properly exported
template/nestJs/src/app.module.ts (2)

38-40: Application module wiring and DI look consistent; confirm ApplicationService export.

Imports, module registration, and constructor injection for ApplicationService follow the same pattern as existing services (MenuService, RoleService, etc.), so the wiring here looks correct. Just make sure that ApplicationModule actually exports ApplicationService, otherwise Nest won’t be able to resolve the dependency for AppModule.

Also applies to: 50-50, 89-90


127-134: Permission list change appears to be formatting-only.

The user permissions are now listed on multiple lines but the set of actions is unchanged and consistent with the existing pattern. No behavioral impact here.

@kagol
Copy link
Member

kagol commented Nov 27, 2025

@zhaoxiaofeng876 已检视,主要有以下问题:

  1. 底部页脚被遮挡了一部分
image
  1. 在 1920px(台式机) 和 1366px(笔记本) 两个常见分辨率下,卡片右侧没有右对齐,建议设计成卡片宽度不要写死,写成最大最小宽度

  2. 顶部的页签和分页的联动有问题,当处于前部应用页签时,点击分页到第二页,然后切换到开发服务或者交互设计,页面无数据,并且页码是第一页(建议:切换页签时,重置分页到第1页,并且数据也回到第1页的数据,因为每个页签数据不一样,回到第一页是最保险的方式)

image

再次切回全部应用,数据是第2页的数据,但页码是错误的第1页(建议同上,切换页签时返回第一页就不存在这个问题了)

image
  1. 手机端的页签、搜索框、分页有些折行,建议页签宽度不写死(对应是里面的button的padding不写死);搜索框宽度不写死;分页在手机端去掉每页数据条数下拉框、跳至这两个模块

大致效果如下:
image

  1. 搜索框输入关键词,按回车搜索之后,输入框失焦了,导致再次搜索时需要鼠标点击输入框先聚焦,才能继续输入,操作不太连续,可以优化下

@zhaoxiaofeng876
Copy link
Contributor Author

@zhaoxiaofeng876 已检视,主要有以下问题:

  1. 底部页脚被遮挡了一部分
image 2. 在 1920px(台式机) 和 1366px(笔记本) 两个常见分辨率下,卡片右侧没有右对齐,建议设计成卡片宽度不要写死,写成最大最小宽度 3. 顶部的页签和分页的联动有问题,当处于前部应用页签时,点击分页到第二页,然后切换到开发服务或者交互设计,页面无数据,并且页码是第一页(建议:切换页签时,重置分页到第1页,并且数据也回到第1页的数据,因为每个页签数据不一样,回到第一页是最保险的方式) image 再次切回全部应用,数据是第2页的数据,但页码是错误的第1页(建议同上,切换页签时返回第一页就不存在这个问题了) image 4. 手机端的页签、搜索框、分页有些折行,建议页签宽度不写死(对应是里面的button的padding不写死);搜索框宽度不写死;分页在手机端去掉每页数据条数下拉框、跳至这两个模块

大致效果如下: image

  1. 搜索框输入关键词,按回车搜索之后,输入框失焦了,导致再次搜索时需要鼠标点击输入框先聚焦,才能继续输入,操作不太连续,可以优化下

@zhaoxiaofeng876 zhaoxiaofeng876 deleted the zhaoxiaofeng-card branch November 28, 2025 02:40
@zhaoxiaofeng876 zhaoxiaofeng876 restored the zhaoxiaofeng-card branch November 28, 2025 02:46
@kagol
Copy link
Member

kagol commented Nov 28, 2025

@zhaoxiaofeng876 还有一个小小的问题,移动端场景下,出现了三个滚动条,应该只出现一个滚动条
image

PC端如果数据量多的情况下,也会出现双滚动条(感觉外面那根滚动条不应该出现)
image

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
template/tinyvue/src/views/list/card-list/index.vue (1)

150-153: Consider using template ref instead of getElementById.

While document.getElementById('card-list') works, using a template ref would be more idiomatic in Vue and avoids direct DOM queries.

You could refactor to:

+ const cardListRef = ref(null);
+
  async function fetchData() {
    // ...
    try {
      cardLoadingState.value = TinyLoading.service({
-       target: document.getElementById('card-list'),
+       target: cardListRef.value,
      });

And update the template:

-      <div id="card-list" class="card-container">
+      <div ref="cardListRef" class="card-container">
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc8ea1d and 6b81da3.

📒 Files selected for processing (1)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🔇 Additional comments (3)
template/tinyvue/src/views/list/card-list/index.vue (3)

196-205: Good responsive grid implementation.

The card grid uses repeat(auto-fill, minmax(320px, 1fr)) with width: auto on cards, which addresses the PR objective about avoiding fixed card widths. Cards will now stretch to fill available space and align properly at different resolutions.


127-131: Pagination reset on classification change looks good.

The handler correctly resets currentPage to 1 when the classification filter changes, which addresses the tabs/pagination sync issue mentioned in the PR objectives. This ensures users see page 1 data when switching between "all", "services", and "design" classifications.


13-18: I'll help you verify the review comment about the TinySearch component's event handling and focus behavior. Let me start by exploring the codebase to understand the TinySearch component and the current implementation.
<function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

First, let's find the TinySearch component definition

fd -type f -name ".vue" -o -name ".ts" -o -name "*.js" | head -20

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Search for TinySearch component definition

rg -n "TinySearch" --type vue --type ts --type js -C 3 | head -50

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Look at the specific file mentioned in the review

cat -n template/tinyvue/src/views/list/card-list/index.vue | head -50

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Check for handleRefresh implementation in the same file

rg -n "handleRefresh" template/tinyvue/src/views/list/card-list/index.vue -A 5

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Look for TinySearch component documentation or definition in the project

fd -type f ( -name "search" -o -name "input" ) -path "/components/" | grep -i search

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Check package.json for the TinyVue library version

cat package.json | grep -A 2 -B 2 "tiny" || cat package.json | head -30

</invoke_parameters>
</function_calls>

Now let me search for TinySearch component API documentation and check for similar patterns in the codebase:
<function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Search for other uses of TinySearch in the project to see patterns

rg -n "tiny-search" --type vue -B 2 -A 2

</invoke_parameters>
</function_calls>
<invoke_name>shell
<invoke_parameters>
#!/bin/bash

Check if there's documentation or comments about search events

rg -n "@change|@search|@Enter" --type vue -B 1 -A 1 | head -50

</invoke_parameters>
</function_calls>

Let me also search for the TinyVue documentation online and check the exact component behavior:
<function_calls>
<invoke_name>web_search
OpenTiny Vue TinySearch component events API documentation

</invoke_parameters>
</function_calls>

<Image :src="card.icon" />
<div class="header mt2 mb-2">{{ card.name }}</div>
<div class="line-clamp-2">{{ card.description }}</div>
<div class="mt2">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix CSS class name typo.

Same issue as line 27: mt2 should be mt-2.

Apply this diff:

-          <div class="mt2">
+          <div class="mt-2">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="mt2">
<div class="mt-2">
🤖 Prompt for AI Agents
In template/tinyvue/src/views/list/card-list/index.vue around line 29, the CSS
utility class is misspelled as "mt2"; change it to the correct "mt-2" (and check
nearby line 27 which had the same typo) so the spacing utility works as
intended.

Comment on lines +166 to +182
.container-list {
flex: 1 1 auto;
flex-direction: column;
justify-content: space-between;
overflow-x: hidden;
overflow-y: auto;
}
.content {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 53px); // 53px is the height of breadcrumb
overflow: auto;
background: #fff;
border-radius: 10px;
padding: 20px;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove nested scroll containers to fix multiple scrollbars.

The PR objectives specifically mention multiple scrollbars appearing (3 on mobile, 2 on desktop). This is caused by nested overflow: auto declarations:

  • .container-list has overflow-y: auto (line 171)
  • .content has overflow: auto (line 178)

Remove one level of scrolling to ensure only a single scrollbar appears.

Consider applying this diff to let .content handle the scrolling:

   .container-list {
     flex: 1 1 auto;
     flex-direction: column;
     justify-content: space-between;
-    overflow-x: hidden;
-    overflow-y: auto;
   }

Verify this resolves the multiple scrollbar issue mentioned in the PR review comments.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.container-list {
flex: 1 1 auto;
flex-direction: column;
justify-content: space-between;
overflow-x: hidden;
overflow-y: auto;
}
.content {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 53px); // 53px is the height of breadcrumb
overflow: auto;
background: #fff;
border-radius: 10px;
padding: 20px;
}
.container-list {
flex: 1 1 auto;
flex-direction: column;
justify-content: space-between;
}
.content {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 53px); // 53px is the height of breadcrumb
overflow: auto;
background: #fff;
border-radius: 10px;
padding: 20px;
}
🤖 Prompt for AI Agents
In template/tinyvue/src/views/list/card-list/index.vue around lines 166 to 182,
there are nested scroll containers causing multiple scrollbars: .container-list
sets overflow-y: auto and .content sets overflow: auto. Remove the inner/outer
duplicate scrolling by deleting the overflow rule from one of them (preferably
remove overflow-y: auto from .container-list so .content handles scrolling) and
keep a single overflow on the appropriate element; ensure height calculations
remain correct (adjust .content height calc if needed) so only one scrollbar
appears.

@zhaoxiaofeng876
Copy link
Contributor Author

zhaoxiaofeng876 commented Nov 28, 2025

@zhaoxiaofeng876 还有一个小小的问题,移动端场景下,出现了三个滚动条,应该只出现一个滚动条 image

PC端如果数据量多的情况下,也会出现双滚动条(感觉外面那根滚动条不应该出现) image

已修改:
image

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
template/tinyvue/src/views/list/card-list/index.vue (3)

132-135: Consider resetting pagination on refresh to avoid empty result pages

search() and classifyChange() both reset pager.currentPage = 1 before fetching, but handleRefresh() does not. If the user is on a high page and the dataset shrinks (e.g., backend data or filters change), clicking refresh can show an empty page even though valid data exists on earlier pages.

If the UX expectation is that the refresh icon behaves like a “fresh search”, you may want to align it with search():

  function handleRefresh() {
-    fetchData();
+    pager.value.currentPage = 1;
+    fetchData();
  }

If you intentionally want refresh to keep the current page, then this is fine as is—just be aware of the empty-page edge case.


31-33: Fix remaining mt2 spacing class typo

There’s still a mt2 class on the tag container; this was flagged previously and should be mt-2 to match the utility naming pattern and actually apply the margin.

-          <div class="mt2">
+          <div class="mt-2">

175-191: Resolve nested scroll containers to eliminate multiple scrollbars and footer overlap

You still have two scrollable containers:

  • .container-list with overflow-x: hidden; overflow-y: auto;
  • .content with overflow: auto; height: calc(100% - 53px);

This nesting is very likely the cause of the “three scrollbars on mobile / two on desktop” and footer overlap issues called out in the PR review.

To ensure there is only a single scrollbar, one of these should stop managing overflow. For example, you can let .content own scrolling and make .container-list non-scrollable:

  .container-list {
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: space-between;
-   overflow-x: hidden;
-   overflow-y: auto;
  }

Alternatively, if the layout is supposed to scroll at the .container-list level, drop the overflow and height from .content instead. The key is to keep exactly one effective scroll container in this stack and verify that the footer is fully visible and no extra scrollbars appear on mobile and desktop.

🧹 Nitpick comments (1)
template/tinyvue/src/views/list/card-list/index.vue (1)

12-21: Re‑check search input focus behavior; consider explicit re‑focus

The previous review noted that after pressing Enter to search, the search input loses focus. In this version, tiny-search still only emits @change="search", and search() just resets the page and refetches; there’s no explicit focus management.

If tiny-search still blurs the field on change/Enter, users will keep having to click back into the input for repeated searches.

I suggest:

  • Give the search component a template ref, e.g. ref="searchRef".
  • After calling fetchData() in search(), call the component’s focus method if available (e.g. searchRef.value?.focus?.()), or otherwise wire a key event that preserves focus.

Please verify this behavior in the running app and adjust as needed so the search box stays focused for continuous input.

Also applies to: 127-130

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b81da3 and ebf1c7f.

📒 Files selected for processing (1)
  • template/tinyvue/src/views/list/card-list/index.vue (1 hunks)
🔇 Additional comments (1)
template/tinyvue/src/views/list/card-list/index.vue (1)

45-55: Pager behavior and tab/pagination linkage look good

  • classifyChange resets pager.currentPage to 1 and refetches, which addresses the earlier “tabs vs. pagination out of sync” issue.
  • currentChange/sizeChange correctly sync the pager state with the data fetch.
  • The ResizeObserver-based layout switch for <tiny-pager> hides sizes and jumper on narrow widths, matching the mobile pagination simplification request.

No changes needed here from my side.

Also applies to: 109-121, 136-145

@kagol kagol added the enhancement New feature or request label Nov 28, 2025
@github-actions github-actions bot removed the enhancement New feature or request label Nov 28, 2025
@kagol kagol merged commit 63cba85 into opentiny:dev Nov 28, 2025
1 check passed
@kagol
Copy link
Member

kagol commented Nov 28, 2025

@all-contributors please add @zhaoxiaofeng876 for code.

@allcontributors
Copy link
Contributor

@kagol

I've put up a pull request to add @zhaoxiaofeng876! 🎉

@kagol kagol added the enhancement New feature or request label Jan 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments