feat(AiProviders): improve layout and styling for OpenAI edit and models pages

This commit is contained in:
Supra4E8C
2026-02-14 20:35:16 +08:00
parent b702cd6e4c
commit d140fe1061
3 changed files with 156 additions and 81 deletions

View File

@@ -511,9 +511,9 @@ export function AiProvidersOpenAIEditPage() {
>
<Card>
{invalidIndexParam || invalidIndex ? (
<div className="hint">{t('common.invalid_provider_index')}</div>
<div className={styles.sectionHint}>{t('common.invalid_provider_index')}</div>
) : (
<>
<div className={styles.openaiEditForm}>
<Input
label={t('ai_providers.openai_add_modal_name_label')}
value={form.name}
@@ -579,7 +579,7 @@ export function AiProvidersOpenAIEditPage() {
</div>
{/* 提示文本 */}
<div className="hint">{t('ai_providers.openai_models_hint')}</div>
<div className={styles.sectionHint}>{t('ai_providers.openai_models_hint')}</div>
{/* 模型列表 */}
<ModelInputList
@@ -649,14 +649,14 @@ export function AiProvidersOpenAIEditPage() {
)}
</div>
<div className={`form-group ${styles.keyEntriesSection}`}>
<div className={styles.keyEntriesSection}>
<div className={styles.keyEntriesHeader}>
<label>{t('ai_providers.openai_add_modal_keys_label')}</label>
<label className={styles.keyEntriesTitle}>{t('ai_providers.openai_add_modal_keys_label')}</label>
<span className={styles.keyEntriesHint}>{t('ai_providers.openai_keys_hint')}</span>
</div>
{renderKeyEntries(form.apiKeyEntries)}
</div>
</>
</div>
)}
</Card>
</SecondaryScreenShell>

View File

@@ -153,70 +153,76 @@ export function AiProvidersOpenAIModelsPage() {
loadingLabel={t('common.loading')}
>
<Card>
<div className="hint" style={{ marginBottom: 8 }}>
{t('ai_providers.openai_models_fetch_hint')}
</div>
<div className="form-group">
<label>{t('ai_providers.openai_models_fetch_url_label')}</label>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input className="input" readOnly value={endpoint} />
<Button
variant="secondary"
size="sm"
onClick={() => void fetchOpenaiModelDiscovery({ allowFallback: true })}
loading={fetching}
disabled={disableControls || saving}
>
{t('ai_providers.openai_models_fetch_refresh')}
</Button>
<div className={styles.openaiModelsContent}>
<div className={styles.sectionHint}>{t('ai_providers.openai_models_fetch_hint')}</div>
<div className={styles.openaiModelsEndpointSection}>
<label className={styles.openaiModelsEndpointLabel}>
{t('ai_providers.openai_models_fetch_url_label')}
</label>
<div className={styles.openaiModelsEndpointControls}>
<input
className={`input ${styles.openaiModelsEndpointInput}`}
readOnly
value={endpoint}
/>
<Button
variant="secondary"
size="sm"
onClick={() => void fetchOpenaiModelDiscovery({ allowFallback: true })}
loading={fetching}
disabled={disableControls || saving}
>
{t('ai_providers.openai_models_fetch_refresh')}
</Button>
</div>
</div>
</div>
<Input
label={t('ai_providers.openai_models_search_label')}
placeholder={t('ai_providers.openai_models_search_placeholder')}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={fetching}
/>
{error && <div className="error-box">{error}</div>}
{fetching ? (
<div className="hint">{t('ai_providers.openai_models_fetch_loading')}</div>
) : models.length === 0 ? (
<div className="hint">{t('ai_providers.openai_models_fetch_empty')}</div>
) : filteredModels.length === 0 ? (
<div className="hint">{t('ai_providers.openai_models_search_empty')}</div>
) : (
<div className={styles.modelDiscoveryList}>
{filteredModels.map((model) => {
const checked = selected.has(model.name);
return (
<label
key={model.name}
className={`${styles.modelDiscoveryRow} ${
checked ? styles.modelDiscoveryRowSelected : ''
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleSelection(model.name)}
/>
<div className={styles.modelDiscoveryMeta}>
<div className={styles.modelDiscoveryName}>
{model.name}
{model.alias && (
<span className={styles.modelDiscoveryAlias}>{model.alias}</span>
<Input
label={t('ai_providers.openai_models_search_label')}
placeholder={t('ai_providers.openai_models_search_placeholder')}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={fetching}
/>
{error && <div className="error-box">{error}</div>}
{fetching ? (
<div className={styles.sectionHint}>{t('ai_providers.openai_models_fetch_loading')}</div>
) : models.length === 0 ? (
<div className={styles.sectionHint}>{t('ai_providers.openai_models_fetch_empty')}</div>
) : filteredModels.length === 0 ? (
<div className={styles.sectionHint}>{t('ai_providers.openai_models_search_empty')}</div>
) : (
<div className={styles.modelDiscoveryList}>
{filteredModels.map((model) => {
const checked = selected.has(model.name);
return (
<label
key={model.name}
className={`${styles.modelDiscoveryRow} ${
checked ? styles.modelDiscoveryRowSelected : ''
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleSelection(model.name)}
/>
<div className={styles.modelDiscoveryMeta}>
<div className={styles.modelDiscoveryName}>
{model.name}
{model.alias && (
<span className={styles.modelDiscoveryAlias}>{model.alias}</span>
)}
</div>
{model.description && (
<div className={styles.modelDiscoveryDesc}>{model.description}</div>
)}
</div>
{model.description && (
<div className={styles.modelDiscoveryDesc}>{model.description}</div>
)}
</div>
</label>
);
})}
</div>
)}
</label>
);
})}
</div>
)}
</div>
</Card>
</SecondaryScreenShell>
);

