chore(portal): 补充 bitable/UI 组件的类型声明与组件文件
Test / backend-test (push) Waiting to run Details
Test / frontend-unit (push) Waiting to run Details
Test / api-e2e (push) Waiting to run Details
Test / frontend-e2e (push) Waiting to run Details
Deploy to Production / deploy (push) Failing after 15m58s Details

This commit is contained in:
Chiguyong 2026-07-07 22:42:27 +08:00
parent 0979612a35
commit 0ad0ee668b
15 changed files with 971 additions and 0 deletions

View File

@ -189,9 +189,23 @@ declare module 'vue' {
ToolCallIndicator: typeof import('./src/components/chat/ToolCallIndicator.vue')['default']
TopNav: typeof import('./src/components/layout/TopNav.vue')['default']
UiCard: typeof import('./src/components/ui/UiCard.vue')['default']
UiCheckbox: typeof import('./src/components/ui/UiCheckbox.vue')['default']
UiCheckboxGroup: typeof import('./src/components/ui/UiCheckboxGroup.vue')['default']
UiDatePicker: typeof import('./src/components/ui/UiDatePicker.vue')['default']
UiDateRangePicker: typeof import('./src/components/ui/UiDateRangePicker.vue')['default']
UiFormField: typeof import('./src/components/ui/UiFormField.vue')['default']
UiFormSection: typeof import('./src/components/ui/UiFormSection.vue')['default']
UiInput: typeof import('./src/components/ui/UiInput.vue')['default']
UiMetricCard: typeof import('./src/components/ui/UiMetricCard.vue')['default']
UiPageHeader: typeof import('./src/components/ui/UiPageHeader.vue')['default']
UiPageShell: typeof import('./src/components/ui/UiPageShell.vue')['default']
UiRadio: typeof import('./src/components/ui/UiRadio.vue')['default']
UiRadioGroup: typeof import('./src/components/ui/UiRadioGroup.vue')['default']
UiSearchInput: typeof import('./src/components/ui/UiSearchInput.vue')['default']
UiSelect: typeof import('./src/components/ui/UiSelect.vue')['default']
UiSwitch: typeof import('./src/components/ui/UiSwitch.vue')['default']
UiTabs: typeof import('./src/components/ui/UiTabs.vue')['default']
UiTextArea: typeof import('./src/components/ui/UiTextArea.vue')['default']
UiTrendBadge: typeof import('./src/components/ui/UiTrendBadge.vue')['default']
UsagePanel: typeof import('./src/components/evolution/UsagePanel.vue')['default']
UserBubble: typeof import('./src/components/chat/messages/UserBubble.vue')['default']

View File

@ -0,0 +1,53 @@
<template>
<a-checkbox
:checked="modelValue"
:disabled="disabled"
:indeterminate="indeterminate"
class="ui-checkbox"
@update:checked="emit('update:modelValue', $event)"
@change="emit('change', $event)"
>
<slot />
</a-checkbox>
</template>
<script setup lang="ts">
import { Checkbox as ACheckbox } from 'ant-design-vue'
interface IProps {
modelValue: boolean
disabled?: boolean
indeterminate?: boolean
}
withDefaults(defineProps<IProps>(), {
disabled: false,
indeterminate: false,
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
change: [e: Event]
}>()
</script>
<style scoped>
.ui-checkbox :deep(.ant-checkbox-inner) {
border-radius: var(--radius-sm, 4px);
border-color: var(--border-color-hover);
background: var(--bg-primary);
}
.ui-checkbox :deep(.ant-checkbox:hover .ant-checkbox-inner) {
border-color: var(--border-color-active);
}
.ui-checkbox :deep(.ant-checkbox-checked .ant-checkbox-inner) {
background: var(--color-primary);
border-color: var(--color-primary);
}
.ui-checkbox :deep(.ant-checkbox-wrapper) {
color: var(--text-primary);
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<a-checkbox-group
:value="modelValue"
:options="options"
:disabled="disabled"
:class="['ui-checkbox-group', { 'ui-checkbox-group--vertical': direction === 'vertical' }]"
@update:value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { CheckboxGroup as ACheckboxGroup } from 'ant-design-vue'
interface IOption {
label: string
value: any
disabled?: boolean
}
interface IProps {
modelValue: Array<any>
options: IOption[]
disabled?: boolean
direction?: 'horizontal' | 'vertical'
}
withDefaults(defineProps<IProps>(), {
disabled: false,
direction: 'horizontal',
})
const emit = defineEmits<{
'update:modelValue': [value: Array<any>]
change: [value: Array<any>]
}>()
</script>
<style scoped>
.ui-checkbox-group :deep(.ant-checkbox-wrapper) {
color: var(--text-primary);
}
.ui-checkbox-group :deep(.ant-checkbox-inner) {
border-radius: var(--radius-sm, 4px);
border-color: var(--border-color-hover);
background: var(--bg-primary);
}
.ui-checkbox-group :deep(.ant-checkbox-checked .ant-checkbox-inner) {
background: var(--color-primary);
border-color: var(--color-primary);
}
.ui-checkbox-group--vertical {
display: flex;
flex-direction: column;
gap: var(--space-2, 8px);
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<a-date-picker
:value="modelValue"
:placeholder="placeholder"
:show-time="showTime"
:format="format"
:disabled="disabled"
:size="antSize"
class="ui-date-picker"
@update:value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { DatePicker as ADatePicker } from 'ant-design-vue'
interface IProps {
modelValue: any
placeholder?: string
showTime?: boolean
format?: string
disabled?: boolean
size?: 'sm' | 'md'
}
const props = withDefaults(defineProps<IProps>(), {
format: 'YYYY-MM-DD',
disabled: false,
size: 'md',
})
const emit = defineEmits<{
'update:modelValue': [value: any]
change: [value: any]
}>()
const antSize = computed(() => (props.size === 'sm' ? 'small' : 'middle'))
</script>
<style scoped>
.ui-date-picker {
width: 100%;
}
.ui-date-picker :deep(.ant-picker) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-date-picker :deep(.ant-picker:hover) {
border-color: var(--border-color-hover);
}
.ui-date-picker :deep(.ant-picker-focused) {
border-color: var(--border-color-active);
box-shadow: none;
}
.ui-date-picker :deep(.ant-picker-input > input) {
color: var(--text-primary);
}
.ui-date-picker :deep(.ant-picker-input > input::placeholder) {
color: var(--text-placeholder);
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<a-range-picker
:value="modelValue"
:placeholder="placeholders"
:show-time="showTime"
:format="format"
:disabled="disabled"
class="ui-date-range-picker"
@update:value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { RangePicker as ARangePicker } from 'ant-design-vue'
interface IProps {
modelValue?: [any, any]
placeholders?: [string, string]
showTime?: boolean
format?: string
disabled?: boolean
}
withDefaults(defineProps<IProps>(), {
disabled: false,
})
const emit = defineEmits<{
'update:modelValue': [value: [any, any]]
change: [value: [any, any]]
}>()
</script>
<style scoped>
.ui-date-range-picker {
width: 100%;
}
.ui-date-range-picker :deep(.ant-picker) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-date-range-picker :deep(.ant-picker:hover) {
border-color: var(--border-color-hover);
}
.ui-date-range-picker :deep(.ant-picker-focused) {
border-color: var(--border-color-active);
box-shadow: none;
}
.ui-date-range-picker :deep(.ant-picker-input > input) {
color: var(--text-primary);
}
.ui-date-range-picker :deep(.ant-picker-input > input::placeholder) {
color: var(--text-placeholder);
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<div class="ui-form-field">
<label v-if="label" class="ui-form-field__label">
<span v-if="required" class="ui-form-field__required">*</span>
<span class="ui-form-field__label-text">{{ label }}</span>
</label>
<div class="ui-form-field__control">
<slot />
</div>
<p v-if="error" class="ui-form-field__error">{{ error }}</p>
<p v-else-if="hint" class="ui-form-field__hint">{{ hint }}</p>
</div>
</template>
<script setup lang="ts">
interface IProps {
label?: string
required?: boolean
error?: string
hint?: string
}
withDefaults(defineProps<IProps>(), {
required: false,
})
</script>
<style scoped>
.ui-form-field {
display: flex;
flex-direction: column;
gap: var(--space-1, 4px);
}
.ui-form-field__label {
display: inline-flex;
align-items: center;
gap: var(--space-1, 4px);
font-size: var(--font-sm, 13px);
color: var(--text-secondary);
line-height: 1.5;
}
.ui-form-field__required {
color: var(--color-error);
line-height: 1;
}
.ui-form-field__label-text {
min-width: 0;
}
.ui-form-field__control {
min-width: 0;
}
.ui-form-field__error {
margin: 0;
font-size: var(--font-xs, 12px);
color: var(--color-error);
line-height: 1.5;
}
.ui-form-field__hint {
margin: 0;
font-size: var(--font-xs, 12px);
color: var(--text-tertiary);
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<UiCard padding="lg" class="ui-form-section">
<div v-if="title || description" class="ui-form-section__header">
<h3 v-if="title" class="ui-form-section__title">{{ title }}</h3>
<p v-if="description" class="ui-form-section__description">{{ description }}</p>
</div>
<div class="ui-form-section__body">
<slot />
</div>
</UiCard>
</template>
<script setup lang="ts">
import UiCard from './UiCard.vue'
interface IProps {
title?: string
description?: string
}
withDefaults(defineProps<IProps>(), {})
</script>
<style scoped>
.ui-form-section__header {
margin-bottom: var(--space-4, 16px);
}
.ui-form-section__title {
margin: 0;
font-size: var(--font-md, 16px);
font-weight: var(--font-weight-medium, 500);
color: var(--text-primary);
line-height: 1.4;
}
.ui-form-section__description {
margin: var(--space-1, 4px) 0 0;
font-size: var(--font-sm, 13px);
color: var(--text-tertiary);
line-height: 1.5;
}
.ui-form-section__body {
margin-top: var(--space-4, 16px);
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<a-input
:value="modelValue"
:placeholder="placeholder"
:size="antSize"
:disabled="disabled"
:readonly="readonly"
:maxlength="maxLength"
:allow-clear="allowClear"
class="ui-input"
@update:value="onUpdate"
@change="emit('change', $event)"
@press-enter="emit('pressEnter', $event)"
>
<template v-if="$slots.prefix" #prefix><slot name="prefix" /></template>
<template v-if="$slots.suffix" #suffix><slot name="suffix" /></template>
</a-input>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Input as AInput } from 'ant-design-vue'
interface IProps {
modelValue: string | number
placeholder?: string
size?: 'sm' | 'md'
disabled?: boolean
readonly?: boolean
maxLength?: number
allowClear?: boolean
}
const props = withDefaults(defineProps<IProps>(), {
size: 'md',
disabled: false,
readonly: false,
allowClear: true,
})
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [e: Event]
pressEnter: [e: KeyboardEvent]
}>()
const antSize = computed(() => (props.size === 'sm' ? 'small' : 'middle'))
function onUpdate(val: string) {
emit('update:modelValue', val)
}
</script>
<style scoped>
.ui-input {
width: 100%;
}
.ui-input :deep(.ant-input) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
color: var(--text-primary);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-input :deep(.ant-input::placeholder) {
color: var(--text-placeholder);
}
.ui-input :deep(.ant-input:hover) {
border-color: var(--border-color-hover);
}
.ui-input :deep(.ant-input:focus),
.ui-input :deep(.ant-input-focused) {
border-color: var(--border-color-active);
box-shadow: none;
}
.ui-input :deep(.ant-input-affix-wrapper) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-input :deep(.ant-input-affix-wrapper:hover) {
border-color: var(--border-color-hover);
}
.ui-input :deep(.ant-input-affix-wrapper:focus),
.ui-input :deep(.ant-input-affix-wrapper-focused) {
border-color: var(--border-color-active);
box-shadow: none;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<a-radio :value="value" :disabled="disabled" class="ui-radio">
<slot />
</a-radio>
</template>
<script setup lang="ts">
import { Radio as ARadio } from 'ant-design-vue'
interface IProps {
value: any
disabled?: boolean
}
withDefaults(defineProps<IProps>(), {
disabled: false,
})
</script>
<style scoped>
.ui-radio :deep(.ant-radio-wrapper) {
color: var(--text-primary);
}
.ui-radio :deep(.ant-radio-inner) {
border-color: var(--border-color-hover);
background: var(--bg-primary);
}
.ui-radio :deep(.ant-radio-checked .ant-radio-inner) {
border-color: var(--color-primary);
}
.ui-radio :deep(.ant-radio-inner::after) {
background-color: var(--color-primary);
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<a-radio-group
:value="modelValue"
:options="options"
:disabled="disabled"
:class="['ui-radio-group', { 'ui-radio-group--vertical': direction === 'vertical' }]"
@update:value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { RadioGroup as ARadioGroup } from 'ant-design-vue'
interface IOption {
label: string
value: any
disabled?: boolean
}
interface IProps {
modelValue: any
options: IOption[]
disabled?: boolean
direction?: 'horizontal' | 'vertical'
}
withDefaults(defineProps<IProps>(), {
disabled: false,
direction: 'horizontal',
})
const emit = defineEmits<{
'update:modelValue': [value: any]
change: [e: Event]
}>()
</script>
<style scoped>
.ui-radio-group :deep(.ant-radio-wrapper) {
color: var(--text-primary);
}
.ui-radio-group :deep(.ant-radio-inner) {
border-color: var(--border-color-hover);
background: var(--bg-primary);
}
.ui-radio-group :deep(.ant-radio-checked .ant-radio-inner) {
border-color: var(--color-primary);
}
.ui-radio-group :deep(.ant-radio-inner::after) {
background-color: var(--color-primary);
}
.ui-radio-group--vertical {
display: flex;
flex-direction: column;
gap: var(--space-2, 8px);
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<a-input
:value="modelValue"
:placeholder="placeholder"
:size="antSize"
:allow-clear="true"
class="ui-search-input"
@update:value="onInput"
>
<template #prefix>
<SearchOutlined class="ui-search-input__icon" />
</template>
</a-input>
</template>
<script setup lang="ts">
import { computed, watch, onBeforeUnmount, ref } from 'vue'
import { Input as AInput } from 'ant-design-vue'
import { SearchOutlined } from '@ant-design/icons-vue'
interface IProps {
modelValue: string
placeholder?: string
debounce?: number
size?: 'sm' | 'md'
}
const props = withDefaults(defineProps<IProps>(), {
placeholder: '搜索...',
debounce: 300,
size: 'md',
})
const emit = defineEmits<{
'update:modelValue': [value: string]
search: [value: string]
}>()
const antSize = computed(() => (props.size === 'sm' ? 'small' : 'middle'))
let timer: ReturnType<typeof setTimeout> | null = null
const lastValue = ref(props.modelValue)
function clearTimer() {
if (timer) {
clearTimeout(timer)
timer = null
}
}
function onInput(val: string) {
emit('update:modelValue', val)
}
watch(
() => props.modelValue,
(val) => {
if (val === lastValue.value) return
clearTimer()
//
if (val === '') {
lastValue.value = val
emit('search', val)
return
}
timer = setTimeout(() => {
lastValue.value = val
emit('search', val)
}, props.debounce)
},
)
onBeforeUnmount(() => {
clearTimer()
})
</script>
<style scoped>
.ui-search-input {
width: 100%;
}
.ui-search-input__icon {
color: var(--text-tertiary);
}
.ui-search-input :deep(.ant-input-affix-wrapper) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-search-input :deep(.ant-input-affix-wrapper:hover) {
border-color: var(--border-color-hover);
}
.ui-search-input :deep(.ant-input-affix-wrapper:focus),
.ui-search-input :deep(.ant-input-affix-wrapper-focused) {
border-color: var(--border-color-active);
box-shadow: none;
}
.ui-search-input :deep(.ant-input::placeholder) {
color: var(--text-placeholder);
}
</style>

View File

@ -0,0 +1,102 @@
<template>
<a-select
:value="modelValue"
:options="options"
:placeholder="placeholder"
:mode="multiple ? 'multiple' : undefined"
:show-search="searchable"
:allow-clear="clearable"
:disabled="disabled"
:size="antSize"
:loading="loading"
class="ui-select"
@update:value="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Select as ASelect } from 'ant-design-vue'
interface IOption {
label: string
value: any
disabled?: boolean
}
interface IProps {
modelValue: any
options: IOption[]
placeholder?: string
multiple?: boolean
searchable?: boolean
clearable?: boolean
disabled?: boolean
size?: 'sm' | 'md'
loading?: boolean
}
const props = withDefaults(defineProps<IProps>(), {
placeholder: '请选择',
multiple: false,
searchable: false,
clearable: true,
disabled: false,
size: 'md',
loading: false,
})
const emit = defineEmits<{
'update:modelValue': [value: any]
change: [value: any]
}>()
const antSize = computed(() => (props.size === 'sm' ? 'small' : 'middle'))
</script>
<style scoped>
.ui-select {
width: 100%;
}
.ui-select :deep(.ant-select-selector) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-select :deep(.ant-select-selector .ant-select-selection-placeholder) {
color: var(--text-placeholder);
}
.ui-select :deep(.ant-select:hover .ant-select-selector) {
border-color: var(--border-color-hover);
}
.ui-select :deep(.ant-select-focused .ant-select-selector),
.ui-select :deep(.ant-select-selector:focus) {
border-color: var(--border-color-active);
box-shadow: none;
}
</style>
<style>
/* 下拉面板非 scoped使用全局样式覆盖背景 */
.ui-select + .ant-select-dropdown,
.ant-select-dropdown {
background: var(--bg-elevated);
border-radius: var(--radius-md, 6px);
}
.ant-select-dropdown .ant-select-item {
color: var(--text-secondary);
}
.ant-select-dropdown .ant-select-item-option-active,
.ant-select-dropdown .ant-select-item-option-selected {
background: var(--bg-tertiary);
color: var(--text-primary);
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<a-switch
:checked="modelValue"
:disabled="disabled"
:size="antSize"
:checked-children="checkedText"
:un-checked-children="uncheckedText"
class="ui-switch"
@update:checked="emit('update:modelValue', $event)"
@change="emit('change', $event)"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Switch as ASwitch } from 'ant-design-vue'
interface IProps {
modelValue: boolean
disabled?: boolean
size?: 'sm' | 'md'
checkedText?: string
uncheckedText?: string
}
const props = withDefaults(defineProps<IProps>(), {
disabled: false,
size: 'md',
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
change: [value: boolean]
}>()
const antSize = computed(() => (props.size === 'sm' ? 'small' : 'default'))
</script>
<style scoped>
.ui-switch :deep(.ant-switch-checked) {
background: var(--color-primary);
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<a-tabs
class="ui-tabs"
:active-key="modelValue"
:tab-position="position"
@change="onChange"
>
<a-tab-pane
v-for="item in items"
:key="item.key"
:disabled="item.disabled"
>
<template #tab>
<span class="ui-tabs__tab-inner">
<component :is="item.icon" v-if="item.icon" class="ui-tabs__tab-icon" />
<span>{{ item.label }}</span>
</span>
</template>
</a-tab-pane>
</a-tabs>
</template>
<script setup lang="ts">
import { Tabs as ATabs, TabPane as ATabPane } from 'ant-design-vue'
import type { Component } from 'vue'
interface ITabItem {
key: string
label: string
icon?: Component
disabled?: boolean
}
interface IProps {
items: ITabItem[]
modelValue: string
position?: 'top' | 'left'
}
withDefaults(defineProps<IProps>(), {
position: 'top',
})
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [value: string]
}>()
function onChange(activeKey: string | number) {
const key = String(activeKey)
emit('update:modelValue', key)
emit('change', key)
}
</script>
<style scoped>
/* ponytail: 仅作导航 tab 用,隐藏空内容区。需要内容请用 a-tabs 原生写法。 */
.ui-tabs :deep(.ant-tabs-content-holder) {
display: none;
}
.ui-tabs :deep(.ant-tabs-tab) {
padding: var(--space-2) var(--space-4) !important;
font-size: var(--font-sm);
color: var(--text-secondary);
border-radius: var(--radius-md);
transition: color var(--transition-fast), background var(--transition-fast);
margin: 0 var(--space-1) !important;
}
.ui-tabs :deep(.ant-tabs-tab:hover) {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.ui-tabs :deep(.ant-tabs-tab-active) {
color: var(--text-primary) !important;
}
.ui-tabs :deep(.ant-tabs-tab-active .ant-tabs-tab-btn)

View File

@ -0,0 +1,66 @@
<template>
<a-textarea
:value="modelValue"
:placeholder="placeholder"
:rows="rows"
:auto-size="autoSize"
:maxlength="maxLength"
:disabled="disabled"
:show-count="showCount"
class="ui-textarea"
@update:value="emit('update:modelValue', $event)"
/>
</template>
<script setup lang="ts">
import { Textarea as ATextarea } from 'ant-design-vue'
interface IAutoSize {
minRows?: number
maxRows?: number
}
interface IProps {
modelValue: string
placeholder?: string
rows?: number
autoSize?: boolean | IAutoSize
maxLength?: number
disabled?: boolean
showCount?: boolean
}
withDefaults(defineProps<IProps>(), {
rows: 3,
autoSize: false,
disabled: false,
showCount: false,
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
<style scoped>
.ui-textarea :deep(.ant-input) {
border-radius: var(--radius-md, 6px);
border-color: var(--border-color);
color: var(--text-primary);
background: var(--bg-primary);
transition: border-color var(--transition-fast, 150ms ease);
}
.ui-textarea :deep(.ant-input::placeholder) {
color: var(--text-placeholder);
}
.ui-textarea :deep(.ant-input:hover) {
border-color: var(--border-color-hover);
}
.ui-textarea :deep(.ant-input:focus) {
border-color: var(--border-color-active);
box-shadow: none;
}
</style>