Files
Serendipity edbf78f236 feat: 初始化 Halo 暗色模式插件
- Halo Plugin 后端(Java/Gradle),含 DarkModePlugin 主类和测试
- Vue 3 + TypeScript 前端 UI,包含主题切换组件和设置页面
- 暗色模式 CSS 变量和覆盖样式(布局/编辑器/表单/滚动条等)
- 设计文档和调查文档
- Halo 插件/主题开发 Agent Skills

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 21:03:00 +08:00

253 lines
11 KiB
Markdown

# UI Components
Halo provides two layers of UI primitives for plugin frontends:
1. **Base component library** (`@halo-dev/components`) — install and import explicitly
2. **Business components & directives** — globally registered, use directly without import
> Full base component docs: https://halo-ui-components.halo-run.workers.dev
>
> For forms, see [ui-forms.md](ui-forms.md) — Halo uses FormKit (globally registered) with many custom inputs.
## Official Docs Routing
Component APIs change across Halo versions. Treat the lists below as common
shortcuts, not an exhaustive API reference.
| Need | Official docs |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base components from `@halo-dev/components` | https://halo-ui-components.halo-run.workers.dev |
| Business component index | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/index.md |
| `AttachmentSelectorModal` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/attachment-selector-modal.md |
| `AttachmentFileTypeIcon` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md |
| `AnnotationsForm` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/annotations-form.md |
| `FilterDropdown` / `FilterCleanButton` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/filter-dropdown.md |
| `HasPermission` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/has-permission.md |
| `PluginDetailModal` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/plugin-detail-modal.md |
| `SearchInput` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/search-input.md |
| `UppyUpload` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/uppy-upload.md |
| `VCodemirror` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/v-codemirror.md |
| `v-permission` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/v-permission.md |
| `v-tooltip` | https://raw.githubusercontent.com/halo-dev/docs/refs/heads/main/docs/developer-guide/plugin/api-reference/ui/components/v-tooltip.md |
## Base Component Library
Install:
```bash
pnpm install @halo-dev/components
```
Import and use:
```vue
<script lang="ts" setup>
import { ref } from "vue";
import { VButton, VModal, VCard } from "@halo-dev/components";
const visible = ref(false);
</script>
<template>
<VButton type="secondary" @click="visible = true">Open</VButton>
<VModal v-if="visible" @close="visible = false" title="Title">
<VCard>Content</VCard>
</VModal>
</template>
```
Common exported components from `@halo-dev/components`:
| Component | Purpose |
| ----------------------------------------------- | -------------------------------------------------------------------------- |
| `VAlert` | Alert banner |
| `VAvatar` / `VAvatarGroup` | User avatar(s) |
| `VButton` | Button with variants: `default`, `primary`, `secondary`, `danger`, `ghost` |
| `VCard` | Card container |
| `VDescription` / `VDescriptionItem` | Key-value description list |
| `VDialog` + `Dialog` (manager) | Dialog with imperative API |
| `VDropdownDivider` / `VDropdownItem` | Dropdown menu items |
| `VEmpty` | Empty state placeholder |
| `VEntity` / `VEntityContainer` / `VEntityField` | Entity list item layout |
| `VLoading` | Loading spinner/overlay |
| `VMenu` / `VMenuItem` / `VMenuLabel` | Menu navigation |
| `VModal` | Modal with `v-model:visible` |
| `VPagination` | Pagination control |
| `VPageHeader` | Page header with back button/title/actions |
| `VSpace` | Flex spacing layout |
| `VStatusDot` | Status indicator dot |
| `VSwitch` | Toggle switch |
| `VTabbar` / `VTabs` / `VTabItem` | Tab navigation |
| `VTag` | Colored tag/badge |
| `Toast` (manager) | Toast notification imperative API |
| `VTooltipComponent` / `vTooltip` | Tooltip component/directive |
## Business Components (Globally Registered)
These are available without import in any plugin Vue component.
### VCodemirror
Code editor.
```vue
<VCodemirror v-model="value" height="300px" language="yaml" />
```
| Prop | Type | Default | Description |
| ------------ | -------- | -------- | --------------------------------------------- |
| `modelValue` | `string` | `""` | Binding value |
| `height` | `string` | `"auto"` | Editor height |
| `language` | `string` | `"yaml"` | Language: `yaml`, `html`, `js`, `css`, `json` |
| `extensions` | `array` | `[]` | Codemirror extensions |
### AttachmentSelectorModal
Attachment picker modal (Console only).
```vue
<script setup>
const visible = ref(false);
function onSelect(attachments) {
console.log(attachments); // AttachmentLike[]
}
</script>
<template>
<VButton @click="visible = true">Select</VButton>
<AttachmentSelectorModal
v-if="visible"
@close="visible = false"
:accepts="['image/*']"
:min="1"
:max="5"
@select="onSelect"
/>
</template>
```
| Prop | Type | Default | Description |
| ------------- | ---------- | --------- | ----------------------- |
| `visible` | `boolean` | `false` | Controlled visibility |
| `accepts` | `string[]` | `["*/*"]` | Accepted MIME types |
| `min` / `max` | `number` | — | Min/max selection count |
### UppyUpload
File upload component.
```vue
<UppyUpload
endpoint="/apis/api.console.halo.run/v1alpha1/attachments/upload"
:meta="{ policyName, groupName }"
@uploaded="onUploaded"
@error="onError"
/>
```
| Prop | Type | Default | Description |
| ------------- | ------------------------- | -------- | ------------------------------- |
| `endpoint` | `string` | required | Upload API endpoint |
| `meta` | `Record<string, unknown>` | — | Extra metadata sent with upload |
| `autoProceed` | `boolean` | `false` | Auto-upload on select |
| `method` | `string` | `"post"` | HTTP method |
### SearchInput
Search input that only triggers on Enter (not while typing).
```vue
<SearchInput v-model="keyword" placeholder="Search..." />
```
### AttachmentFileTypeIcon
File-type icon for attachment/file lists.
```vue
<AttachmentFileTypeIcon fileName="example.png" :display-ext="true" />
```
### AnnotationsForm
Renders the Annotations form for a given Extension group/kind.
```vue
<script setup>
const formRef = ref();
async function handleSubmit() {
formRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
formRef.value || {};
if (customFormInvalid || specFormInvalid) return;
const merged = { ...annotations, ...customAnnotations };
// ...submit merged
}
</script>
<template>
<AnnotationsForm ref="formRef" :value="currentAnnotations" kind="Post" group="content.halo.run" />
<VButton @click="handleSubmit">Save</VButton>
</template>
```
### FilterDropdown / FilterCleanButton
Generic filter dropdown and clear button for list pages.
```vue
<FilterDropdown
v-model="sortValue"
label="Sort"
:items="[
{ label: 'Newest', value: 'creationTimestamp,desc' },
{ label: 'Oldest', value: 'creationTimestamp,asc' },
]"
/>
<FilterCleanButton @click="resetFilters" />
```
### PluginDetailModal
Open a plugin's detail/settings modal inline.
```vue
<PluginDetailModal v-if="visible" name="my-plugin" @close="visible = false" />
```
| Prop | Type | Description |
| ------ | -------- | -------------------- |
| `name` | `string` | Plugin metadata.name |
### HasPermission
Render content only when the user has the required permissions.
```vue
<HasPermission :permissions="['system:posts:manage']">
<VButton type="danger">Delete</VButton>
</HasPermission>
```
## Directives (Globally Registered)
### v-permission
Conditionally render based on permissions.
```vue
<VButton type="danger" v-permission="['system:posts:manage']">Delete</VButton>
```
Equivalent component: `<HasPermission :permissions="['system:posts:manage']">...</HasPermission>`
### v-tooltip
Add tooltip to any element.
```vue
<IconDeleteBin v-tooltip="'Delete this item'" />
```