mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
feat: enhance theme customizer with shadow options and update styles
- Added shadow customization options to the theme customizer, allowing users to select from 'none', 'low', 'medium', and 'high'. - Updated the theme customizer settings interface and default values to include shadow settings. - Enhanced the CSS variables for shadows in common.scss to support different shadow levels based on user selection. - Modified the VirtualSlideView component styles to improve layout and scrolling behavior. - Updated localization files for English, Simplified Chinese, and Traditional Chinese to include new shadow-related terms. - Adjusted various components to ensure consistent application of shadow styles across the application.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
themeCustomizerPrimaryColors,
|
themeCustomizerPrimaryColors,
|
||||||
useThemeCustomizer,
|
useThemeCustomizer,
|
||||||
type ThemeCustomizerLayout,
|
type ThemeCustomizerLayout,
|
||||||
|
type ThemeCustomizerShadow,
|
||||||
type ThemeCustomizerSkin,
|
type ThemeCustomizerSkin,
|
||||||
type ThemeCustomizerTheme,
|
type ThemeCustomizerTheme,
|
||||||
} from '@/composables/useThemeCustomizer'
|
} from '@/composables/useThemeCustomizer'
|
||||||
@@ -27,8 +28,17 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const customColorInput = ref<HTMLInputElement | null>(null)
|
const customColorInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const { isCustomized, resetSettings, setLayout, setPrimaryColor, setSemiDarkMenu, setSkin, setTheme, settings } =
|
const {
|
||||||
useThemeCustomizer()
|
isCustomized,
|
||||||
|
resetSettings,
|
||||||
|
setLayout,
|
||||||
|
setPrimaryColor,
|
||||||
|
setSemiDarkMenu,
|
||||||
|
setShadow,
|
||||||
|
setSkin,
|
||||||
|
setTheme,
|
||||||
|
settings,
|
||||||
|
} = useThemeCustomizer()
|
||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
@@ -98,6 +108,30 @@ const skinOptions = computed<Array<{ title: string; value: ThemeCustomizerSkin }
|
|||||||
{ title: t('theme.customizer.skinBordered'), value: 'bordered' },
|
{ title: t('theme.customizer.skinBordered'), value: 'bordered' },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const shadowOptions = computed<
|
||||||
|
Array<{
|
||||||
|
title: string
|
||||||
|
value: ThemeCustomizerShadow
|
||||||
|
}>
|
||||||
|
>(() => [
|
||||||
|
{
|
||||||
|
title: t('theme.customizer.shadowNone'),
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('theme.customizer.shadowLow'),
|
||||||
|
value: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('theme.customizer.shadowMedium'),
|
||||||
|
value: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('theme.customizer.shadowHigh'),
|
||||||
|
value: 'high',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const layoutOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerLayout }>>(() => [
|
const layoutOptions = computed<Array<{ icon: string; title: string; value: ThemeCustomizerLayout }>>(() => [
|
||||||
{ title: t('theme.customizer.layoutVertical'), value: 'vertical', icon: 'mdi-dock-left' },
|
{ title: t('theme.customizer.layoutVertical'), value: 'vertical', icon: 'mdi-dock-left' },
|
||||||
{ title: t('theme.customizer.layoutCollapsed'), value: 'collapsed', icon: 'mdi-dock-window' },
|
{ title: t('theme.customizer.layoutCollapsed'), value: 'collapsed', icon: 'mdi-dock-window' },
|
||||||
@@ -109,6 +143,7 @@ const showLayoutSection = computed(() => !appMode.value)
|
|||||||
const hasAppModeCustomization = computed(() => {
|
const hasAppModeCustomization = computed(() => {
|
||||||
return (
|
return (
|
||||||
settings.value.primaryColor !== defaultPrimaryColor ||
|
settings.value.primaryColor !== defaultPrimaryColor ||
|
||||||
|
settings.value.shadow !== 'none' ||
|
||||||
settings.value.skin !== 'default' ||
|
settings.value.skin !== 'default' ||
|
||||||
settings.value.theme !== 'auto'
|
settings.value.theme !== 'auto'
|
||||||
)
|
)
|
||||||
@@ -150,6 +185,7 @@ async function handleResetSettings() {
|
|||||||
|
|
||||||
// App 模式共享定制器,但保留桌面导航相关偏好,只重置 App 侧可调整的外观设置。
|
// App 模式共享定制器,但保留桌面导航相关偏好,只重置 App 侧可调整的外观设置。
|
||||||
await setPrimaryColor(defaultPrimaryColor)
|
await setPrimaryColor(defaultPrimaryColor)
|
||||||
|
await setShadow('none')
|
||||||
await setSkin('default')
|
await setSkin('default')
|
||||||
await setTheme('auto')
|
await setTheme('auto')
|
||||||
}
|
}
|
||||||
@@ -262,6 +298,35 @@ async function handleResetSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VDivider class="mt-7" />
|
||||||
|
|
||||||
|
<h3 class="theme-customizer-section-title">{{ t('theme.customizer.shadow') }}</h3>
|
||||||
|
<div class="theme-customizer-preview-grid theme-customizer-preview-grid--shadow">
|
||||||
|
<div
|
||||||
|
v-for="shadow in shadowOptions"
|
||||||
|
:key="shadow.value"
|
||||||
|
class="theme-customizer-preview-option"
|
||||||
|
:class="{ 'is-active': settings.shadow === shadow.value }"
|
||||||
|
@click="setShadow(shadow.value)"
|
||||||
|
>
|
||||||
|
<span class="theme-customizer-shadow-scene" :class="`theme-customizer-shadow-scene--${shadow.value}`">
|
||||||
|
<span class="theme-customizer-shadow-scene__panel">
|
||||||
|
<span class="theme-customizer-shadow-scene__panel-line" />
|
||||||
|
<span
|
||||||
|
class="theme-customizer-shadow-scene__panel-line theme-customizer-shadow-scene__panel-line--short"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="theme-customizer-shadow-scene__card">
|
||||||
|
<span class="theme-customizer-shadow-scene__badge" />
|
||||||
|
<span class="theme-customizer-shadow-scene__line theme-customizer-shadow-scene__line--short" />
|
||||||
|
<span class="theme-customizer-shadow-scene__line" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>{{ shadow.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
<div v-if="showSemiDarkMenuOption" class="theme-customizer-semi-dark">
|
||||||
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
<span>{{ t('theme.customizer.semiDarkMenu') }}</span>
|
||||||
<VSwitch
|
<VSwitch
|
||||||
@@ -573,6 +638,10 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-customizer-preview-grid--shadow {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.theme-customizer-preview-option {
|
.theme-customizer-preview-option {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -584,7 +653,8 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
||||||
.theme-customizer-mini-layout {
|
.theme-customizer-mini-layout,
|
||||||
|
.theme-customizer-shadow-scene {
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
background: rgba(var(--v-theme-primary), 0.04);
|
background: rgba(var(--v-theme-primary), 0.04);
|
||||||
@@ -632,6 +702,11 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
|||||||
.theme-customizer-mini-layout--horizontal {
|
.theme-customizer-mini-layout--horizontal {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 24% 1fr;
|
grid-template-rows: 24% 1fr;
|
||||||
|
|
||||||
|
.mini-sidebar {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-sidebar,
|
.mini-sidebar,
|
||||||
@@ -667,10 +742,111 @@ html[data-theme='transparent'] .v-overlay__content:has(.theme-customizer-drawer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-customizer-mini-layout--horizontal {
|
.theme-customizer-shadow-scene {
|
||||||
.mini-sidebar {
|
position: relative;
|
||||||
flex-direction: row;
|
display: block;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(var(--v-theme-on-surface), 0.02), rgba(var(--v-theme-on-surface), 0.06)),
|
||||||
|
rgb(var(--v-theme-surface));
|
||||||
|
block-size: 110px;
|
||||||
|
inline-size: 100%;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__panel,
|
||||||
|
.theme-customizer-shadow-scene__card {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
box-shadow: none;
|
||||||
|
transition: box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__panel {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
inset-block-start: 16px;
|
||||||
|
inset-inline: 14px;
|
||||||
|
min-block-size: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__card {
|
||||||
|
gap: 8px;
|
||||||
|
inset-block-end: 12px;
|
||||||
|
inset-inline: 20px 16px;
|
||||||
|
min-block-size: 46px;
|
||||||
|
padding-block: 10px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__panel-line,
|
||||||
|
.theme-customizer-shadow-scene__line,
|
||||||
|
.theme-customizer-shadow-scene__badge {
|
||||||
|
display: block;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__badge {
|
||||||
|
block-size: 6px;
|
||||||
|
inline-size: 34%;
|
||||||
|
min-inline-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__panel-line,
|
||||||
|
.theme-customizer-shadow-scene__line {
|
||||||
|
block-size: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__panel-line--short,
|
||||||
|
.theme-customizer-shadow-scene__line--short {
|
||||||
|
inline-size: 62%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene--low {
|
||||||
|
.theme-customizer-shadow-scene__panel {
|
||||||
|
box-shadow:
|
||||||
|
0 8px 18px rgba(var(--v-theme-on-surface), 0.08),
|
||||||
|
0 2px 6px rgba(var(--v-theme-on-surface), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__card {
|
||||||
|
box-shadow:
|
||||||
|
0 10px 22px rgba(var(--v-theme-on-surface), 0.1),
|
||||||
|
0 4px 10px rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene--medium {
|
||||||
|
.theme-customizer-shadow-scene__panel {
|
||||||
|
box-shadow:
|
||||||
|
0 12px 28px rgba(var(--v-theme-on-surface), 0.12),
|
||||||
|
0 4px 12px rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__card {
|
||||||
|
box-shadow:
|
||||||
|
0 16px 34px rgba(var(--v-theme-on-surface), 0.14),
|
||||||
|
0 6px 16px rgba(var(--v-theme-on-surface), 0.09);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene--high {
|
||||||
|
.theme-customizer-shadow-scene__panel {
|
||||||
|
box-shadow:
|
||||||
|
0 16px 38px rgba(var(--v-theme-on-surface), 0.16),
|
||||||
|
0 6px 18px rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-customizer-shadow-scene__card {
|
||||||
|
box-shadow:
|
||||||
|
0 22px 48px rgba(var(--v-theme-on-surface), 0.18),
|
||||||
|
0 8px 22px rgba(var(--v-theme-on-surface), 0.12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ function handleCardClick() {
|
|||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<div
|
<div
|
||||||
class="w-full h-full rounded-lg overflow-hidden relative"
|
class="w-full h-full rounded-lg relative"
|
||||||
:class="{
|
:class="{
|
||||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
|
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering && !props.sortable,
|
||||||
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
|
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
|
||||||
@@ -414,12 +414,12 @@ function handleCardClick() {
|
|||||||
<VCard
|
<VCard
|
||||||
v-bind="hover.props"
|
v-bind="hover.props"
|
||||||
:key="props.media?.id"
|
:key="props.media?.id"
|
||||||
class="flex flex-col h-full"
|
class="flex flex-col h-full overflow-hidden"
|
||||||
:class="{
|
:class="{
|
||||||
'subscribe-card-paused': subscribeState === 'S',
|
'subscribe-card-paused': subscribeState === 'S',
|
||||||
'cursor-move': props.sortable,
|
'cursor-move': props.sortable,
|
||||||
}"
|
}"
|
||||||
rounded="0"
|
rounded="lg"
|
||||||
min-height="150"
|
min-height="150"
|
||||||
@click="handleCardClick"
|
@click="handleCardClick"
|
||||||
:ripple="!props.batchMode && !props.sortable"
|
:ripple="!props.batchMode && !props.sortable"
|
||||||
@@ -506,10 +506,26 @@ function handleCardClick() {
|
|||||||
{{ subscribeProgressTooltip }}
|
{{ subscribeProgressTooltip }}
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
</div>
|
</div>
|
||||||
<VIcon v-if="props.media?.username && props.sortable" icon="mdi-account" size="small" color="white" class="flex-shrink-0 me-1" />
|
<VIcon
|
||||||
<IconBtn v-else-if="props.media?.username" icon="mdi-account" size="small" color="white" class="flex-shrink-0" />
|
v-if="props.media?.username && props.sortable"
|
||||||
|
icon="mdi-account"
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
class="flex-shrink-0 me-1"
|
||||||
|
/>
|
||||||
|
<IconBtn
|
||||||
|
v-else-if="props.media?.username"
|
||||||
|
icon="mdi-account"
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
<!-- 用户名过长时限制在卡片宽度内,并用省略号展示剩余内容 -->
|
<!-- 用户名过长时限制在卡片宽度内,并用省略号展示剩余内容 -->
|
||||||
<span v-if="props.media?.username" class="min-w-0 truncate text-subtitle-2 text-white" :title="props.media?.username">
|
<span
|
||||||
|
v-if="props.media?.username"
|
||||||
|
class="min-w-0 truncate text-subtitle-2 text-white"
|
||||||
|
:title="props.media?.username"
|
||||||
|
>
|
||||||
{{ props.media?.username }}
|
{{ props.media?.username }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,14 +593,15 @@ function handleCardClick() {
|
|||||||
.subscribe-card-pending-tint {
|
.subscribe-card-pending-tint {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe-card-pending-tint::after {
|
.subscribe-card-pending-tint::after {
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: inset 0 0 48px rgba(56, 189, 248, 40%); // sky-400
|
||||||
|
content: '';
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: inset 0 0 48px rgba(56, 189, 248, 0.4); // sky-400
|
|
||||||
z-index: 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -594,22 +611,23 @@ function handleCardClick() {
|
|||||||
*/
|
*/
|
||||||
.best-version-badge {
|
.best-version-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
z-index: 4;
|
||||||
left: 8px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 4;
|
border-radius: 50%;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
|
background: rgba(0, 0, 0, 75%);
|
||||||
|
block-size: 24px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 50%);
|
||||||
|
inline-size: 24px;
|
||||||
|
inset-block-start: 6px;
|
||||||
|
inset-inline-start: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.best-version-badge-full {
|
.best-version-badge-full {
|
||||||
background: rgba(255, 255, 255, 0.22);
|
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 22%);
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 255, 255, 15%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -38,54 +38,22 @@ onConnect((connection: Connection) => {
|
|||||||
// 当前选中的流程边ID
|
// 当前选中的流程边ID
|
||||||
const selectedEdgeId = ref<string | null>(null)
|
const selectedEdgeId = ref<string | null>(null)
|
||||||
|
|
||||||
// 当前选中的动作节点ID
|
|
||||||
const selectedNodeId = ref<string | null>(null)
|
|
||||||
|
|
||||||
// 流程边配置表单
|
// 流程边配置表单
|
||||||
const edgeForm = ref({
|
const edgeForm = ref({
|
||||||
condition: '',
|
condition: '',
|
||||||
join_policy: '',
|
|
||||||
branch_policy: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 动作节点执行配置表单
|
// 后端动作固定契约,供条件构造器读取上一节点输出
|
||||||
const nodeForm = ref({
|
const actionDefinitions = ref<any[]>([])
|
||||||
inputs_text: '',
|
|
||||||
outputs_text: '',
|
// 动作类型到契约的映射
|
||||||
join_policy: '',
|
const actionContractMap = computed(() => {
|
||||||
fail_policy: '',
|
return actionDefinitions.value.reduce((result, action) => {
|
||||||
branch_policy: '',
|
result[action.type] = action.contract || {}
|
||||||
concurrency_key: '',
|
return result
|
||||||
timeout: null as number | null,
|
}, {} as Record<string, any>)
|
||||||
retry_max_attempts: null as number | null,
|
|
||||||
retry_interval: null as number | null,
|
|
||||||
retry_backoff: null as number | null,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 汇合策略选项
|
|
||||||
const joinPolicyOptions = computed(() => [
|
|
||||||
{ title: t('dialog.workflowActions.joinPolicyDefault'), value: '' },
|
|
||||||
{ title: t('dialog.workflowActions.joinPolicyAllSuccess'), value: 'all_success' },
|
|
||||||
{ title: t('dialog.workflowActions.joinPolicyAnySuccess'), value: 'any_success' },
|
|
||||||
{ title: t('dialog.workflowActions.joinPolicyAllDone'), value: 'all_done' },
|
|
||||||
{ title: t('dialog.workflowActions.joinPolicyFailFast'), value: 'fail_fast' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 分支策略选项
|
|
||||||
const branchPolicyOptions = computed(() => [
|
|
||||||
{ title: t('dialog.workflowActions.branchPolicyDefault'), value: '' },
|
|
||||||
{ title: t('dialog.workflowActions.branchPolicyParallel'), value: 'parallel' },
|
|
||||||
{ title: t('dialog.workflowActions.branchPolicyExclusive'), value: 'exclusive' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 失败策略选项
|
|
||||||
const failPolicyOptions = computed(() => [
|
|
||||||
{ title: t('dialog.workflowActions.failPolicyDefault'), value: '' },
|
|
||||||
{ title: t('dialog.workflowActions.failPolicyStop'), value: 'stop' },
|
|
||||||
{ title: t('dialog.workflowActions.failPolicyContinue'), value: 'continue' },
|
|
||||||
{ title: t('dialog.workflowActions.failPolicyIgnore'), value: 'ignore' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 获取指定节点端口的类型(输入/输出)
|
// 获取指定节点端口的类型(输入/输出)
|
||||||
const getPortType = (node: GraphNode, handleId: string) => {
|
const getPortType = (node: GraphNode, handleId: string) => {
|
||||||
// 检查是否是输入端口(对应 handleBounds.target)
|
// 检查是否是输入端口(对应 handleBounds.target)
|
||||||
@@ -122,30 +90,30 @@ const getEdgeConfigValue = (edge: any, key: string) => {
|
|||||||
return edge?.[key] ?? edge?.data?.[key] ?? ''
|
return edge?.[key] ?? edge?.data?.[key] ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一流程边数据结构,确保条件和汇合策略能被后端读取
|
// 复制对象并移除不再由前端编辑的高级配置
|
||||||
|
const omitConfigKeys = (value: any, keys: string[]) => {
|
||||||
|
const result = { ...(value || {}) }
|
||||||
|
keys.forEach(key => delete result[key])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一流程边数据结构,前端只编辑边条件,汇合和分支策略由执行器默认处理
|
||||||
const normalizeWorkflowEdge = (edge: any) => {
|
const normalizeWorkflowEdge = (edge: any) => {
|
||||||
const condition = String(getEdgeConfigValue(edge, 'condition') || '').trim()
|
const condition = String(getEdgeConfigValue(edge, 'condition') || '').trim()
|
||||||
const joinPolicy = String(getEdgeConfigValue(edge, 'join_policy') || '').trim()
|
|
||||||
const branchPolicy = String(getEdgeConfigValue(edge, 'branch_policy') || '').trim()
|
|
||||||
const edgeClass = String(edge?.class || '')
|
const edgeClass = String(edge?.class || '')
|
||||||
.replace(/\bworkflow-conditional-edge\b/g, '')
|
.replace(/\bworkflow-conditional-edge\b/g, '')
|
||||||
.trim()
|
.trim()
|
||||||
const data = {
|
const data = omitConfigKeys(edge?.data, ['join_policy', 'branch_policy'])
|
||||||
...(edge?.data || {}),
|
data.condition = condition || undefined
|
||||||
condition: condition || undefined,
|
const edgePayload = omitConfigKeys(edge, ['join_policy', 'branch_policy'])
|
||||||
join_policy: joinPolicy || undefined,
|
|
||||||
branch_policy: branchPolicy || undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edgePayload,
|
||||||
animated: edge?.animated ?? true,
|
animated: edge?.animated ?? true,
|
||||||
type: edge?.type || 'animation',
|
type: edge?.type || 'animation',
|
||||||
label: condition ? t('dialog.workflowActions.edgeConditionalLabel') : undefined,
|
label: condition ? t('dialog.workflowActions.edgeConditionalLabel') : undefined,
|
||||||
class: [edgeClass, condition ? 'workflow-conditional-edge' : ''].filter(Boolean).join(' ') || undefined,
|
class: [edgeClass, condition ? 'workflow-conditional-edge' : ''].filter(Boolean).join(' ') || undefined,
|
||||||
condition: condition || undefined,
|
condition: condition || undefined,
|
||||||
join_policy: joinPolicy || undefined,
|
|
||||||
branch_policy: branchPolicy || undefined,
|
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,129 +123,25 @@ const normalizeWorkflowEdges = () => {
|
|||||||
edges.value = (edges.value || []).map(edge => normalizeWorkflowEdge(edge))
|
edges.value = (edges.value || []).map(edge => normalizeWorkflowEdge(edge))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断扩展配置是否为空,避免旧 data 中的空值覆盖顶层字段
|
// 统一动作节点数据结构,高级运行配置由后端默认值和动作契约接管
|
||||||
const isEmptyConfigValue = (value: any) => {
|
|
||||||
if (value === undefined || value === null || value === '') return true
|
|
||||||
if (Array.isArray(value)) return value.length === 0
|
|
||||||
if (typeof value === 'object') return Object.keys(value).length === 0
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取动作节点扩展配置,兼容顶层字段与 data 字段
|
|
||||||
const getNodeConfigValue = (node: any, key: string) => {
|
|
||||||
const nodeValue = node?.[key]
|
|
||||||
if (!isEmptyConfigValue(nodeValue)) return nodeValue
|
|
||||||
const dataValue = node?.data?.[key]
|
|
||||||
return isEmptyConfigValue(dataValue) ? undefined : dataValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将输入声明统一为路径数组
|
|
||||||
const normalizeInputPaths = (value: any) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map(item => String(item).trim()).filter(Boolean)
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value
|
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map(item => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 JSON 形式的结构化配置
|
|
||||||
const parseStructuredConfig = (value: string, label: string) => {
|
|
||||||
const text = String(value || '').trim()
|
|
||||||
if (!text) return undefined
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text)
|
|
||||||
if (parsed && (Array.isArray(parsed) || typeof parsed === 'object')) {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
throw new Error(t('dialog.workflowActions.invalidJsonConfig', { label }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试把存量结构化配置标准化为对象或数组
|
|
||||||
const normalizeStructuredConfig = (value: any) => {
|
|
||||||
if (isEmptyConfigValue(value)) return undefined
|
|
||||||
if (Array.isArray(value) || typeof value === 'object') return value
|
|
||||||
if (typeof value !== 'string') return undefined
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return parsed && (Array.isArray(parsed) || typeof parsed === 'object') ? parsed : undefined
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将结构化配置格式化为面板中的 JSON 文本
|
|
||||||
const stringifyStructuredConfig = (value: any) => {
|
|
||||||
const normalizedValue = normalizeStructuredConfig(value)
|
|
||||||
return normalizedValue ? JSON.stringify(normalizedValue, null, 2) : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数值字段统一清洗,空值会在保存时被移除
|
|
||||||
const normalizeNumber = (value: any, minValue = 0, integer = false) => {
|
|
||||||
if (value === undefined || value === null || value === '') return undefined
|
|
||||||
const numberValue = Number(value)
|
|
||||||
if (!Number.isFinite(numberValue) || numberValue < minValue) return undefined
|
|
||||||
return integer ? Math.floor(numberValue) : numberValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取节点重试策略
|
|
||||||
const normalizeRetryConfig = (value: any) => {
|
|
||||||
const retryConfig = normalizeStructuredConfig(value)
|
|
||||||
return retryConfig && !Array.isArray(retryConfig) ? retryConfig : {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据面板表单构造重试策略
|
|
||||||
const buildRetryConfigFromForm = () => {
|
|
||||||
const retryConfig: Record<string, number> = {}
|
|
||||||
const maxAttempts = normalizeNumber(nodeForm.value.retry_max_attempts, 1, true)
|
|
||||||
const interval = normalizeNumber(nodeForm.value.retry_interval, 0)
|
|
||||||
const backoff = normalizeNumber(nodeForm.value.retry_backoff, 1)
|
|
||||||
if (maxAttempts !== undefined) retryConfig.max_attempts = maxAttempts
|
|
||||||
if (interval !== undefined) retryConfig.interval = interval
|
|
||||||
if (backoff !== undefined) retryConfig.backoff = backoff
|
|
||||||
return Object.keys(retryConfig).length ? retryConfig : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一动作节点数据结构,确保执行策略能被后端读取
|
|
||||||
const normalizeWorkflowNode = (node: any) => {
|
const normalizeWorkflowNode = (node: any) => {
|
||||||
const inputPaths = normalizeInputPaths(getNodeConfigValue(node, 'inputs'))
|
const hiddenConfigKeys = [
|
||||||
const outputs = normalizeStructuredConfig(getNodeConfigValue(node, 'outputs'))
|
'inputs',
|
||||||
const joinPolicy = String(getNodeConfigValue(node, 'join_policy') || '').trim()
|
'outputs',
|
||||||
const failPolicy = String(getNodeConfigValue(node, 'fail_policy') || '').trim()
|
'join_policy',
|
||||||
const branchPolicy = String(getNodeConfigValue(node, 'branch_policy') || '').trim()
|
'fail_policy',
|
||||||
const concurrencyKey = String(getNodeConfigValue(node, 'concurrency_key') || '').trim()
|
'branch_policy',
|
||||||
const timeout = normalizeNumber(getNodeConfigValue(node, 'timeout'), 1, true)
|
'concurrency_key',
|
||||||
const retryConfig = normalizeRetryConfig(getNodeConfigValue(node, 'retry'))
|
'timeout',
|
||||||
const retry = Object.keys(retryConfig).length ? retryConfig : undefined
|
'retry',
|
||||||
const data = {
|
'contract',
|
||||||
...(node?.data || {}),
|
'_contract',
|
||||||
inputs: inputPaths.length ? inputPaths : undefined,
|
]
|
||||||
outputs: outputs || undefined,
|
const data = omitConfigKeys(node?.data, hiddenConfigKeys)
|
||||||
join_policy: joinPolicy || undefined,
|
const nodePayload = omitConfigKeys(node, hiddenConfigKeys)
|
||||||
fail_policy: failPolicy || undefined,
|
|
||||||
branch_policy: branchPolicy || undefined,
|
|
||||||
concurrency_key: concurrencyKey || undefined,
|
|
||||||
timeout: timeout || undefined,
|
|
||||||
retry,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...nodePayload,
|
||||||
inputs: inputPaths.length ? inputPaths : undefined,
|
|
||||||
outputs: outputs || undefined,
|
|
||||||
join_policy: joinPolicy || undefined,
|
|
||||||
fail_policy: failPolicy || undefined,
|
|
||||||
branch_policy: branchPolicy || undefined,
|
|
||||||
concurrency_key: concurrencyKey || undefined,
|
|
||||||
timeout: timeout || undefined,
|
|
||||||
retry,
|
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,16 +157,39 @@ const getNodeName = (nodeId?: string) => {
|
|||||||
return (node as any)?.name || node?.data?.label || nodeId || ''
|
return (node as any)?.name || node?.data?.label || nodeId || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取流程边源节点可用于条件判断的输出字段
|
||||||
|
const getEdgeConditionFields = (edge: any) => {
|
||||||
|
const sourceNode = edge
|
||||||
|
? nodes.value.find(node => node.id === edge.source)
|
||||||
|
: null
|
||||||
|
const contract = sourceNode ? actionContractMap.value[sourceNode.type] || {} : {}
|
||||||
|
const fields = contract.condition_fields || contract.outputs || []
|
||||||
|
return Array.isArray(fields)
|
||||||
|
? fields.filter((field: any) => field?.name || field)
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断流程边是否存在可编辑条件
|
||||||
|
const canConfigureEdge = (edge: any) => {
|
||||||
|
const condition = String(getEdgeConfigValue(edge, 'condition') || '').trim()
|
||||||
|
return Boolean(condition || getEdgeConditionFields(edge).length)
|
||||||
|
}
|
||||||
|
|
||||||
// 选中流程边时打开设置面板
|
// 选中流程边时打开设置面板
|
||||||
function handleEdgeClick(params: any) {
|
async function handleEdgeClick(params: any) {
|
||||||
const edge = params?.edge
|
const edge = params?.edge
|
||||||
if (!edge) return
|
if (!edge) return
|
||||||
selectedNodeId.value = null
|
if (!actionDefinitions.value.length) {
|
||||||
|
await loadActionDefinitions()
|
||||||
|
}
|
||||||
|
if (!canConfigureEdge(edge)) {
|
||||||
|
closeEdgeSettings()
|
||||||
|
$toast.info(t('dialog.workflowActions.edgeNoConditionFields'))
|
||||||
|
return
|
||||||
|
}
|
||||||
selectedEdgeId.value = edge.id
|
selectedEdgeId.value = edge.id
|
||||||
edgeForm.value = {
|
edgeForm.value = {
|
||||||
condition: String(getEdgeConfigValue(edge, 'condition') || ''),
|
condition: String(getEdgeConfigValue(edge, 'condition') || ''),
|
||||||
join_policy: String(getEdgeConfigValue(edge, 'join_policy') || ''),
|
|
||||||
branch_policy: String(getEdgeConfigValue(edge, 'branch_policy') || ''),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +198,6 @@ function closeEdgeSettings() {
|
|||||||
selectedEdgeId.value = null
|
selectedEdgeId.value = null
|
||||||
edgeForm.value = {
|
edgeForm.value = {
|
||||||
condition: '',
|
condition: '',
|
||||||
join_policy: '',
|
|
||||||
branch_policy: '',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,14 +209,10 @@ function saveEdgeSettings() {
|
|||||||
return normalizeWorkflowEdge({
|
return normalizeWorkflowEdge({
|
||||||
...edge,
|
...edge,
|
||||||
condition: edgeForm.value.condition,
|
condition: edgeForm.value.condition,
|
||||||
join_policy: edgeForm.value.join_policy,
|
|
||||||
data: {
|
data: {
|
||||||
...(edge.data || {}),
|
...(edge.data || {}),
|
||||||
condition: edgeForm.value.condition,
|
condition: edgeForm.value.condition,
|
||||||
join_policy: edgeForm.value.join_policy,
|
|
||||||
branch_policy: edgeForm.value.branch_policy,
|
|
||||||
},
|
},
|
||||||
branch_policy: edgeForm.value.branch_policy,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$toast.success(t('dialog.workflowActions.edgeSaveSuccess'))
|
$toast.success(t('dialog.workflowActions.edgeSaveSuccess'))
|
||||||
@@ -350,106 +231,49 @@ const selectedEdge = computed(() => {
|
|||||||
return edges.value.find(edge => edge.id === selectedEdgeId.value) || null
|
return edges.value.find(edge => edge.id === selectedEdgeId.value) || null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当前选中的动作节点
|
// 当前边可用于条件判断的输出字段
|
||||||
const selectedNode = computed(() => {
|
const selectedEdgeConditionFields = computed(() => (
|
||||||
if (!selectedNodeId.value) return null
|
selectedEdge.value ? getEdgeConditionFields(selectedEdge.value) : []
|
||||||
return nodes.value.find(node => node.id === selectedNodeId.value) || null
|
))
|
||||||
})
|
|
||||||
|
|
||||||
// 将节点数据填入右侧执行配置面板
|
// 当前边的条件下拉选项,按源节点固定输出自动生成
|
||||||
function fillNodeForm(node: any) {
|
const edgeConditionOptions = computed(() => {
|
||||||
const retryConfig = normalizeRetryConfig(getNodeConfigValue(node, 'retry'))
|
const sourceNode = selectedEdge.value
|
||||||
nodeForm.value = {
|
? nodes.value.find(node => node.id === selectedEdge.value?.source)
|
||||||
inputs_text: normalizeInputPaths(getNodeConfigValue(node, 'inputs')).join('\n'),
|
: null
|
||||||
outputs_text: stringifyStructuredConfig(getNodeConfigValue(node, 'outputs')),
|
const options = [{ title: t('dialog.workflowActions.conditionAlways'), value: '' }]
|
||||||
join_policy: String(getNodeConfigValue(node, 'join_policy') || ''),
|
selectedEdgeConditionFields.value.forEach((field: any) => {
|
||||||
fail_policy: String(getNodeConfigValue(node, 'fail_policy') || ''),
|
const fieldName = field.name || field
|
||||||
branch_policy: String(getNodeConfigValue(node, 'branch_policy') || ''),
|
if (!fieldName) return
|
||||||
concurrency_key: String(getNodeConfigValue(node, 'concurrency_key') || ''),
|
const fieldLabel = field.label || fieldName
|
||||||
timeout: normalizeNumber(getNodeConfigValue(node, 'timeout'), 1, true) ?? null,
|
if (field.kind === 'list') {
|
||||||
retry_max_attempts: normalizeNumber(retryConfig.max_attempts, 1, true) ?? null,
|
options.push({
|
||||||
retry_interval: normalizeNumber(retryConfig.interval, 0) ?? null,
|
title: t('dialog.workflowActions.conditionHasOutput', { field: fieldLabel }),
|
||||||
retry_backoff: normalizeNumber(retryConfig.backoff, 1) ?? null,
|
value: `outputs.${sourceNode?.id}.${fieldName}.count > 0`,
|
||||||
}
|
})
|
||||||
}
|
options.push({
|
||||||
|
title: t('dialog.workflowActions.conditionNoOutput', { field: fieldLabel }),
|
||||||
// 选中动作节点时打开执行配置面板
|
value: `outputs.${sourceNode?.id}.${fieldName}.count == 0`,
|
||||||
function handleNodeClick(params: any) {
|
})
|
||||||
const node = params?.node
|
return
|
||||||
if (!node) return
|
}
|
||||||
if (node.name == '备注') return
|
options.push({
|
||||||
selectedEdgeId.value = null
|
title: t('dialog.workflowActions.conditionHasValue', { field: fieldLabel }),
|
||||||
selectedNodeId.value = node.id
|
value: `outputs.${sourceNode?.id}.${fieldName} != None`,
|
||||||
fillNodeForm(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭动作节点执行配置面板
|
|
||||||
function closeNodeSettings() {
|
|
||||||
selectedNodeId.value = null
|
|
||||||
nodeForm.value = {
|
|
||||||
inputs_text: '',
|
|
||||||
outputs_text: '',
|
|
||||||
join_policy: '',
|
|
||||||
fail_policy: '',
|
|
||||||
branch_policy: '',
|
|
||||||
concurrency_key: '',
|
|
||||||
timeout: null,
|
|
||||||
retry_max_attempts: null,
|
|
||||||
retry_interval: null,
|
|
||||||
retry_backoff: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据面板表单构造动作节点执行配置
|
|
||||||
function buildNodeConfigFromForm() {
|
|
||||||
return {
|
|
||||||
inputs: normalizeInputPaths(nodeForm.value.inputs_text),
|
|
||||||
outputs: parseStructuredConfig(nodeForm.value.outputs_text, t('dialog.workflowActions.nodeOutputsLabel')),
|
|
||||||
join_policy: nodeForm.value.join_policy,
|
|
||||||
fail_policy: nodeForm.value.fail_policy,
|
|
||||||
branch_policy: nodeForm.value.branch_policy,
|
|
||||||
concurrency_key: nodeForm.value.concurrency_key,
|
|
||||||
timeout: normalizeNumber(nodeForm.value.timeout, 1, true),
|
|
||||||
retry: buildRetryConfigFromForm(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存动作节点执行配置
|
|
||||||
function saveNodeSettings() {
|
|
||||||
if (!selectedNodeId.value) return
|
|
||||||
let nodeConfig: any
|
|
||||||
try {
|
|
||||||
nodeConfig = buildNodeConfigFromForm()
|
|
||||||
} catch (error: any) {
|
|
||||||
$toast.error(error.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nodes.value = nodes.value.map(node => {
|
|
||||||
if (node.id !== selectedNodeId.value) return node
|
|
||||||
return normalizeWorkflowNode({
|
|
||||||
...node,
|
|
||||||
inputs: nodeConfig.inputs,
|
|
||||||
outputs: nodeConfig.outputs,
|
|
||||||
join_policy: nodeConfig.join_policy,
|
|
||||||
fail_policy: nodeConfig.fail_policy,
|
|
||||||
branch_policy: nodeConfig.branch_policy,
|
|
||||||
concurrency_key: nodeConfig.concurrency_key,
|
|
||||||
timeout: nodeConfig.timeout,
|
|
||||||
retry: nodeConfig.retry,
|
|
||||||
data: {
|
|
||||||
...(node.data || {}),
|
|
||||||
inputs: nodeConfig.inputs,
|
|
||||||
outputs: nodeConfig.outputs,
|
|
||||||
join_policy: nodeConfig.join_policy,
|
|
||||||
fail_policy: nodeConfig.fail_policy,
|
|
||||||
branch_policy: nodeConfig.branch_policy,
|
|
||||||
concurrency_key: nodeConfig.concurrency_key,
|
|
||||||
timeout: nodeConfig.timeout,
|
|
||||||
retry: nodeConfig.retry,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$toast.success(t('dialog.workflowActions.nodeSaveSuccess'))
|
if (edgeForm.value.condition && !options.some(item => item.value === edgeForm.value.condition)) {
|
||||||
|
options.push({
|
||||||
|
title: t('dialog.workflowActions.conditionCustom'),
|
||||||
|
value: edgeForm.value.condition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中动作节点时关闭可能打开的边条件面板,不再提供节点运行设置
|
||||||
|
function handleNodeClick() {
|
||||||
|
closeEdgeSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义节点类型
|
// 自定义节点类型
|
||||||
@@ -478,6 +302,17 @@ for (const path in components) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载动作契约,供边条件构造器使用
|
||||||
|
async function loadActionDefinitions() {
|
||||||
|
try {
|
||||||
|
const actionList = await api.get('workflow/actions')
|
||||||
|
actionDefinitions.value = Array.isArray(actionList) ? actionList : []
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
actionDefinitions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 定义输入参数
|
// 定义输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
workflow: Object as PropType<Workflow>,
|
workflow: Object as PropType<Workflow>,
|
||||||
@@ -590,6 +425,7 @@ function shareWorkflow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadActionDefinitions()
|
||||||
if (props.workflow) {
|
if (props.workflow) {
|
||||||
nodes.value = props.workflow.actions ?? []
|
nodes.value = props.workflow.actions ?? []
|
||||||
edges.value = props.workflow.flows ?? []
|
edges.value = props.workflow.flows ?? []
|
||||||
@@ -611,8 +447,8 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
nodes,
|
nodes,
|
||||||
() => {
|
() => {
|
||||||
if (selectedNodeId.value && !selectedNode.value) {
|
if (selectedEdge.value && !canConfigureEdge(selectedEdge.value)) {
|
||||||
closeNodeSettings()
|
closeEdgeSettings()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
@@ -691,33 +527,11 @@ const isMacOS = computed(() => {
|
|||||||
<span>{{ getNodeName(selectedEdge.target) }}</span>
|
<span>{{ getNodeName(selectedEdge.target) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VTextarea
|
<VSelect
|
||||||
v-model="edgeForm.condition"
|
v-model="edgeForm.condition"
|
||||||
|
:items="edgeConditionOptions"
|
||||||
:label="t('dialog.workflowActions.edgeConditionLabel')"
|
:label="t('dialog.workflowActions.edgeConditionLabel')"
|
||||||
:placeholder="t('dialog.workflowActions.edgeConditionPlaceholder')"
|
|
||||||
rows="3"
|
|
||||||
auto-grow
|
|
||||||
clearable
|
clearable
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VSelect
|
|
||||||
v-model="edgeForm.join_policy"
|
|
||||||
:items="joinPolicyOptions"
|
|
||||||
:label="t('dialog.workflowActions.edgeJoinPolicyLabel')"
|
|
||||||
item-title="title"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VSelect
|
|
||||||
v-model="edgeForm.branch_policy"
|
|
||||||
:items="branchPolicyOptions"
|
|
||||||
:label="t('dialog.workflowActions.edgeBranchPolicyLabel')"
|
|
||||||
item-title="title"
|
item-title="title"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -739,121 +553,6 @@ const isMacOS = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedNode" class="workflow-edge-panel workflow-node-panel">
|
|
||||||
<div class="edge-panel-header">
|
|
||||||
<div class="edge-panel-title">
|
|
||||||
<VIcon icon="mdi-tune-variant" size="20" />
|
|
||||||
<span>{{ t('dialog.workflowActions.nodeSettingsTitle') }}</span>
|
|
||||||
</div>
|
|
||||||
<VBtn icon variant="text" size="small" @click="closeNodeSettings">
|
|
||||||
<VIcon icon="mdi-close" />
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edge-route">
|
|
||||||
<VIcon icon="mdi-checkbox-blank-circle-outline" size="16" />
|
|
||||||
<span>{{ getNodeName(selectedNode.id) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VSelect
|
|
||||||
v-model="nodeForm.join_policy"
|
|
||||||
:items="joinPolicyOptions"
|
|
||||||
:label="t('dialog.workflowActions.nodeJoinPolicyLabel')"
|
|
||||||
item-title="title"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VSelect
|
|
||||||
v-model="nodeForm.fail_policy"
|
|
||||||
:items="failPolicyOptions"
|
|
||||||
:label="t('dialog.workflowActions.nodeFailPolicyLabel')"
|
|
||||||
item-title="title"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VSelect
|
|
||||||
v-model="nodeForm.branch_policy"
|
|
||||||
:items="branchPolicyOptions"
|
|
||||||
:label="t('dialog.workflowActions.nodeBranchPolicyLabel')"
|
|
||||||
item-title="title"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VTextField
|
|
||||||
v-model="nodeForm.concurrency_key"
|
|
||||||
:label="t('dialog.workflowActions.nodeConcurrencyKeyLabel')"
|
|
||||||
:placeholder="t('dialog.workflowActions.nodeConcurrencyKeyPlaceholder')"
|
|
||||||
clearable
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="workflow-number-grid">
|
|
||||||
<VTextField
|
|
||||||
v-model.number="nodeForm.timeout"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
:label="t('dialog.workflowActions.nodeTimeoutLabel')"
|
|
||||||
clearable
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
<VTextField
|
|
||||||
v-model.number="nodeForm.retry_max_attempts"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
:label="t('dialog.workflowActions.nodeRetryAttemptsLabel')"
|
|
||||||
clearable
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
<VTextField
|
|
||||||
v-model.number="nodeForm.retry_interval"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.1"
|
|
||||||
:label="t('dialog.workflowActions.nodeRetryIntervalLabel')"
|
|
||||||
clearable
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
<VTextField
|
|
||||||
v-model.number="nodeForm.retry_backoff"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="0.1"
|
|
||||||
:label="t('dialog.workflowActions.nodeRetryBackoffLabel')"
|
|
||||||
clearable
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edge-panel-actions">
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn variant="text" @click="closeNodeSettings">
|
|
||||||
{{ t('dialog.workflowActions.edgeCancel') }}
|
|
||||||
</VBtn>
|
|
||||||
<VBtn color="primary" @click="saveNodeSettings">
|
|
||||||
{{ t('dialog.workflowActions.edgeSave') }}
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WorkflowSidebar @component-click="handleComponentClick" />
|
<WorkflowSidebar @component-click="handleComponentClick" />
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -914,10 +613,6 @@ const isMacOS = computed(() => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-node-panel {
|
|
||||||
inline-size: min(420px, calc(100vw - 32px));
|
|
||||||
}
|
|
||||||
|
|
||||||
.edge-panel-header {
|
.edge-panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -958,12 +653,6 @@ const isMacOS = computed(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-number-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-flow__minimap {
|
.vue-flow__minimap {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
@@ -1042,8 +731,5 @@ const isMacOS = computed(() => {
|
|||||||
max-block-size: min(72vh, calc(100% - 112px));
|
max-block-size: min(72vh, calc(100% - 112px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-number-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ function slideNext(next: boolean) {
|
|||||||
if (!element) return
|
if (!element) return
|
||||||
|
|
||||||
const visibleCount = Math.max(1, Math.trunc(element.clientWidth / itemStep.value))
|
const visibleCount = Math.max(1, Math.trunc(element.clientWidth / itemStep.value))
|
||||||
const currentIndex = element.scrollLeft === 0 ? 0 : Math.trunc((element.scrollLeft + itemStep.value / 2) / itemStep.value)
|
const currentIndex =
|
||||||
|
element.scrollLeft === 0 ? 0 : Math.trunc((element.scrollLeft + itemStep.value / 2) / itemStep.value)
|
||||||
let targetLeft = 0
|
let targetLeft = 0
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
@@ -345,15 +346,15 @@ watch(
|
|||||||
|
|
||||||
.slider-content-container {
|
.slider-content-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-content {
|
.slider-content {
|
||||||
overflow: scroll hidden !important;
|
margin-block-end: -8px;
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
|
overflow: scroll visible;
|
||||||
overscroll-behavior-x: contain !important;
|
overscroll-behavior-x: contain !important;
|
||||||
padding-block: 8px;
|
padding-block: 0 8px;
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
@@ -380,6 +381,11 @@ watch(
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-slide-item,
|
||||||
|
.loading-track > * {
|
||||||
|
padding-block-end: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-button {
|
.nav-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
@@ -399,8 +405,12 @@ watch(
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 10%);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 10%);
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.3s ease,
|
transition:
|
||||||
box-shadow 0.3s ease, border-color 0.3s ease;
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||||
|
background-color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease,
|
||||||
|
border-color 0.3s ease;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
block-size: 22px;
|
block-size: 22px;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed, onMounted, onScopeDispose, readonly, ref } from 'vue'
|
|||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||||
import { saveLocalTheme } from '@/@core/utils/theme'
|
import { saveLocalTheme } from '@/@core/utils/theme'
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
import { themeManager } from '@/utils/themeManager'
|
import { themeManager } from '@/utils/themeManager'
|
||||||
|
|
||||||
export const THEME_CUSTOMIZER_STORAGE_KEY = 'moviepilot-theme-customizer'
|
export const THEME_CUSTOMIZER_STORAGE_KEY = 'moviepilot-theme-customizer'
|
||||||
@@ -23,6 +24,7 @@ export const themeCustomizerPrimaryColors = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type ThemeCustomizerLayout = 'collapsed' | 'horizontal' | 'vertical'
|
export type ThemeCustomizerLayout = 'collapsed' | 'horizontal' | 'vertical'
|
||||||
|
export type ThemeCustomizerShadow = 'none' | 'low' | 'medium' | 'high'
|
||||||
export type ThemeCustomizerSkin = 'bordered' | 'default'
|
export type ThemeCustomizerSkin = 'bordered' | 'default'
|
||||||
export type ThemeCustomizerTheme = 'auto' | 'dark' | 'light' | 'purple' | 'transparent'
|
export type ThemeCustomizerTheme = 'auto' | 'dark' | 'light' | 'purple' | 'transparent'
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ export interface ThemeCustomizerSettings {
|
|||||||
layout: ThemeCustomizerLayout
|
layout: ThemeCustomizerLayout
|
||||||
primaryColor: string
|
primaryColor: string
|
||||||
semiDarkMenu: boolean
|
semiDarkMenu: boolean
|
||||||
|
shadow: ThemeCustomizerShadow
|
||||||
skin: ThemeCustomizerSkin
|
skin: ThemeCustomizerSkin
|
||||||
theme: ThemeCustomizerTheme
|
theme: ThemeCustomizerTheme
|
||||||
}
|
}
|
||||||
@@ -38,6 +41,7 @@ type VuetifyThemeApi = ReturnType<typeof useTheme>
|
|||||||
|
|
||||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||||
const validLayouts: ThemeCustomizerLayout[] = ['vertical', 'collapsed', 'horizontal']
|
const validLayouts: ThemeCustomizerLayout[] = ['vertical', 'collapsed', 'horizontal']
|
||||||
|
const validShadows: ThemeCustomizerShadow[] = ['none', 'low', 'medium', 'high']
|
||||||
const validSkins: ThemeCustomizerSkin[] = ['default', 'bordered']
|
const validSkins: ThemeCustomizerSkin[] = ['default', 'bordered']
|
||||||
const validThemes: ThemeCustomizerTheme[] = ['auto', 'light', 'dark', 'purple', 'transparent']
|
const validThemes: ThemeCustomizerTheme[] = ['auto', 'light', 'dark', 'purple', 'transparent']
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ function getDefaultThemeCustomizerSettings(): ThemeCustomizerSettings {
|
|||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
primaryColor: defaultPrimaryColor,
|
primaryColor: defaultPrimaryColor,
|
||||||
semiDarkMenu: false,
|
semiDarkMenu: false,
|
||||||
|
shadow: 'none',
|
||||||
skin: 'default',
|
skin: 'default',
|
||||||
theme: readStoredThemePreference(),
|
theme: readStoredThemePreference(),
|
||||||
}
|
}
|
||||||
@@ -78,7 +83,12 @@ function normalizeThemeCustomizerSettings(settings: Partial<ThemeCustomizerSetti
|
|||||||
: fallback.layout,
|
: fallback.layout,
|
||||||
primaryColor: isHexColor(settings.primaryColor) ? settings.primaryColor.toUpperCase() : fallback.primaryColor,
|
primaryColor: isHexColor(settings.primaryColor) ? settings.primaryColor.toUpperCase() : fallback.primaryColor,
|
||||||
semiDarkMenu: typeof settings.semiDarkMenu === 'boolean' ? settings.semiDarkMenu : fallback.semiDarkMenu,
|
semiDarkMenu: typeof settings.semiDarkMenu === 'boolean' ? settings.semiDarkMenu : fallback.semiDarkMenu,
|
||||||
skin: validSkins.includes(settings.skin as ThemeCustomizerSkin) ? (settings.skin as ThemeCustomizerSkin) : fallback.skin,
|
shadow: validShadows.includes(settings.shadow as ThemeCustomizerShadow)
|
||||||
|
? (settings.shadow as ThemeCustomizerShadow)
|
||||||
|
: fallback.shadow,
|
||||||
|
skin: validSkins.includes(settings.skin as ThemeCustomizerSkin)
|
||||||
|
? (settings.skin as ThemeCustomizerSkin)
|
||||||
|
: fallback.skin,
|
||||||
theme: validThemes.includes(settings.theme as ThemeCustomizerTheme)
|
theme: validThemes.includes(settings.theme as ThemeCustomizerTheme)
|
||||||
? (settings.theme as ThemeCustomizerTheme)
|
? (settings.theme as ThemeCustomizerTheme)
|
||||||
: fallback.theme,
|
: fallback.theme,
|
||||||
@@ -151,15 +161,19 @@ export function applyPrimaryColorToVuetify(color: string, themeApi: VuetifyTheme
|
|||||||
localStorage.setItem('materio-initial-loader-color', color)
|
localStorage.setItem('materio-initial-loader-color', color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 布局、皮肤和局部菜单风格只依赖根节点属性,CSS 可以在不刷新页面的情况下即时响应。 */
|
/** 布局、阴影、皮肤和局部菜单风格只依赖根节点属性,CSS 可以在不刷新页面的情况下即时响应。 */
|
||||||
export function applyThemeCustomizerRootSettings(settings: Pick<ThemeCustomizerSettings, 'layout' | 'semiDarkMenu' | 'skin'>) {
|
export function applyThemeCustomizerRootSettings(
|
||||||
|
settings: Pick<ThemeCustomizerSettings, 'layout' | 'semiDarkMenu' | 'shadow' | 'skin'>,
|
||||||
|
) {
|
||||||
if (!isBrowser()) return
|
if (!isBrowser()) return
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme-layout', settings.layout)
|
document.documentElement.setAttribute('data-theme-layout', settings.layout)
|
||||||
document.documentElement.setAttribute('data-theme-semi-dark-menu', String(settings.semiDarkMenu))
|
document.documentElement.setAttribute('data-theme-semi-dark-menu', String(settings.semiDarkMenu))
|
||||||
|
document.documentElement.setAttribute('data-theme-shadow', settings.shadow)
|
||||||
document.documentElement.setAttribute('data-theme-skin', settings.skin)
|
document.documentElement.setAttribute('data-theme-skin', settings.skin)
|
||||||
document.body.setAttribute('data-theme-layout', settings.layout)
|
document.body.setAttribute('data-theme-layout', settings.layout)
|
||||||
document.body.setAttribute('data-theme-semi-dark-menu', String(settings.semiDarkMenu))
|
document.body.setAttribute('data-theme-semi-dark-menu', String(settings.semiDarkMenu))
|
||||||
|
document.body.setAttribute('data-theme-shadow', settings.shadow)
|
||||||
document.body.setAttribute('data-theme-skin', settings.skin)
|
document.body.setAttribute('data-theme-skin', settings.skin)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +224,7 @@ export function persistPartialThemeCustomizerSettings(patch: Partial<ThemeCustom
|
|||||||
|
|
||||||
settingsState.value = nextSettings
|
settingsState.value = nextSettings
|
||||||
persistThemeCustomizerSettings(nextSettings)
|
persistThemeCustomizerSettings(nextSettings)
|
||||||
|
applyPrimaryColorToVuetify(nextSettings.primaryColor, vuetify.theme)
|
||||||
applyThemeCustomizerRootSettings(nextSettings)
|
applyThemeCustomizerRootSettings(nextSettings)
|
||||||
dispatchThemeCustomizerChange(nextSettings)
|
dispatchThemeCustomizerChange(nextSettings)
|
||||||
|
|
||||||
@@ -221,6 +236,7 @@ export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettin
|
|||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
primaryColor: defaultPrimaryColor,
|
primaryColor: defaultPrimaryColor,
|
||||||
semiDarkMenu: false,
|
semiDarkMenu: false,
|
||||||
|
shadow: 'none',
|
||||||
skin: 'default',
|
skin: 'default',
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
})
|
})
|
||||||
@@ -229,6 +245,7 @@ export function isDefaultThemeCustomizerSettings(settings: ThemeCustomizerSettin
|
|||||||
settings.layout === defaults.layout &&
|
settings.layout === defaults.layout &&
|
||||||
settings.primaryColor === defaults.primaryColor &&
|
settings.primaryColor === defaults.primaryColor &&
|
||||||
settings.semiDarkMenu === defaults.semiDarkMenu &&
|
settings.semiDarkMenu === defaults.semiDarkMenu &&
|
||||||
|
settings.shadow === defaults.shadow &&
|
||||||
settings.skin === defaults.skin &&
|
settings.skin === defaults.skin &&
|
||||||
settings.theme === defaults.theme
|
settings.theme === defaults.theme
|
||||||
)
|
)
|
||||||
@@ -251,7 +268,10 @@ export function useThemeCustomizer() {
|
|||||||
applyPrimaryColorToVuetify(nextSettings.primaryColor, themeApi)
|
applyPrimaryColorToVuetify(nextSettings.primaryColor, themeApi)
|
||||||
applyThemeCustomizerRootSettings(nextSettings)
|
applyThemeCustomizerRootSettings(nextSettings)
|
||||||
|
|
||||||
if (previousTheme !== nextSettings.theme || themeApi.global.name.value !== getResolvedThemeName(nextSettings.theme)) {
|
if (
|
||||||
|
previousTheme !== nextSettings.theme ||
|
||||||
|
themeApi.global.name.value !== getResolvedThemeName(nextSettings.theme)
|
||||||
|
) {
|
||||||
await applyThemePreference(nextSettings.theme, themeApi)
|
await applyThemePreference(nextSettings.theme, themeApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +286,10 @@ export function useThemeCustomizer() {
|
|||||||
return updateSettings({ theme })
|
return updateSettings({ theme })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setShadow(shadow: ThemeCustomizerShadow) {
|
||||||
|
return updateSettings({ shadow })
|
||||||
|
}
|
||||||
|
|
||||||
function setSkin(skin: ThemeCustomizerSkin) {
|
function setSkin(skin: ThemeCustomizerSkin) {
|
||||||
return updateSettings({ skin })
|
return updateSettings({ skin })
|
||||||
}
|
}
|
||||||
@@ -283,6 +307,7 @@ export function useThemeCustomizer() {
|
|||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
primaryColor: defaultPrimaryColor,
|
primaryColor: defaultPrimaryColor,
|
||||||
semiDarkMenu: false,
|
semiDarkMenu: false,
|
||||||
|
shadow: 'none',
|
||||||
skin: 'default',
|
skin: 'default',
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
})
|
})
|
||||||
@@ -315,6 +340,7 @@ export function useThemeCustomizer() {
|
|||||||
setLayout,
|
setLayout,
|
||||||
setPrimaryColor,
|
setPrimaryColor,
|
||||||
setSemiDarkMenu,
|
setSemiDarkMenu,
|
||||||
|
setShadow,
|
||||||
setSkin,
|
setSkin,
|
||||||
setTheme,
|
setTheme,
|
||||||
settings: readonly(settings),
|
settings: readonly(settings),
|
||||||
|
|||||||
+11
-30
@@ -168,6 +168,11 @@ export default {
|
|||||||
skins: 'Framework',
|
skins: 'Framework',
|
||||||
skinDefault: 'Default',
|
skinDefault: 'Default',
|
||||||
skinBordered: 'Bordered',
|
skinBordered: 'Bordered',
|
||||||
|
shadow: 'Shadows',
|
||||||
|
shadowNone: 'Flat',
|
||||||
|
shadowLow: 'Soft',
|
||||||
|
shadowMedium: 'Balanced',
|
||||||
|
shadowHigh: 'Bold',
|
||||||
semiDarkMenu: 'Semi Dark Menu',
|
semiDarkMenu: 'Semi Dark Menu',
|
||||||
layout: 'Layout',
|
layout: 'Layout',
|
||||||
layoutVertical: 'Vertical',
|
layoutVertical: 'Vertical',
|
||||||
@@ -2468,40 +2473,16 @@ export default {
|
|||||||
codeCopied: 'Task workflow code copied to clipboard!',
|
codeCopied: 'Task workflow code copied to clipboard!',
|
||||||
edgeSettingsTitle: 'Flow Condition',
|
edgeSettingsTitle: 'Flow Condition',
|
||||||
edgeConditionLabel: 'Condition',
|
edgeConditionLabel: 'Condition',
|
||||||
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
conditionAlways: 'Always continue',
|
||||||
edgeJoinPolicyLabel: 'Target Join Policy',
|
conditionHasOutput: 'Has {field} output',
|
||||||
edgeBranchPolicyLabel: 'Source Branch Policy',
|
conditionNoOutput: 'No {field} output',
|
||||||
joinPolicyDefault: 'Default',
|
conditionHasValue: '{field} has value',
|
||||||
joinPolicyAllSuccess: 'All Success',
|
conditionCustom: 'Custom condition (preserved)',
|
||||||
joinPolicyAnySuccess: 'Any Success',
|
edgeNoConditionFields: 'The previous node has no output available for conditions',
|
||||||
joinPolicyAllDone: 'All Done',
|
|
||||||
joinPolicyFailFast: 'Fail Fast',
|
|
||||||
branchPolicyDefault: 'Default',
|
|
||||||
branchPolicyParallel: 'Parallel',
|
|
||||||
branchPolicyExclusive: 'Exclusive First',
|
|
||||||
failPolicyDefault: 'Default',
|
|
||||||
failPolicyStop: 'Stop on Failure',
|
|
||||||
failPolicyContinue: 'Continue on Failure',
|
|
||||||
failPolicyIgnore: 'Ignore Failure',
|
|
||||||
edgeConditionalLabel: 'Condition',
|
edgeConditionalLabel: 'Condition',
|
||||||
edgeSave: 'Save',
|
edgeSave: 'Save',
|
||||||
edgeCancel: 'Cancel',
|
edgeCancel: 'Cancel',
|
||||||
edgeSaveSuccess: 'Flow condition saved',
|
edgeSaveSuccess: 'Flow condition saved',
|
||||||
nodeSettingsTitle: 'Action Execution Policy',
|
|
||||||
nodeInputsLabel: 'Input Declarations',
|
|
||||||
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
|
||||||
nodeOutputsLabel: 'Output Declarations',
|
|
||||||
nodeJoinPolicyLabel: 'Node Join Policy',
|
|
||||||
nodeFailPolicyLabel: 'Failure Policy',
|
|
||||||
nodeBranchPolicyLabel: 'Branch Policy',
|
|
||||||
nodeConcurrencyKeyLabel: 'Concurrency Key',
|
|
||||||
nodeConcurrencyKeyPlaceholder: 'downloader',
|
|
||||||
nodeTimeoutLabel: 'Timeout Seconds',
|
|
||||||
nodeRetryAttemptsLabel: 'Retry Attempts',
|
|
||||||
nodeRetryIntervalLabel: 'Retry Interval',
|
|
||||||
nodeRetryBackoffLabel: 'Backoff Factor',
|
|
||||||
nodeSaveSuccess: 'Action execution policy saved',
|
|
||||||
invalidJsonConfig: '{label} is not a valid JSON object or array',
|
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: 'Update Site Cookie & UA',
|
title: 'Update Site Cookie & UA',
|
||||||
|
|||||||
+11
-30
@@ -168,6 +168,11 @@ export default {
|
|||||||
skins: '框架',
|
skins: '框架',
|
||||||
skinDefault: '默认',
|
skinDefault: '默认',
|
||||||
skinBordered: '边框',
|
skinBordered: '边框',
|
||||||
|
shadow: '阴影',
|
||||||
|
shadowNone: '无阴影',
|
||||||
|
shadowLow: '柔和',
|
||||||
|
shadowMedium: '标准',
|
||||||
|
shadowHigh: '强烈',
|
||||||
semiDarkMenu: '半暗菜单',
|
semiDarkMenu: '半暗菜单',
|
||||||
layout: '布局',
|
layout: '布局',
|
||||||
layoutVertical: '垂直',
|
layoutVertical: '垂直',
|
||||||
@@ -2420,40 +2425,16 @@ export default {
|
|||||||
codeCopied: '任务流程代码已复制到剪贴板!',
|
codeCopied: '任务流程代码已复制到剪贴板!',
|
||||||
edgeSettingsTitle: '流程条件',
|
edgeSettingsTitle: '流程条件',
|
||||||
edgeConditionLabel: '流转条件',
|
edgeConditionLabel: '流转条件',
|
||||||
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
conditionAlways: '无条件流转',
|
||||||
edgeJoinPolicyLabel: '目标汇合策略',
|
conditionHasOutput: '有{field}输出',
|
||||||
edgeBranchPolicyLabel: '源分支策略',
|
conditionNoOutput: '没有{field}输出',
|
||||||
joinPolicyDefault: '默认',
|
conditionHasValue: '{field}有值',
|
||||||
joinPolicyAllSuccess: '全部成功',
|
conditionCustom: '自定义条件(保留现有)',
|
||||||
joinPolicyAnySuccess: '任一成功',
|
edgeNoConditionFields: '上一节点没有可用于条件判断的输出',
|
||||||
joinPolicyAllDone: '全部完成',
|
|
||||||
joinPolicyFailFast: '失败即停',
|
|
||||||
branchPolicyDefault: '默认',
|
|
||||||
branchPolicyParallel: '并行',
|
|
||||||
branchPolicyExclusive: '互斥首选',
|
|
||||||
failPolicyDefault: '默认',
|
|
||||||
failPolicyStop: '失败停止',
|
|
||||||
failPolicyContinue: '失败继续',
|
|
||||||
failPolicyIgnore: '忽略失败',
|
|
||||||
edgeConditionalLabel: '条件',
|
edgeConditionalLabel: '条件',
|
||||||
edgeSave: '保存',
|
edgeSave: '保存',
|
||||||
edgeCancel: '取消',
|
edgeCancel: '取消',
|
||||||
edgeSaveSuccess: '流程条件已保存',
|
edgeSaveSuccess: '流程条件已保存',
|
||||||
nodeSettingsTitle: '动作执行策略',
|
|
||||||
nodeInputsLabel: '输入声明',
|
|
||||||
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
|
||||||
nodeOutputsLabel: '输出声明',
|
|
||||||
nodeJoinPolicyLabel: '节点汇合策略',
|
|
||||||
nodeFailPolicyLabel: '失败策略',
|
|
||||||
nodeBranchPolicyLabel: '分支策略',
|
|
||||||
nodeConcurrencyKeyLabel: '并发互斥键',
|
|
||||||
nodeConcurrencyKeyPlaceholder: 'downloader',
|
|
||||||
nodeTimeoutLabel: '超时秒数',
|
|
||||||
nodeRetryAttemptsLabel: '重试次数',
|
|
||||||
nodeRetryIntervalLabel: '重试间隔',
|
|
||||||
nodeRetryBackoffLabel: '退避倍数',
|
|
||||||
nodeSaveSuccess: '动作执行策略已保存',
|
|
||||||
invalidJsonConfig: '{label} 不是有效的 JSON 对象或数组',
|
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: '更新站点Cookie & UA',
|
title: '更新站点Cookie & UA',
|
||||||
|
|||||||
+11
-30
@@ -168,6 +168,11 @@ export default {
|
|||||||
skins: '框架',
|
skins: '框架',
|
||||||
skinDefault: '默認',
|
skinDefault: '默認',
|
||||||
skinBordered: '邊框',
|
skinBordered: '邊框',
|
||||||
|
shadow: '陰影',
|
||||||
|
shadowNone: '無陰影',
|
||||||
|
shadowLow: '柔和',
|
||||||
|
shadowMedium: '標準',
|
||||||
|
shadowHigh: '強烈',
|
||||||
semiDarkMenu: '半暗菜單',
|
semiDarkMenu: '半暗菜單',
|
||||||
layout: '佈局',
|
layout: '佈局',
|
||||||
layoutVertical: '垂直',
|
layoutVertical: '垂直',
|
||||||
@@ -2421,40 +2426,16 @@ export default {
|
|||||||
codeCopied: '任務流程代碼已複製到剪貼簿!',
|
codeCopied: '任務流程代碼已複製到剪貼簿!',
|
||||||
edgeSettingsTitle: '流程條件',
|
edgeSettingsTitle: '流程條件',
|
||||||
edgeConditionLabel: '流轉條件',
|
edgeConditionLabel: '流轉條件',
|
||||||
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
conditionAlways: '無條件流轉',
|
||||||
edgeJoinPolicyLabel: '目標匯合策略',
|
conditionHasOutput: '有{field}輸出',
|
||||||
edgeBranchPolicyLabel: '來源分支策略',
|
conditionNoOutput: '沒有{field}輸出',
|
||||||
joinPolicyDefault: '預設',
|
conditionHasValue: '{field}有值',
|
||||||
joinPolicyAllSuccess: '全部成功',
|
conditionCustom: '自訂條件(保留現有)',
|
||||||
joinPolicyAnySuccess: '任一成功',
|
edgeNoConditionFields: '上一節點沒有可用於條件判斷的輸出',
|
||||||
joinPolicyAllDone: '全部完成',
|
|
||||||
joinPolicyFailFast: '失敗即停',
|
|
||||||
branchPolicyDefault: '預設',
|
|
||||||
branchPolicyParallel: '並行',
|
|
||||||
branchPolicyExclusive: '互斥首選',
|
|
||||||
failPolicyDefault: '預設',
|
|
||||||
failPolicyStop: '失敗停止',
|
|
||||||
failPolicyContinue: '失敗繼續',
|
|
||||||
failPolicyIgnore: '忽略失敗',
|
|
||||||
edgeConditionalLabel: '條件',
|
edgeConditionalLabel: '條件',
|
||||||
edgeSave: '儲存',
|
edgeSave: '儲存',
|
||||||
edgeCancel: '取消',
|
edgeCancel: '取消',
|
||||||
edgeSaveSuccess: '流程條件已儲存',
|
edgeSaveSuccess: '流程條件已儲存',
|
||||||
nodeSettingsTitle: '動作執行策略',
|
|
||||||
nodeInputsLabel: '輸入宣告',
|
|
||||||
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
|
||||||
nodeOutputsLabel: '輸出宣告',
|
|
||||||
nodeJoinPolicyLabel: '節點匯合策略',
|
|
||||||
nodeFailPolicyLabel: '失敗策略',
|
|
||||||
nodeBranchPolicyLabel: '分支策略',
|
|
||||||
nodeConcurrencyKeyLabel: '並發互斥鍵',
|
|
||||||
nodeConcurrencyKeyPlaceholder: 'downloader',
|
|
||||||
nodeTimeoutLabel: '超時秒數',
|
|
||||||
nodeRetryAttemptsLabel: '重試次數',
|
|
||||||
nodeRetryIntervalLabel: '重試間隔',
|
|
||||||
nodeRetryBackoffLabel: '退避倍數',
|
|
||||||
nodeSaveSuccess: '動作執行策略已儲存',
|
|
||||||
invalidJsonConfig: '{label} 不是有效的 JSON 物件或陣列',
|
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: '更新站點Cookie & UA',
|
title: '更新站點Cookie & UA',
|
||||||
|
|||||||
+108
-27
@@ -33,8 +33,62 @@ html.v-overlay-scroll-blocked body {
|
|||||||
|
|
||||||
// 全局卡片阴影 token:卡片统一不使用投影,避免透明主题和密集布局下出现脏边。
|
// 全局卡片阴影 token:卡片统一不使用投影,避免透明主题和密集布局下出现脏边。
|
||||||
html {
|
html {
|
||||||
|
--app-shadow-rgb: 15, 23, 42;
|
||||||
--app-card-rest-shadow: none;
|
--app-card-rest-shadow: none;
|
||||||
--app-card-hover-shadow: none;
|
--app-card-hover-shadow: none;
|
||||||
|
--app-fab-shadow: none;
|
||||||
|
--app-fab-shadow-strong: none;
|
||||||
|
--app-fab-shadow-hover: none;
|
||||||
|
--app-fab-shadow-strong-hover: none;
|
||||||
|
--app-fab-shadow-active: none;
|
||||||
|
--app-overlay-shadow: none;
|
||||||
|
--app-surface-shadow: none;
|
||||||
|
--app-surface-hover-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'],
|
||||||
|
html[data-theme='purple'],
|
||||||
|
html[data-theme='transparent'] {
|
||||||
|
--app-shadow-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme-shadow='low'] {
|
||||||
|
--app-card-rest-shadow: 0 10px 24px rgba(var(--app-shadow-rgb), 0.06), 0 2px 8px rgba(var(--app-shadow-rgb), 0.04);
|
||||||
|
--app-card-hover-shadow: 0 14px 30px rgba(var(--app-shadow-rgb), 0.08), 0 4px 12px rgba(var(--app-shadow-rgb), 0.05);
|
||||||
|
--app-fab-shadow: 0 16px 34px rgba(var(--app-shadow-rgb), 0.16), 0 6px 16px rgba(var(--app-shadow-rgb), 0.1);
|
||||||
|
--app-fab-shadow-strong: 0 20px 40px rgba(var(--app-shadow-rgb), 0.2), 0 8px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||||
|
--app-fab-shadow-hover: 0 22px 42px rgba(var(--app-shadow-rgb), 0.22), 0 8px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||||
|
--app-fab-shadow-strong-hover: 0 26px 46px rgba(var(--app-shadow-rgb), 0.24), 0 10px 22px rgba(var(--app-shadow-rgb), 0.14);
|
||||||
|
--app-fab-shadow-active: 0 10px 22px rgba(var(--app-shadow-rgb), 0.16), 0 3px 8px rgba(var(--app-shadow-rgb), 0.1);
|
||||||
|
--app-overlay-shadow: 0 18px 42px rgba(var(--app-shadow-rgb), 0.14), 0 6px 18px rgba(var(--app-shadow-rgb), 0.08);
|
||||||
|
--app-surface-shadow: 0 10px 24px rgba(var(--app-shadow-rgb), 0.07), 0 2px 8px rgba(var(--app-shadow-rgb), 0.05);
|
||||||
|
--app-surface-hover-shadow: 0 14px 30px rgba(var(--app-shadow-rgb), 0.09), 0 4px 12px rgba(var(--app-shadow-rgb), 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme-shadow='medium'] {
|
||||||
|
--app-card-rest-shadow: 0 14px 32px rgba(var(--app-shadow-rgb), 0.09), 0 4px 12px rgba(var(--app-shadow-rgb), 0.06);
|
||||||
|
--app-card-hover-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.11), 0 6px 16px rgba(var(--app-shadow-rgb), 0.07);
|
||||||
|
--app-fab-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.2), 0 7px 18px rgba(var(--app-shadow-rgb), 0.12);
|
||||||
|
--app-fab-shadow-strong: 0 24px 48px rgba(var(--app-shadow-rgb), 0.24), 0 10px 24px rgba(var(--app-shadow-rgb), 0.14);
|
||||||
|
--app-fab-shadow-hover: 0 24px 46px rgba(var(--app-shadow-rgb), 0.24), 0 10px 22px rgba(var(--app-shadow-rgb), 0.14);
|
||||||
|
--app-fab-shadow-strong-hover: 0 30px 54px rgba(var(--app-shadow-rgb), 0.28), 0 12px 28px rgba(var(--app-shadow-rgb), 0.16);
|
||||||
|
--app-fab-shadow-active: 0 12px 26px rgba(var(--app-shadow-rgb), 0.18), 0 4px 10px rgba(var(--app-shadow-rgb), 0.12);
|
||||||
|
--app-overlay-shadow: 0 24px 56px rgba(var(--app-shadow-rgb), 0.18), 0 10px 24px rgba(var(--app-shadow-rgb), 0.1);
|
||||||
|
--app-surface-shadow: 0 14px 32px rgba(var(--app-shadow-rgb), 0.1), 0 4px 12px rgba(var(--app-shadow-rgb), 0.07);
|
||||||
|
--app-surface-hover-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.12), 0 6px 16px rgba(var(--app-shadow-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme-shadow='high'] {
|
||||||
|
--app-card-rest-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.12), 0 6px 18px rgba(var(--app-shadow-rgb), 0.08);
|
||||||
|
--app-card-hover-shadow: 0 22px 50px rgba(var(--app-shadow-rgb), 0.15), 0 8px 22px rgba(var(--app-shadow-rgb), 0.1);
|
||||||
|
--app-fab-shadow: 0 22px 48px rgba(var(--app-shadow-rgb), 0.24), 0 10px 24px rgba(var(--app-shadow-rgb), 0.14);
|
||||||
|
--app-fab-shadow-strong: 0 28px 58px rgba(var(--app-shadow-rgb), 0.3), 0 12px 30px rgba(var(--app-shadow-rgb), 0.18);
|
||||||
|
--app-fab-shadow-hover: 0 28px 56px rgba(var(--app-shadow-rgb), 0.28), 0 12px 28px rgba(var(--app-shadow-rgb), 0.17);
|
||||||
|
--app-fab-shadow-strong-hover: 0 34px 64px rgba(var(--app-shadow-rgb), 0.34), 0 14px 32px rgba(var(--app-shadow-rgb), 0.2);
|
||||||
|
--app-fab-shadow-active: 0 14px 30px rgba(var(--app-shadow-rgb), 0.22), 0 5px 12px rgba(var(--app-shadow-rgb), 0.14);
|
||||||
|
--app-overlay-shadow: 0 30px 70px rgba(var(--app-shadow-rgb), 0.22), 0 14px 30px rgba(var(--app-shadow-rgb), 0.12);
|
||||||
|
--app-surface-shadow: 0 18px 40px rgba(var(--app-shadow-rgb), 0.13), 0 6px 18px rgba(var(--app-shadow-rgb), 0.09);
|
||||||
|
--app-surface-hover-shadow: 0 22px 50px rgba(var(--app-shadow-rgb), 0.16), 0 8px 22px rgba(var(--app-shadow-rgb), 0.11);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度条样式
|
// 进度条样式
|
||||||
@@ -59,17 +113,33 @@ html {
|
|||||||
// 统一系统内卡片阴影,显式覆盖 Vuetify elevation 或局部卡片默认投影。
|
// 统一系统内卡片阴影,显式覆盖 Vuetify elevation 或局部卡片默认投影。
|
||||||
.v-card,
|
.v-card,
|
||||||
.v-application .v-card.v-card[class] {
|
.v-application .v-card.v-card[class] {
|
||||||
box-shadow: none !important;
|
box-shadow: var(--app-surface-shadow) !important;
|
||||||
transition: box-shadow 0.2s ease;
|
transition: box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.v-card:hover,
|
.v-card:hover,
|
||||||
.v-application .v-card.v-card[class]:hover {
|
.v-application .v-card.v-card[class]:hover {
|
||||||
box-shadow: none !important;
|
box-shadow: var(--app-surface-hover-shadow) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只给外层 surface 加阴影,卡片内部的子组件保持平面,避免层级噪声。
|
||||||
|
.v-card .v-card,
|
||||||
|
.v-card .v-sheet,
|
||||||
|
.v-card .v-list,
|
||||||
|
.v-card .v-expansion-panel,
|
||||||
|
.v-card .v-table,
|
||||||
|
.v-card .v-window,
|
||||||
|
.v-card .v-toolbar,
|
||||||
|
.v-card .v-navigation-drawer,
|
||||||
|
.v-card .v-stepper,
|
||||||
|
.v-card .v-alert,
|
||||||
|
.v-card .v-avatar,
|
||||||
|
.v-card .v-chip {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
// 主题定制器的 bordered 皮肤:保持原布局密度,只给主要容器增加清晰边界。
|
// 主题定制器的 bordered 皮肤:保持原布局密度,只给主要容器增加清晰边界。
|
||||||
html[data-theme-skin='bordered'] {
|
html[data-theme-skin='bordered'] {
|
||||||
.v-card:not(.bg-primary),
|
.v-card:not(.bg-primary),
|
||||||
@@ -78,6 +148,12 @@ html[data-theme-skin='bordered'] {
|
|||||||
.v-expansion-panel,
|
.v-expansion-panel,
|
||||||
.v-list {
|
.v-list {
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1) !important;
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-sheet,
|
||||||
|
.v-table,
|
||||||
|
.v-expansion-panel,
|
||||||
|
.v-list {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,12 +251,6 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
--app-card-surface-opacity: var(--transparent-opacity-light, 0.2);
|
--app-card-surface-opacity: var(--transparent-opacity-light, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="transparent"],
|
|
||||||
.v-theme--transparent {
|
|
||||||
--app-card-rest-shadow: none;
|
|
||||||
--app-card-hover-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保证卡片右上角的浮动操作区始终高于可点击的卡片内容层,避免误触发详情打开。
|
// 保证卡片右上角的浮动操作区始终高于可点击的卡片内容层,避免误触发详情打开。
|
||||||
.app-card-top-action {
|
.app-card-top-action {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -546,9 +616,7 @@ html[data-theme="transparent"],
|
|||||||
.compact-fab .v-btn {
|
.compact-fab .v-btn {
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
box-shadow:
|
box-shadow: var(--app-fab-shadow) !important;
|
||||||
0 16px 34px rgb(15 23 42 / 16%),
|
|
||||||
0 6px 16px rgb(15 23 42 / 10%);
|
|
||||||
opacity: 0.98;
|
opacity: 0.98;
|
||||||
transition:
|
transition:
|
||||||
transform 0.18s ease,
|
transform 0.18s ease,
|
||||||
@@ -559,9 +627,7 @@ html[data-theme="transparent"],
|
|||||||
|
|
||||||
.compact-fab--primary .v-btn {
|
.compact-fab--primary .v-btn {
|
||||||
block-size: 3rem !important;
|
block-size: 3rem !important;
|
||||||
box-shadow:
|
box-shadow: var(--app-fab-shadow-strong) !important;
|
||||||
0 20px 40px rgb(15 23 42 / 20%),
|
|
||||||
0 8px 18px rgb(15 23 42 / 12%);
|
|
||||||
inline-size: 3rem !important;
|
inline-size: 3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,24 +646,18 @@ html[data-theme="transparent"],
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.compact-fab .v-btn:hover {
|
.compact-fab .v-btn:hover {
|
||||||
box-shadow:
|
box-shadow: var(--app-fab-shadow-hover) !important;
|
||||||
0 22px 42px rgb(15 23 42 / 22%),
|
|
||||||
0 8px 18px rgb(15 23 42 / 12%);
|
|
||||||
filter: saturate(1.03);
|
filter: saturate(1.03);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-fab--primary .v-btn:hover {
|
.compact-fab--primary .v-btn:hover {
|
||||||
box-shadow:
|
box-shadow: var(--app-fab-shadow-strong-hover) !important;
|
||||||
0 26px 46px rgb(15 23 42 / 24%),
|
|
||||||
0 10px 22px rgb(15 23 42 / 14%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact-fab .v-btn:active {
|
.compact-fab .v-btn:active {
|
||||||
box-shadow:
|
box-shadow: var(--app-fab-shadow-active) !important;
|
||||||
0 10px 22px rgb(15 23 42 / 16%),
|
|
||||||
0 3px 8px rgb(15 23 42 / 10%);
|
|
||||||
transform: translateY(0) scale(0.98);
|
transform: translateY(0) scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +683,7 @@ html[data-theme="transparent"],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 601px) {
|
@media (width >= 601px) {
|
||||||
html[data-theme-customizer-open='true'] .compact-fab-stack {
|
html[data-theme-customizer-open='true'] .compact-fab-stack {
|
||||||
inset-inline-end: calc(var(--theme-customizer-fab-offset) + max(1rem, calc(env(safe-area-inset-right) + 1rem)));
|
inset-inline-end: calc(var(--theme-customizer-fab-offset) + max(1rem, calc(env(safe-area-inset-right) + 1rem)));
|
||||||
}
|
}
|
||||||
@@ -633,7 +693,7 @@ html[data-theme="transparent"],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 601px) and (max-width: 768px) {
|
@media (width >= 601px) and (width <= 768px) {
|
||||||
html[data-theme-customizer-open='true'] .compact-fab-stack {
|
html[data-theme-customizer-open='true'] .compact-fab-stack {
|
||||||
inset-inline-end: calc(
|
inset-inline-end: calc(
|
||||||
var(--theme-customizer-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
var(--theme-customizer-fab-offset) + max(0.875rem, calc(env(safe-area-inset-right) + 0.875rem))
|
||||||
@@ -665,16 +725,19 @@ html[data-theme="transparent"],
|
|||||||
.v-overlay__content .v-list{
|
.v-overlay__content .v-list{
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
background-color: rgb(var(--v-theme-surface), 0.9) !important;
|
background-color: rgb(var(--v-theme-surface), 0.9) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
padding-inline: 0.5rem !important;
|
padding-inline: 0.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-overlay__content .v-card:not(.bg-primary){
|
.v-overlay__content .v-card:not(.bg-primary){
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
background-color: rgb(var(--v-theme-surface), 0.95) !important;
|
background-color: rgb(var(--v-theme-surface), 0.95) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
.v-list, .v-table {
|
.v-list, .v-table {
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,18 +756,24 @@ html[data-theme="transparent"],
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v-overlay__content {
|
.v-overlay__content {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
||||||
transition: opacity 0.2s ease !important;
|
transition: opacity 0.2s ease, box-shadow 0.2s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-menu > .v-overlay__content {
|
.v-menu > .v-overlay__content {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-dialog--fullscreen > .v-overlay__content > .v-card {
|
.v-dialog--fullscreen > .v-overlay__content > .v-card {
|
||||||
padding-block-end: calc(env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
padding-block-end: calc(env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-dialog--fullscreen > .v-overlay__content {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.v-dialog > .v-overlay__content {
|
.v-dialog > .v-overlay__content {
|
||||||
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
@@ -713,6 +782,18 @@ html[data-theme="transparent"],
|
|||||||
padding-block-end: env(safe-area-inset-bottom);
|
padding-block-end: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-dialog > .v-overlay__content > .v-card,
|
||||||
|
.v-bottom-sheet > .v-bottom-sheet__content.v-overlay__content > .v-card,
|
||||||
|
.v-menu > .v-overlay__content > .v-card,
|
||||||
|
.v-menu > .v-overlay__content > .v-list {
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--app-overlay-shadow) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-dialog--fullscreen > .v-overlay__content > .v-card {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-icon-button {
|
.settings-icon-button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.95rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user