View File

@@ -322,7 +322,7 @@
gap: 6px;
max-height: 360px;
overflow-y: auto;
margin-top: 8px;
margin-top: 0;
padding-right: 4px;
}
@@ -561,11 +561,72 @@
// Model Config Section - Unified Layout
// ============================================
.modelConfigSection {
margin-bottom: $spacing-md;
.openaiEditForm {
display: flex;
flex-direction: column;
gap: $spacing-md;
:global(.form-group) {
margin-bottom: 0;
}
:global(.status-badge) {
margin-bottom: 0;
align-self: flex-start;
}
}
.sectionHint {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--text-secondary);
}
.openaiModelsContent {
display: flex;
flex-direction: column;
gap: $spacing-md;
:global(.form-group) {
margin-bottom: 0;
}
}
.openaiModelsEndpointSection {
display: flex;
flex-direction: column;
gap: $spacing-xs;
}
.openaiModelsEndpointLabel {
display: block;
margin: 0;
font-weight: 600;
color: var(--text-primary);
}
.openaiModelsEndpointControls {
display: flex;
align-items: center;
gap: $spacing-sm;
@include mobile {
flex-direction: column;
align-items: stretch;
}
}
.openaiModelsEndpointInput {
flex: 1;
min-width: 0;
}
.modelConfigSection {
margin-bottom: 0;
display: flex;
flex-direction: column;
gap: $spacing-md;
}
.modelConfigHeader {
@@ -581,10 +642,11 @@
}
.modelConfigTitle {
margin: 0;
font-weight: 600;
color: var(--text-primary);
font-size: 14px;
line-height: 1.4;
line-height: 1.5;
}
.modelConfigToolbar {
@@ -646,7 +708,7 @@
align-items: flex-start;
justify-content: space-between;
gap: $spacing-md;
margin-top: $spacing-sm;
margin-top: 0;
padding: $spacing-sm $spacing-md;
border: 1px solid var(--border-color);
border-radius: $radius-md;
@@ -661,7 +723,7 @@
.modelTestMeta {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
min-width: 0;
}
@@ -669,13 +731,13 @@
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
line-height: 1.4;
line-height: 1.5;
}
.modelTestHint {
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.4;
line-height: 1.5;
}
.modelTestControls {
@@ -697,22 +759,29 @@
.keyEntriesSection {
margin-bottom: 0;
display: flex;
flex-direction: column;
gap: $spacing-sm;
}
.keyEntriesHeader {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: $spacing-sm;
gap: 6px;
margin-bottom: 0;
}
label {
margin: 0;
}
.keyEntriesTitle {
display: block;
margin: 0;
font-weight: 600;
color: var(--text-primary);
line-height: 1.5;
}
.keyEntriesHint {
font-size: 13px;
line-height: 1.4;
line-height: 1.5;
color: var(--text-secondary);
}