Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions ui/src/views/home/components/HomeQuickCreate.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { ModelProviderItem } from '@/api/types'
import ModelProviderApi from '@/api/admin/model-provider'
Expand All @@ -8,9 +8,15 @@ import ButtonCreateApplication from '@/views/application/components/ButtonCreate
import ButtonCreateKnowledge from '@/views/knowledge/components/ButtonCreateKnowledge.vue'
import ButtonCreateTool from '@/views/tool/components/ButtonCreateTool.vue'
import ButtonAddModel from '@/views/model/create-model/ButtonAddModel.vue'
import perm from '@/permission'
const props = defineProps<{ workspaceId: string }>()
const router = useRouter()

/* 任一资源可创建时展示快捷创建区域。 */
const showQuickCreate = computed(
() => perm.application.workspace.create() || perm.knowledge.workspace.create() || perm.tool.workspace.create() || perm.model.workspace.create(),
)

/* 智能体快捷导入完成后进入列表页面。其他创建会随组件进入对应的详情页面 */
function handleCreateApplication() {
return router.push({ name: 'workspace-application-list', params: { workspaceId: props.workspaceId } })
Expand Down Expand Up @@ -41,10 +47,18 @@ onMounted(() =>
)
</script>
<template>
<section>
<section v-if="showQuickCreate">
<h4 class="mb-4 mt-4">快捷创建</h4>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<!-- 创建智能体 -->
<ButtonCreateApplication trigger="hover" fit-trigger-width folder-id="default" popperClass="rounded-xl!" @refresh="handleCreateApplication">
<ButtonCreateApplication
v-if="perm.application.workspace.create()"
trigger="hover"
fit-trigger-width
folder-id="default"
popperClass="rounded-xl!"
@refresh="handleCreateApplication"
>
<template #trigger>
<el-card shadow="hover">
<div class="flex-between gap-3">
Expand All @@ -59,7 +73,14 @@ onMounted(() =>
</template>
</ButtonCreateApplication>
<!-- 创建知识库 -->
<ButtonCreateKnowledge trigger="hover" fit-trigger-width popper-class="rounded-xl!" folder-id="default" @refresh="handleCreateKnowledge">
<ButtonCreateKnowledge
v-if="perm.knowledge.workspace.create()"
trigger="hover"
fit-trigger-width
popper-class="rounded-xl!"
folder-id="default"
@refresh="handleCreateKnowledge"
>
<template #trigger>
<el-card shadow="hover">
<div class="flex-between gap-3">
Expand All @@ -74,7 +95,14 @@ onMounted(() =>
</template>
</ButtonCreateKnowledge>
<!-- 创建工具 -->
<ButtonCreateTool trigger="hover" fit-trigger-width popper-class="rounded-xl!" folder-id="default" @refresh="handleCreateTool">
<ButtonCreateTool
v-if="perm.tool.workspace.create()"
trigger="hover"
fit-trigger-width
popper-class="rounded-xl!"
folder-id="default"
@refresh="handleCreateTool"
>
<template #trigger>
<el-card shadow="hover">
<div class="flex-between gap-3">
Expand All @@ -89,7 +117,13 @@ onMounted(() =>
</template>
</ButtonCreateTool>
<!-- 添加模型 -->
<ButtonAddModel :current-provider="defaultProvider" :providers="providers" :api="ModelApi" @refresh="handleCreateModel">
<ButtonAddModel
v-if="perm.model.workspace.create()"
:current-provider="defaultProvider"
:providers="providers"
:api="ModelApi"
@refresh="handleCreateModel"
>
<template #default="{ open }">
<!-- 打开模型创建入口 -->
<el-card shadow="hover" class="cursor-pointer" @click="open">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here are the issues and suggestions for the diff:

1. Duplicated permission checks (maintenance risk)
The same permission expressions (perm.application.workspace.create(), etc.) are repeated in both showQuickCreate and each v-if. If you later change a permission, you must update it in two places. Consider deriving a small object or using the computed flags:

const can = computed(() => ({
  application: perm.application.workspace.create(),
  knowledge: perm.knowledge.workspace.create(),
  tool: perm.tool.workspace.create(),
  model: perm.model.workspace.create(),
}))
const showQuickCreate = computed(() => Object.values(can.value).some(Boolean))

Then use v-if="can.application" etc.

2. Potential reactivity issue
perm.application.workspace.create() looks like an imperative call, not necessarily reactive. Calling it inside computed only re-evaluates when its reactive dependencies change. Verify that perm.*.create() reads reactive state internally; otherwise the UI won't update when permissions change (e.g., after login or role switch).

3. Possible early-render flicker
If permissions are loaded asynchronously (e.g., fetched after mount), the section will briefly render empty/hidden before permissions resolve. Ensure permission state is ready before rendering, or add a loading guard.

4. Inconsistent fit-trigger-width casing (pre-existing, but worth noting)
ButtonCreateApplication uses fit-trigger-width (kebab-case attr), while the others use popper-class / popperClass inconsistently across components. Not introduced by this diff, but the reformatting exposes it — verify each component's actual prop names to avoid silently ignored props.

5. Import ordering / lint
import perm from '@/permission' is placed after other imports but grouped loosely. Depending on your ESLint config (import/order), you may want to keep third-party and @/ imports ordered.

6. Heading semantics
<h4 class="mb-4 mt-4">快捷创建</h4> — hard-coded Chinese inline, consistent with existing code but consider i18n if the project supports multiple languages (other strings appear to be through components). Minor.

7. Skipping onMounted fetch when no permissions
onMounted still runs fetching providers/model data regardless of showQuickCreate. If the section is hidden entirely, you could skip those API calls when perm.model.workspace.create() is false, to reduce unnecessary requests.

8. Empty grid layout
When showQuickCreate is true but only some cards show, the xl:grid-cols-4 with 1–2 items is fine, but sm:grid-cols-2 handles it. No action needed, just confirm spacing looks right with fewer items.

9. Prop naming consistency
defaultProvider, providers, ModelApi usage unchanged — fine.

Overall the logic is correct; the main improvements are DRY-ing the permission checks (#1) and verifying reactive permission state (#2).

Expand Down
22 changes: 16 additions & 6 deletions ui/src/views/home/components/HomeResourceOverview.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import type HomepageApi from '@/api/admin/workspace/homepage'
import type { HomeApplicationAggregation, HomeKnowledgeAggregation, HomeToolAggregation, HomeModelAggregation } from '@/api/types'
import { toThousands } from '@/utils/number'
import perm from '@/permission'
interface HomeResourceCard {
label: string
route: string
icon: string
color: string
disabled?: boolean
total?: number
details: [string, number | undefined][]
}

/* 资源概况与列表导航 */
const props = defineProps<{ workspaceId: string; api: typeof HomepageApi }>()
const router = useRouter()
const route = useRoute()
const isSystemScope = computed(() => route.meta.scope === 'system')
const loading = ref(false)
const application = ref<HomeApplicationAggregation>()
const knowledge = ref<HomeKnowledgeAggregation>()
Expand All @@ -24,6 +28,7 @@ const model = ref<HomeModelAggregation>()
const resourceGroups = computed<HomeResourceCard[]>(() => [
{
label: '智能体',
disabled: isSystemScope.value || !perm.application.workspace.isShare(),
route: 'workspace-application-list',
icon: 'icon_robot_filled',
color: '#3370FF',
Expand All @@ -35,6 +40,7 @@ const resourceGroups = computed<HomeResourceCard[]>(() => [
},
{
label: '知识库',
disabled: isSystemScope.value || !perm.knowledge.workspace.isShare(),
route: 'workspace-knowledge-list',
icon: 'icon_book_filled',
color: '#7F3BF5',
Expand All @@ -46,6 +52,7 @@ const resourceGroups = computed<HomeResourceCard[]>(() => [
},
{
label: '工具',
disabled: isSystemScope.value || !perm.tool.workspace.isShare(),
route: 'workspace-tools',
icon: 'icon_busy_filled',
color: '#2CA91F',
Expand All @@ -58,6 +65,7 @@ const resourceGroups = computed<HomeResourceCard[]>(() => [
},
{
label: '模型',
disabled: isSystemScope.value || !perm.model.workspace.isShare(),
route: 'workspace-model',
icon: 'icon_dataset_filled',
color: '#FF8800',
Expand Down Expand Up @@ -109,8 +117,9 @@ function loadResources() {
loading.value = false
})
}
function handleNavigate(name: string) {
router.push({ name, params: { workspaceId: props.workspaceId } })
function handleNavigate(card: HomeResourceCard) {
if (card.disabled) return
router.push({ name: card.route, params: { workspaceId: props.workspaceId } })
}
onMounted(loadResources)
defineExpose({ refresh: loadResources })
Expand All @@ -119,11 +128,12 @@ defineExpose({ refresh: loadResources })
<section>
<div v-loading="loading" class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<template v-for="card in resourceGroups" :key="card.route">
<!-- 进入未禁用资源的列表 -->
<el-card
shadow="hover"
class="cursor-pointer hover:border-current!"
:shadow="card.disabled ? 'never' : 'hover'"
:class="{ 'cursor-pointer hover:border-current!': !card.disabled }"
:style="{ color: card.color, backgroundImage: `linear-gradient(to bottom, ${card.color}0A 0%, #FFFFFF 40%, #FFFFFF 100%)` }"
@click="handleNavigate(card.route)"
@click="handleNavigate(card)"
>
<div class="flex-between text-N900">
<div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's my review:

Potential Issues

  1. disabled is both a data property and used in template binding — el-card doesn't have a disabled prop, so that's fine visually, but the disabled state is purely cosmetic. Clicks are guarded in handleNavigate, which is good. However, there's no cursor/opacity feedback distinguishing disabled cards beyond shadow/hover. Consider adding opacity-50 or pointer-events-none (though pointer-events-none would block the click handler entirely, which may be preferable).

  2. No visual affordance for disabled state — users may not realize why a card doesn't navigate. Adding a tooltip or dimmed styling would improve UX.

  3. Accessibility — @click on el-card isn't keyboard-accessible. Consider wrapping navigation in an <a>/<router-link> for semantics, or add role="button", tabindex="0", and @keydown.enter.

  4. Permission checks in a computed — perm.application.workspace.isShare() etc. are called inside a computed. If these functions depend on reactive state, this works; if they read non-reactive globals, the computed won't update when permissions change. Verify reactivity of perm.*.

  5. Repeated isSystemScope.value || !perm.X.workspace.isShare() logic — the pattern is repeated 4 times. Consider a small helper like:

    const canShare = (check: () => boolean) => !isSystemScope.value && check()

Minor / Optimization

  1. route.meta.scope === 'system' — relies on an untyped meta value. Consider augmenting RouteMeta with a typed scope?: 'system' | ... for safety.

  2. handleNavigate(card.route) → handleNavigate(card) — good refactor, since the guard needs the disabled flag.

  3. CSS class string — 'cursor-pointer hover:border-current!': !card.disabled uses object syntax correctly. Note the ! suffix is UnoCSS/Tailwind-v4 important syntax; ensure the preprocessor supports it (it was already present before, so fine).

  4. disabled optional (?) — left optional means every consumer must handle undefined; since all cards set it, consider making it required to catch omissions at compile time.

  5. onMounted(loadResources) — if props.workspaceId changes while mounted (e.g., route param change reusing the component), resources won't reload. Consider watch(() => props.workspaceId, loadResources).

  6. Auto-imports — defineProps, ref, computed are auto-imported presumably; perm is a new manual import, consistent with other imports. Fine.

  7. Comment placement — <!-- 进入未禁用资源的列表 --> above every card via v-for template is fine, but it's rendered per iteration logically; harmless.

Summary

The diff is functionally sound. Main improvements: add visual/keyboard feedback for disabled cards, centralize the permission predicate, make disabled required, and watch workspaceId for reactivity.

Expand Down
1 change: 0 additions & 1 deletion ui/src/views/home/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ onMounted(() => {
<template>
<MkViewLayout title="">
<!-- 快捷创建 -->
<h4 class="mb-4 mt-4">快捷创建</h4>
<HomeQuickCreate :workspace-id="workspaceId" />
<!-- 资源 -->
<h4 class="mb-4 mt-4">资源</h4>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's my review of the diff:

Summary

The change simply removes a <h4 class="mb-4 mt-4">快捷创建</h4> ("Quick Create") heading from the template. This is a minor, low-risk change, but there are a few things to consider.

Potential Issues

  1. Inconsistent section labeling / UX

    • The "资源" (Resources) section still has its heading, but the "快捷创建" (Quick Create) section no longer does. Users may lose context about what the HomeQuickCreate block represents. If the component itself renders its own title, this is fine — otherwise consider keeping a heading or adding one inside the component.
  2. Accessibility

    • If the removed <h4> was the only accessible label for the HomeQuickCreate region, screen-reader users lose that landmark context. Verify whether HomeQuickCreate includes an appropriate aria-label, <h2>/<h3>, or role with labeling.
  3. Spacing / margin artifacts

    • The removed element had mt-4 mb-4. Check that removing it doesn't break vertical spacing between the page title/MkViewLayout and HomeQuickCreate. You may need to add margin to the component wrapper (or a utility class on HomeQuickCreate) to preserve the intended gap.
  4. Empty title="" attribute

    • <MkViewLayout title=""> passes an empty string. If title defaults internally, passing "" explicitly may suppress the default title. Confirm this is intentional; otherwise omit the prop.
  5. Comment now slightly misleading

    • The comment <!-- 快捷创建 --> remains but the visible heading is gone. It's harmless, but if the comment was meant to mark the section, ensure it still makes sense.

Optimization Suggestions

  • Centralize section headings: Since you have repeated <h4 class="mb-4 mt-4">…</h4> patterns, consider extracting a small <SectionHeading> component or using consistent heading levels (e.g., an <MkSection> wrapper). This avoids drift like the current inconsistency.
  • Heading hierarchy: Using <h4> without preceding <h2>/<h3> in the layout can create skipped heading levels. Ensure the document outline is logical for accessibility and SEO.
  • Collapse keyword styling: The mb-4 mt-4 utilities could be simplified (e.g., my-4) — minor readability improvement.

Verdict

The diff is safe but should be verified against:

  • Whether HomeQuickCreate self-labels (for a11y),
  • Spacing consistency after removal,
  • The intent behind title="".

No functional bugs are introduced by the change itself.

Expand Down
Loading