feat: 统一管理端弹窗与内容区交互样式
- 收敛管理端公共 Modal 链路,新增表单弹窗与普通内容弹窗包装\n- 迁移 Bot、知识库、插件、工作流、资源、MCP、数据中枢与系统管理页面级弹窗\n- 统一内容区工具栏、列表容器、导航与顶部按钮的视觉密度和交互节奏
This commit is contained in:
@@ -8,7 +8,10 @@ import {
|
|||||||
} from 'vue-element-plus-x';
|
} from 'vue-element-plus-x';
|
||||||
|
|
||||||
import { registerAccessDirective } from '@easyflow/access';
|
import { registerAccessDirective } from '@easyflow/access';
|
||||||
import { registerLoadingDirective } from '@easyflow/common-ui';
|
import {
|
||||||
|
registerLoadingDirective,
|
||||||
|
setDefaultModalProps,
|
||||||
|
} from '@easyflow/common-ui';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
import { initStores } from '@easyflow/stores';
|
import { initStores } from '@easyflow/stores';
|
||||||
import '@easyflow/styles';
|
import '@easyflow/styles';
|
||||||
@@ -31,10 +34,12 @@ async function bootstrap(namespace: string) {
|
|||||||
// 初始化表单组件
|
// 初始化表单组件
|
||||||
await initSetupEasyFlowForm();
|
await initSetupEasyFlowForm();
|
||||||
|
|
||||||
// // 设置弹窗的默认配置
|
// 设置弹窗的默认配置
|
||||||
// setDefaultModalProps({
|
setDefaultModalProps({
|
||||||
// fullscreenButton: false,
|
bordered: true,
|
||||||
// });
|
fullscreenButton: false,
|
||||||
|
overlayBlur: 0,
|
||||||
|
});
|
||||||
// // 设置抽屉的默认配置
|
// // 设置抽屉的默认配置
|
||||||
// setDefaultDrawerProps({
|
// setDefaultDrawerProps({
|
||||||
// zIndex: 2000,
|
// zIndex: 2000,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { ArrowDown } from '@element-plus/icons-vue';
|
import { ArrowDown, Search } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDropdown,
|
ElDropdown,
|
||||||
@@ -14,9 +14,7 @@ import {
|
|||||||
import { hasPermission } from '#/api/common/hasPermission.ts';
|
import { hasPermission } from '#/api/common/hasPermission.ts';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
// 定义组件属性
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 按钮配置数组
|
|
||||||
buttons: {
|
buttons: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -29,40 +27,43 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 最大显示按钮数量(不包括下拉菜单)
|
|
||||||
maxVisibleButtons: {
|
maxVisibleButtons: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 3,
|
default: 3,
|
||||||
},
|
},
|
||||||
// 搜索框占位符
|
|
||||||
searchPlaceholder: {
|
searchPlaceholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: $t('common.searchPlaceholder'),
|
default: $t('common.searchPlaceholder'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['search', 'button-click', 'buttonClick']);
|
const emit = defineEmits(['search', 'buttonClick']);
|
||||||
|
|
||||||
// 搜索值
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref('');
|
||||||
|
|
||||||
// 计算显示的按钮
|
const filterButtonsByPermission = (buttons) => {
|
||||||
const visibleButtons = computed(() => {
|
return buttons.filter((button) => {
|
||||||
return props.buttons.slice(0, props.maxVisibleButtons);
|
return !button.permission || hasPermission([button.permission]);
|
||||||
});
|
|
||||||
|
|
||||||
// 计算下拉菜单中的按钮
|
|
||||||
const dropdownButtons = computed(() => {
|
|
||||||
const dropdownButtonsTemp = props.buttons.slice(props.maxVisibleButtons);
|
|
||||||
if (dropdownButtonsTemp.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return dropdownButtonsTemp.value.filter((action) => {
|
|
||||||
return hasPermission([action.permission]);
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleButtons = computed(() => {
|
||||||
|
return filterButtonsByPermission(props.buttons).slice(
|
||||||
|
0,
|
||||||
|
props.maxVisibleButtons,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理搜索
|
const dropdownButtons = computed(() => {
|
||||||
|
return filterButtonsByPermission(props.buttons).slice(
|
||||||
|
props.maxVisibleButtons,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emitButtonEvent = (payload) => {
|
||||||
|
emit('buttonClick', payload);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
emit('search', searchValue.value);
|
emit('search', searchValue.value);
|
||||||
};
|
};
|
||||||
@@ -72,9 +73,8 @@ const handleReset = () => {
|
|||||||
emit('search', '');
|
emit('search', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理按钮点击
|
|
||||||
const handleButtonClick = (button) => {
|
const handleButtonClick = (button) => {
|
||||||
emit('buttonClick', {
|
emitButtonEvent({
|
||||||
type: 'button',
|
type: 'button',
|
||||||
key: button.key,
|
key: button.key,
|
||||||
button,
|
button,
|
||||||
@@ -82,9 +82,8 @@ const handleButtonClick = (button) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理下拉菜单点击
|
|
||||||
const handleDropdownClick = (button) => {
|
const handleDropdownClick = (button) => {
|
||||||
emit('buttonClick', {
|
emitButtonEvent({
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
key: button.key,
|
key: button.key,
|
||||||
button,
|
button,
|
||||||
@@ -95,34 +94,34 @@ const handleDropdownClick = (button) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="custom-header">
|
<div class="custom-header">
|
||||||
<!-- 左侧搜索区域 -->
|
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="search-container">
|
<div class="search-group">
|
||||||
<div>
|
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="searchValue"
|
v-model="searchValue"
|
||||||
:placeholder="$t('common.searchPlaceholder')"
|
:placeholder="searchPlaceholder"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
clearable
|
clearable
|
||||||
/>
|
@keyup.enter="handleSearch"
|
||||||
</div>
|
>
|
||||||
<div>
|
<template #prefix>
|
||||||
|
<ElIcon class="search-prefix">
|
||||||
|
<Search />
|
||||||
|
</ElIcon>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
<ElButton type="primary" auto-insert-space @click="handleSearch">
|
<ElButton type="primary" auto-insert-space @click="handleSearch">
|
||||||
{{ $t('button.query') }}
|
{{ $t('button.query') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ElButton auto-insert-space @click="handleReset">
|
<ElButton auto-insert-space @click="handleReset">
|
||||||
{{ $t('button.reset') }}
|
{{ $t('button.reset') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧按钮区域 -->
|
<div
|
||||||
<div class="header-right">
|
v-if="visibleButtons.length > 0 || dropdownButtons.length > 0"
|
||||||
<!-- 显示的按钮(最多3个) -->
|
class="header-right"
|
||||||
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(button, index) in visibleButtons"
|
v-for="(button, index) in visibleButtons"
|
||||||
:key="button.key || index"
|
:key="button.key || index"
|
||||||
@@ -138,14 +137,13 @@ const handleDropdownClick = (button) => {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 下拉菜单(隐藏的按钮) -->
|
|
||||||
<ElDropdown
|
<ElDropdown
|
||||||
v-if="dropdownButtons.length > 0"
|
v-if="dropdownButtons.length > 0"
|
||||||
@command="handleDropdownClick"
|
@command="handleDropdownClick"
|
||||||
>
|
>
|
||||||
<ElButton>
|
<ElButton>
|
||||||
{{ $t('button.more')
|
{{ $t('button.more') }}
|
||||||
}}<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
|
<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
@@ -158,7 +156,7 @@ const handleDropdownClick = (button) => {
|
|||||||
<ElIcon v-if="button.icon">
|
<ElIcon v-if="button.icon">
|
||||||
<component :is="button.icon" />
|
<component :is="button.icon" />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<span style="margin-left: 8px">{{ button.text }}</span>
|
<span class="dropdown-label">{{ button.text }}</span>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
@@ -168,54 +166,109 @@ const handleDropdownClick = (button) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.custom-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left,
|
|
||||||
.header-right {
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-header {
|
.custom-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-left,
|
||||||
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container {
|
.header-left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 300px;
|
width: min(360px, 100%);
|
||||||
border-radius: 4px;
|
}
|
||||||
|
|
||||||
|
.search-input :deep(.el-input__wrapper) {
|
||||||
|
min-height: 40px;
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.92);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.44),
|
||||||
|
0 10px 24px -24px hsl(var(--foreground) / 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input :deep(.el-input__wrapper:hover),
|
||||||
|
.search-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: hsl(var(--surface-subtle) / 0.98);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.56),
|
||||||
|
0 0 0 3px hsl(var(--primary) / 0.08),
|
||||||
|
0 12px 24px -24px hsl(var(--foreground) / 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-prefix {
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
|
.header-right :deep(.el-button),
|
||||||
|
.search-group :deep(.el-button) {
|
||||||
|
min-width: 64px;
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding-inline: 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right :deep(.el-button:not(.el-button--primary)),
|
||||||
|
.search-group :deep(.el-button:not(.el-button--primary)) {
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.86);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.34),
|
||||||
|
0 10px 24px -24px hsl(var(--foreground) / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right :deep(.el-button--primary),
|
||||||
|
.search-group :deep(.el-button--primary) {
|
||||||
|
box-shadow: 0 16px 28px -22px hsl(var(--primary) / 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-label {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.custom-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
227
easyflow-ui-admin/app/src/components/page/ListPageShell.vue
Normal file
227
easyflow-ui-admin/app/src/components/page/ListPageShell.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
contentPadding?: number | string;
|
||||||
|
dense?: boolean;
|
||||||
|
stickyToolbar?: boolean;
|
||||||
|
surface?: 'panel' | 'subtle';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
contentPadding: 0,
|
||||||
|
dense: false,
|
||||||
|
stickyToolbar: false,
|
||||||
|
surface: 'panel',
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const hasToolbar = computed(() => Boolean(slots.filters || slots.actions));
|
||||||
|
|
||||||
|
const contentStyle = computed((): CSSProperties => {
|
||||||
|
const padding =
|
||||||
|
typeof props.contentPadding === 'number'
|
||||||
|
? `${props.contentPadding}px`
|
||||||
|
: props.contentPadding;
|
||||||
|
|
||||||
|
return padding ? { '--list-page-shell-content-padding': padding } : {};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
:class="[
|
||||||
|
'list-page-shell',
|
||||||
|
`is-${surface}`,
|
||||||
|
{
|
||||||
|
'is-dense': dense,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="hasToolbar"
|
||||||
|
:class="[
|
||||||
|
'list-page-shell__toolbar',
|
||||||
|
{
|
||||||
|
'is-sticky': stickyToolbar,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div v-if="$slots.filters" class="list-page-shell__filters">
|
||||||
|
<slot name="filters"></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots.actions" class="list-page-shell__actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-page-shell__content" :style="contentStyle">
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="empty"></slot>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-page-shell {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell.is-dense {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__toolbar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 6px 2px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__toolbar.is-sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 12px;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 10px 10px 6px;
|
||||||
|
background: hsl(var(--glass-tint) / 0.7);
|
||||||
|
border: 1px solid hsl(var(--glass-border) / 0.44);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: var(--shadow-toolbar);
|
||||||
|
backdrop-filter: blur(var(--glass-blur)) saturate(155%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__filters,
|
||||||
|
.list-page-shell__actions {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__filters {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content {
|
||||||
|
--list-page-shell-content-padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: var(--list-page-shell-content-padding);
|
||||||
|
background: hsl(var(--surface-panel));
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: var(--shadow-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell.is-subtle .list-page-shell__content {
|
||||||
|
background: hsl(var(--surface-contrast-soft));
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.page-data-container) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-empty) {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table) {
|
||||||
|
--el-table-bg-color: transparent;
|
||||||
|
--el-table-tr-bg-color: transparent;
|
||||||
|
--el-table-header-bg-color: hsl(var(--table-header-bg));
|
||||||
|
--el-table-border-color: hsl(var(--table-row-border));
|
||||||
|
--el-table-current-row-bg-color: hsl(var(--table-row-hover));
|
||||||
|
--el-fill-color-blank: transparent;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table--border),
|
||||||
|
.list-page-shell__content :deep(.el-table__inner-wrapper),
|
||||||
|
.list-page-shell__content :deep(.el-table__header-wrapper),
|
||||||
|
.list-page-shell__content :deep(.el-table__body-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table__inner-wrapper::before),
|
||||||
|
.list-page-shell__content :deep(.el-table--border::before),
|
||||||
|
.list-page-shell__content :deep(.el-table--border::after) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table th.el-table__cell) {
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
background: hsl(var(--table-header-bg) / 0.86);
|
||||||
|
border-bottom: 1px solid hsl(var(--divider-faint) / 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table th.el-table__cell:first-child) {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table th.el-table__cell:last-child) {
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table td.el-table__cell),
|
||||||
|
.list-page-shell__content :deep(.el-table--border .el-table__cell) {
|
||||||
|
padding: 14px 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid hsl(var(--divider-faint) / 0.46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-table__row:hover > td.el-table__cell) {
|
||||||
|
background: hsl(var(--table-row-hover)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-tag) {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-pagination) {
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__content :deep(.el-pagination button),
|
||||||
|
.list-page-shell__content :deep(.el-pager li) {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__toolbar :deep(.custom-header) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.list-page-shell {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-page-shell__actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<PageDataProps>(), {
|
|||||||
// 响应式数据
|
// 响应式数据
|
||||||
const pageList = ref([]);
|
const pageList = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const queryParams = ref({});
|
const queryParams = ref<Record<string, any>>({});
|
||||||
|
|
||||||
const pageInfo = reactive({
|
const pageInfo = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
@@ -32,7 +32,7 @@ const pageInfo = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 模拟 API 调用 - 这里需要根据你的实际 API 调用方式调整
|
// 模拟 API 调用 - 这里需要根据你的实际 API 调用方式调整
|
||||||
const doGet = async (params: any) => {
|
const doGet = async (params: Record<string, any>) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 这里替换为你的实际 API 调用
|
// 这里替换为你的实际 API 调用
|
||||||
@@ -76,7 +76,7 @@ const handleCurrentChange = (newPage: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 暴露给父组件的方法 (替代 useImperativeHandle)
|
// 暴露给父组件的方法 (替代 useImperativeHandle)
|
||||||
const setQuery = (newQueryParams: string) => {
|
const setQuery = (newQueryParams: Record<string, any>) => {
|
||||||
pageInfo.pageNumber = 1;
|
pageInfo.pageNumber = 1;
|
||||||
pageInfo.pageSize = props.pageSize;
|
pageInfo.pageSize = props.pageSize;
|
||||||
queryParams.value = newQueryParams;
|
queryParams.value = newQueryParams;
|
||||||
@@ -85,6 +85,7 @@ const setQuery = (newQueryParams: string) => {
|
|||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
reload: getPageList,
|
||||||
setQuery,
|
setQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,10 +107,13 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-data-container" v-loading="loading">
|
<div class="page-data-container" v-loading="loading">
|
||||||
<template v-if="pageList.length > 0">
|
<template v-if="pageList.length > 0">
|
||||||
<div>
|
<div class="page-data-container__body">
|
||||||
<slot :page-list="pageList"></slot>
|
<slot :page-list="pageList"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="pageInfo.total > pageInfo.pageSize" class="mx-auto mt-8 w-fit">
|
<div
|
||||||
|
v-if="pageInfo.total > pageInfo.pageSize"
|
||||||
|
class="page-data-container__pagination mx-auto mt-6 w-fit"
|
||||||
|
>
|
||||||
<ElPagination
|
<ElPagination
|
||||||
v-model:current-page="pageInfo.pageNumber"
|
v-model:current-page="pageInfo.pageNumber"
|
||||||
v-model:page-size="pageInfo.pageSize"
|
v-model:page-size="pageInfo.pageSize"
|
||||||
@@ -121,9 +125,36 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<slot v-else name="empty">
|
||||||
<ElEmpty
|
<ElEmpty
|
||||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||||
v-else
|
|
||||||
/>
|
/>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-data-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 18px 20px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-data-container__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-data-container__pagination {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-data-container {
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ const props = defineProps({
|
|||||||
default: () => ['image/gif', 'image/jpeg', 'image/png', 'image/webp'],
|
default: () => ['image/gif', 'image/jpeg', 'image/png', 'image/webp'],
|
||||||
},
|
},
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
|
shape: {
|
||||||
|
type: String,
|
||||||
|
default: 'circle',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['success', 'update:modelValue']);
|
const emit = defineEmits(['success', 'update:modelValue']);
|
||||||
@@ -77,6 +89,11 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|||||||
<template>
|
<template>
|
||||||
<ElUpload
|
<ElUpload
|
||||||
class="avatar-uploader"
|
class="avatar-uploader"
|
||||||
|
:class="[
|
||||||
|
`avatar-uploader--${props.theme}`,
|
||||||
|
{ 'avatar-uploader--rounded': props.shape === 'rounded' },
|
||||||
|
]"
|
||||||
|
:style="{ '--avatar-size': `${props.size}px` }"
|
||||||
:action="`${apiURL}${props.action}`"
|
:action="`${apiURL}${props.action}`"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
@@ -96,30 +113,65 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.avatar-uploader .avatar {
|
.avatar-uploader .avatar {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100px;
|
width: var(--avatar-size);
|
||||||
height: 100px;
|
height: var(--avatar-size);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.avatar-uploader .el-upload {
|
.avatar-uploader .el-upload {
|
||||||
|
width: var(--avatar-size);
|
||||||
|
height: var(--avatar-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #e6e9ee;
|
border: 1px solid hsl(var(--input) / 0.92);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: var(--el-transition-duration-fast);
|
background: hsl(var(--modal-surface-strong) / 0.92);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.72),
|
||||||
|
0 16px 28px -24px hsl(var(--foreground) / 0.24);
|
||||||
|
transition:
|
||||||
|
transform var(--el-transition-duration-fast),
|
||||||
|
border-color var(--el-transition-duration-fast),
|
||||||
|
box-shadow var(--el-transition-duration-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-uploader .el-upload:hover {
|
.avatar-uploader .el-upload:hover {
|
||||||
border-color: var(--el-color-primary);
|
border-color: hsl(var(--primary) / 0.52);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(0 0% 100% / 0.76),
|
||||||
|
0 20px 36px -26px hsl(var(--modal-preview-glow) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader--soft-panel .el-upload {
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at top,
|
||||||
|
hsl(var(--modal-preview-glow) / 0.14),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
145deg,
|
||||||
|
hsl(var(--modal-preview-surface) / 0.98),
|
||||||
|
hsl(var(--modal-preview-surface-strong) / 0.9)
|
||||||
|
);
|
||||||
|
border-color: hsl(var(--modal-preview-border) / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader--rounded .el-upload {
|
||||||
|
border-radius: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon.avatar-uploader-icon {
|
.el-icon.avatar-uploader-icon {
|
||||||
width: 100px;
|
width: var(--avatar-size);
|
||||||
height: 100px;
|
height: var(--avatar-size);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: var(--el-text-color-secondary);
|
color: hsl(var(--text-muted));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,6 +45,25 @@
|
|||||||
"publicChatCopySuccess": "Copied",
|
"publicChatCopySuccess": "Copied",
|
||||||
"publicChatCopyFail": "Copy failed",
|
"publicChatCopyFail": "Copy failed",
|
||||||
"basicInfo": "Basic Info",
|
"basicInfo": "Basic Info",
|
||||||
|
"modal": {
|
||||||
|
"createDescription": "Set the assistant appearance, identity and base availability.",
|
||||||
|
"editDescription": "Update the assistant presentation and base availability.",
|
||||||
|
"previewEyebrow": "Assistant Preview",
|
||||||
|
"previewTitleFallback": "Untitled Assistant",
|
||||||
|
"previewAliasFallback": "The alias will appear here after it is filled in",
|
||||||
|
"previewDescriptionFallback": "Describe the tasks this assistant is good at in one sentence.",
|
||||||
|
"previewStatusEnabled": "Enabled",
|
||||||
|
"previewStatusDisabled": "Disabled",
|
||||||
|
"sectionAppearanceTitle": "Basic Info",
|
||||||
|
"sectionAppearanceDescription": "Define the avatar, name and category.",
|
||||||
|
"sectionIdentityTitle": "Identity",
|
||||||
|
"sectionIdentityDescription": "Configure a stable alias and an external description.",
|
||||||
|
"sectionPublishTitle": "Availability",
|
||||||
|
"sectionPublishDescription": "Control whether the assistant is currently available.",
|
||||||
|
"aliasHint": "Use letters, numbers or hyphens for a stable access identifier.",
|
||||||
|
"descriptionHint": "This summary helps teammates understand when to use the assistant.",
|
||||||
|
"statusHint": "When disabled, the assistant configuration is kept but shown as inactive."
|
||||||
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"welcome": "Please enter welcome message",
|
"welcome": "Please enter welcome message",
|
||||||
"prompt": "You are an AI assistant. Please provide clear and accurate answers based on the user's questions.",
|
"prompt": "You are an AI assistant. Please provide clear and accurate answers based on the user's questions.",
|
||||||
|
|||||||
@@ -45,6 +45,25 @@
|
|||||||
"publicChatCopySuccess": "复制成功",
|
"publicChatCopySuccess": "复制成功",
|
||||||
"publicChatCopyFail": "复制失败",
|
"publicChatCopyFail": "复制失败",
|
||||||
"basicInfo": "基础信息",
|
"basicInfo": "基础信息",
|
||||||
|
"modal": {
|
||||||
|
"createDescription": "设置助手的外观、标识和基础发布状态。",
|
||||||
|
"editDescription": "更新助手的展示信息与基础状态。",
|
||||||
|
"previewEyebrow": "助手预览",
|
||||||
|
"previewTitleFallback": "未命名助手",
|
||||||
|
"previewAliasFallback": "填写别名后会展示访问标识",
|
||||||
|
"previewDescriptionFallback": "用一句话描述这个助手擅长处理的问题场景。",
|
||||||
|
"previewStatusEnabled": "启用中",
|
||||||
|
"previewStatusDisabled": "未启用",
|
||||||
|
"sectionAppearanceTitle": "基础信息",
|
||||||
|
"sectionAppearanceDescription": "确定助手头像、名称和所属分类。",
|
||||||
|
"sectionIdentityTitle": "标识信息",
|
||||||
|
"sectionIdentityDescription": "配置稳定的访问别名与对外说明。",
|
||||||
|
"sectionPublishTitle": "发布设置",
|
||||||
|
"sectionPublishDescription": "控制助手当前是否对外可用。",
|
||||||
|
"aliasHint": "建议使用英文、数字或短横线,便于作为访问标识。",
|
||||||
|
"descriptionHint": "这段介绍会帮助成员快速判断助手适用的任务。",
|
||||||
|
"statusHint": "关闭后助手配置会保留,但不会作为启用状态展示。"
|
||||||
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"welcome": "请输入欢迎语",
|
"welcome": "请输入欢迎语",
|
||||||
"prompt": "你是一个AI助手,请根据用户的问题给出清晰、准确的回答。",
|
"prompt": "你是一个AI助手,请根据用户的问题给出清晰、准确的回答。",
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import type { ActionButton } from '#/components/page/CardList.vue';
|
|||||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
|
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -299,16 +298,21 @@ const getSideList = async () => {
|
|||||||
<!-- 创建&编辑Bot弹窗 -->
|
<!-- 创建&编辑Bot弹窗 -->
|
||||||
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
|
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
|
||||||
|
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
:closable="!saveLoading"
|
||||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="saveLoading"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
|
:submitting="saveLoading"
|
||||||
|
@confirm="handleSubmit"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="formRules"
|
:rules="formRules"
|
||||||
label-width="120px"
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<!-- 动态生成表单项 -->
|
<!-- 动态生成表单项 -->
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
@@ -330,15 +334,6 @@ const getSideList = async () => {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="dialogVisible = false">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,18 +3,12 @@ import type { BotInfo } from '@easyflow/types';
|
|||||||
|
|
||||||
import type { SaveBotParams, UpdateBotParams } from '#/api/ai/bot';
|
import type { SaveBotParams, UpdateBotParams } from '#/api/ai/bot';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import {
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import { saveBot, updateBotApi } from '#/api/ai/bot';
|
import { saveBot, updateBotApi } from '#/api/ai/bot';
|
||||||
@@ -23,98 +17,334 @@ import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
|||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const initialFormData = {
|
const createInitialFormData = (): SaveBotParams => ({
|
||||||
icon: '',
|
|
||||||
title: '',
|
|
||||||
alias: '',
|
alias: '',
|
||||||
description: '',
|
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
status: 1,
|
status: 1,
|
||||||
};
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const dialogType = ref<'create' | 'edit'>('create');
|
const dialogType = ref<'create' | 'edit'>('create');
|
||||||
const formRef = ref<InstanceType<typeof ElForm>>();
|
const formRef = ref<InstanceType<typeof ElForm>>();
|
||||||
const formData = ref<SaveBotParams | UpdateBotParams>(initialFormData);
|
const formData = ref<SaveBotParams | UpdateBotParams>(createInitialFormData());
|
||||||
const rules = {
|
|
||||||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
|
||||||
alias: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
|
||||||
};
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const rules = {
|
||||||
|
alias: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||||
|
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
return `${$t(`button.${dialogType.value}`)}${$t('bot.chatAssistant')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogDescription = computed(() => {
|
||||||
|
return dialogType.value === 'create'
|
||||||
|
? $t('bot.modal.createDescription')
|
||||||
|
: $t('bot.modal.editDescription');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false);
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const [err, res] = await tryit(
|
const [err, res] = await tryit(
|
||||||
dialogType.value === 'create' ? saveBot : updateBotApi,
|
dialogType.value === 'create' ? saveBot : updateBotApi,
|
||||||
)(formData.value as any);
|
)(formData.value as SaveBotParams & UpdateBotParams);
|
||||||
|
|
||||||
if (!err && res.errorCode === 0) {
|
if (!err && res.errorCode === 0) {
|
||||||
emit('success');
|
emit('success');
|
||||||
ElMessage.success($t('message.saveOkMessage'));
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
ElMessage.error(res?.message || $t('message.saveFail'));
|
||||||
open(type: typeof dialogType.value, bot?: BotInfo) {
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(type: 'create' | 'edit', bot?: BotInfo) {
|
||||||
formData.value = bot
|
formData.value = bot
|
||||||
? {
|
? {
|
||||||
id: bot.id,
|
...createInitialFormData(),
|
||||||
icon: bot.icon,
|
|
||||||
title: bot.title,
|
|
||||||
alias: bot.alias,
|
alias: bot.alias,
|
||||||
description: bot.description,
|
|
||||||
categoryId: bot.categoryId,
|
categoryId: bot.categoryId,
|
||||||
|
description: bot.description,
|
||||||
|
icon: bot.icon,
|
||||||
|
id: bot.id,
|
||||||
status: bot.status,
|
status: bot.status,
|
||||||
|
title: bot.title,
|
||||||
}
|
}
|
||||||
: initialFormData;
|
: createInitialFormData();
|
||||||
dialogType.value = type;
|
dialogType.value = type;
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
},
|
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
:title="`${$t(`button.${dialogType}`)}${$t('bot.chatAssistant')}`"
|
:confirm-loading="loading"
|
||||||
draggable
|
:confirm-text="$t('button.save')"
|
||||||
align-center
|
:description="dialogDescription"
|
||||||
|
:submitting="loading"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="lg"
|
||||||
|
@confirm="handleSubmit"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
|
<ElForm
|
||||||
<ElFormItem :label="$t('common.avatar')" prop="icon">
|
ref="formRef"
|
||||||
<UploadAvatar v-model="formData.icon" />
|
:model="formData"
|
||||||
</ElFormItem>
|
:rules="rules"
|
||||||
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
|
class="bot-modal-form"
|
||||||
<DictSelect v-model="formData.categoryId" dict-code="aiBotCategory" />
|
label-position="top"
|
||||||
|
status-icon
|
||||||
|
>
|
||||||
|
<section class="bot-modal-section">
|
||||||
|
<header class="bot-modal-section__header">
|
||||||
|
<div>
|
||||||
|
<h4 class="bot-modal-section__title">
|
||||||
|
{{ $t('bot.modal.sectionAppearanceTitle') }}
|
||||||
|
</h4>
|
||||||
|
<p class="bot-modal-section__description">
|
||||||
|
{{ $t('bot.modal.sectionAppearanceDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bot-modal-appearance-grid">
|
||||||
|
<ElFormItem
|
||||||
|
class="bot-modal-avatar-field"
|
||||||
|
:label="$t('common.avatar')"
|
||||||
|
prop="icon"
|
||||||
|
>
|
||||||
|
<UploadAvatar
|
||||||
|
v-model="formData.icon"
|
||||||
|
:size="88"
|
||||||
|
theme="soft-panel"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<div class="bot-modal-appearance-fields">
|
||||||
<ElFormItem :label="$t('aiWorkflow.title')" prop="title">
|
<ElFormItem :label="$t('aiWorkflow.title')" prop="title">
|
||||||
<ElInput v-model="formData.title" />
|
<ElInput v-model="formData.title" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem :label="$t('plugin.alias')" prop="alias">
|
|
||||||
<ElInput v-model="formData.alias" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem :label="$t('plugin.description')" prop="description">
|
|
||||||
<ElInput type="textarea" :rows="3" v-model="formData.description" />
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem prop="status" :label="$t('aiWorkflow.status')">
|
|
||||||
<DictSelect v-model="formData.status" dict-code="showOrNot" />
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
<template #footer>
|
<ElFormItem :label="$t('aiWorkflow.categoryId')" prop="categoryId">
|
||||||
<ElButton @click="dialogVisible = false">
|
<DictSelect
|
||||||
{{ $t('button.cancel') }}
|
v-model="formData.categoryId"
|
||||||
</ElButton>
|
dict-code="aiBotCategory"
|
||||||
<ElButton
|
/>
|
||||||
type="primary"
|
</ElFormItem>
|
||||||
:loading="loading"
|
|
||||||
:disabled="loading"
|
<ElFormItem
|
||||||
@click="handleSubmit"
|
class="bot-modal-grid__full"
|
||||||
|
:label="$t('plugin.alias')"
|
||||||
|
prop="alias"
|
||||||
>
|
>
|
||||||
{{ $t('button.save') }}
|
<ElInput v-model="formData.alias" />
|
||||||
</ElButton>
|
<div class="bot-modal-field-tip">
|
||||||
</template>
|
{{ $t('bot.modal.aliasHint') }}
|
||||||
</ElDialog>
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bot-modal-section bot-modal-section--compact">
|
||||||
|
<header class="bot-modal-section__header">
|
||||||
|
<div>
|
||||||
|
<h4 class="bot-modal-section__title">
|
||||||
|
{{ $t('bot.modal.sectionPublishTitle') }}
|
||||||
|
</h4>
|
||||||
|
<p class="bot-modal-section__description">
|
||||||
|
{{ $t('bot.modal.sectionPublishDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bot-modal-publish-grid">
|
||||||
|
<ElFormItem :label="$t('plugin.description')" prop="description">
|
||||||
|
<ElInput v-model="formData.description" :rows="3" type="textarea" />
|
||||||
|
<div class="bot-modal-field-tip">
|
||||||
|
{{ $t('bot.modal.descriptionHint') }}
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem
|
||||||
|
class="bot-modal-publish-field"
|
||||||
|
:label="$t('aiWorkflow.status')"
|
||||||
|
prop="status"
|
||||||
|
>
|
||||||
|
<DictSelect v-model="formData.status" dict-code="showOrNot" />
|
||||||
|
<div class="bot-modal-field-tip">
|
||||||
|
{{ $t('bot.modal.statusHint') }}
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bot-modal-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-section {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid hsl(var(--modal-divider));
|
||||||
|
background: hsl(var(--modal-content-surface-strong));
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-section--compact {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-section__header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-section__title {
|
||||||
|
margin: 0;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-section__description {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-appearance-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 112px minmax(0, 1fr);
|
||||||
|
gap: 14px 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-appearance-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-grid__full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-publish-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 220px);
|
||||||
|
gap: 14px 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-publish-field {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-avatar-field {
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-avatar-field :deep(.el-form-item__content) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-field-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-form-item__label) {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-input__wrapper),
|
||||||
|
.bot-modal-form :deep(.el-select__wrapper),
|
||||||
|
.bot-modal-form :deep(.el-textarea__inner) {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: hsl(var(--input-background));
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--input) / 0.92);
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-input__wrapper),
|
||||||
|
.bot-modal-form :deep(.el-select__wrapper) {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-input__wrapper:hover),
|
||||||
|
.bot-modal-form :deep(.el-select__wrapper:hover),
|
||||||
|
.bot-modal-form :deep(.el-textarea__inner:hover) {
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-input__wrapper.is-focus),
|
||||||
|
.bot-modal-form :deep(.el-select__wrapper.is-focused),
|
||||||
|
.bot-modal-form :deep(.el-textarea__inner:focus) {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(var(--primary) / 0.72),
|
||||||
|
0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-textarea__inner) {
|
||||||
|
min-height: 88px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-modal-form :deep(.el-form-item.is-error .el-input__wrapper),
|
||||||
|
.bot-modal-form :deep(.el-form-item.is-error .el-select__wrapper),
|
||||||
|
.bot-modal-form :deep(.el-form-item.is-error .el-textarea__inner) {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(var(--destructive) / 0.8),
|
||||||
|
0 0 0 4px hsl(var(--destructive) / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bot-modal-appearance-grid,
|
||||||
|
.bot-modal-appearance-fields,
|
||||||
|
.bot-modal-publish-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { ElButton, ElDialog, ElForm, ElFormItem, ElInput } from 'element-plus';
|
import { ElButton, ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||||
|
|
||||||
import { sseClient } from '#/api/request';
|
import { sseClient } from '#/api/request';
|
||||||
|
|
||||||
@@ -58,14 +59,21 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
:title="$t('bot.aiOptimizedPrompts')"
|
:title="$t('bot.aiOptimizedPrompts')"
|
||||||
draggable
|
|
||||||
align-center
|
|
||||||
width="550px"
|
width="550px"
|
||||||
|
:centered="true"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="formData" :rules="rules">
|
|
||||||
<ElFormItem prop="prompt">
|
<ElFormItem prop="prompt">
|
||||||
<ElInput type="textarea" :rows="20" v-model="formData.prompt" />
|
<ElInput type="textarea" :rows="20" v-model="formData.prompt" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -87,5 +95,5 @@ defineExpose({
|
|||||||
{{ loading ? $t('button.optimizing') : $t('button.regenerate') }}
|
{{ loading ? $t('button.optimizing') : $t('button.regenerate') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { Delete, MoreFilled } from '@element-plus/icons-vue';
|
import { Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDialog,
|
|
||||||
ElDropdown,
|
ElDropdown,
|
||||||
ElDropdownItem,
|
ElDropdownItem,
|
||||||
ElDropdownMenu,
|
ElDropdownMenu,
|
||||||
@@ -129,30 +129,27 @@ const form = ref({
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
<ElDialog v-model="dialogVisible" :title="$t('button.edit')" width="50%">
|
<EasyFlowFormModal
|
||||||
|
v-model:open="dialogVisible"
|
||||||
|
:closable="!btnLoading"
|
||||||
|
:title="$t('button.edit')"
|
||||||
|
width="50%"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="basicFormRef"
|
ref="basicFormRef"
|
||||||
style="width: 100%; margin-top: 20px"
|
|
||||||
:model="form"
|
:model="form"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<ElInput v-model="form.content" :rows="20" type="textarea" />
|
<ElInput v-model="form.content" :rows="20" type="textarea" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {FormInstance} from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
|
import type { ActionButton } from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
|
import { Delete, Edit, Notebook, Plus, Search } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -10,21 +18,12 @@ import {
|
|||||||
ElMessage,
|
ElMessage,
|
||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import type {ActionButton} from '#/components/page/CardList.vue';
|
import { api } from '#/api/request';
|
||||||
import CardPage from '#/components/page/CardList.vue';
|
|
||||||
|
|
||||||
import {computed, onMounted, ref} from 'vue';
|
|
||||||
import {useRouter} from 'vue-router';
|
|
||||||
|
|
||||||
import {$t} from '@easyflow/locales';
|
|
||||||
|
|
||||||
import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue';
|
|
||||||
import {tryit} from 'radash';
|
|
||||||
|
|
||||||
import {api} from '#/api/request';
|
|
||||||
import defaultIcon from '#/assets/ai/knowledge/book.svg';
|
import defaultIcon from '#/assets/ai/knowledge/book.svg';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import CardPage from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||||
@@ -321,16 +320,21 @@ function changeCategory(category: any) {
|
|||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
:closable="!saveLoading"
|
||||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="saveLoading"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
|
:submitting="saveLoading"
|
||||||
|
@confirm="handleSubmit"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="formRules"
|
:rules="formRules"
|
||||||
label-width="120px"
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<!-- 动态生成表单项 -->
|
<!-- 动态生成表单项 -->
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
@@ -352,16 +356,7 @@ function changeCategory(category: any) {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="dialogVisible = false">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
|
|
||||||
<!-- 新增知识库模态框-->
|
<!-- 新增知识库模态框-->
|
||||||
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {FormInstance} from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -14,14 +18,10 @@ import {
|
|||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import {onMounted, ref} from 'vue';
|
import { api } from '#/api/request';
|
||||||
|
|
||||||
import {InfoFilled} from '@element-plus/icons-vue';
|
|
||||||
|
|
||||||
import {api} from '#/api/request';
|
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||||
import {$t} from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
const embeddingLlmList = ref<any>([]);
|
const embeddingLlmList = ref<any>([]);
|
||||||
@@ -93,7 +93,7 @@ const defaultEntity = {
|
|||||||
const normalizeEntity = (raw: any = {}) => {
|
const normalizeEntity = (raw: any = {}) => {
|
||||||
const options = {
|
const options = {
|
||||||
canUpdateEmbeddingModel: true,
|
canUpdateEmbeddingModel: true,
|
||||||
...(raw.options || {}),
|
...raw.options,
|
||||||
};
|
};
|
||||||
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
||||||
options.rerankEnable = !!raw.rerankModelId;
|
options.rerankEnable = !!raw.rerankModelId;
|
||||||
@@ -203,20 +203,24 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:centered="true"
|
||||||
align-center
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="xl"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="150px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="icon"
|
prop="icon"
|
||||||
@@ -413,20 +417,7 @@ defineExpose({
|
|||||||
<ElSwitch v-model="entity.searchEngineEnable" />
|
<ElSwitch v-model="entity.searchEngineEnable" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import {
|
import { ElForm, ElFormItem, ElInput, ElTreeSelect } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElTreeSelect,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -97,14 +91,22 @@ const handleSubmit = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
:model-value="modelValue"
|
:open="modelValue"
|
||||||
:title="title"
|
:title="title"
|
||||||
width="520px"
|
width="520px"
|
||||||
:close-on-click-modal="false"
|
:confirm-text="$t('button.save')"
|
||||||
@close="closeDialog"
|
@update:open="emit('update:modelValue', $event)"
|
||||||
|
@cancel="closeDialog"
|
||||||
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-position="top">
|
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
:label="$t('documentCollection.faq.categoryName')"
|
:label="$t('documentCollection.faq.categoryName')"
|
||||||
prop="categoryName"
|
prop="categoryName"
|
||||||
@@ -130,12 +132,5 @@ const handleSubmit = () => {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="closeDialog">{{ $t('button.cancel') }}</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit">
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import type { IDomEditor } from '@wangeditor/editor';
|
|||||||
|
|
||||||
import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
|
import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -189,15 +188,20 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
class="faq-edit-dialog"
|
class="faq-edit-dialog"
|
||||||
:model-value="modelValue"
|
:open="modelValue"
|
||||||
:title="form.id ? $t('button.edit') : $t('button.add')"
|
:title="form.id ? $t('button.edit') : $t('button.add')"
|
||||||
width="min(920px, 92vw)"
|
width="min(920px, 92vw)"
|
||||||
:close-on-click-modal="false"
|
:confirm-text="$t('button.save')"
|
||||||
@close="closeDialog"
|
@update:open="emit('update:modelValue', $event)"
|
||||||
|
@cancel="closeDialog"
|
||||||
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
class="faq-form easyflow-modal-form easyflow-modal-form--compact"
|
||||||
|
label-position="top"
|
||||||
>
|
>
|
||||||
<ElForm class="faq-form" label-position="top">
|
|
||||||
<ElFormItem :label="$t('documentCollection.faq.category')">
|
<ElFormItem :label="$t('documentCollection.faq.category')">
|
||||||
<ElTreeSelect
|
<ElTreeSelect
|
||||||
v-model="form.categoryId"
|
v-model="form.categoryId"
|
||||||
@@ -233,17 +237,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<div class="dialog-footer">
|
|
||||||
<ElButton class="footer-btn" @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton class="footer-btn" type="primary" @click="handleSubmit">
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import type { UploadFile } from 'element-plus';
|
|||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleCloseFilled,
|
CircleCloseFilled,
|
||||||
Document,
|
Document,
|
||||||
@@ -16,7 +18,6 @@ import {
|
|||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDialog,
|
|
||||||
ElIcon,
|
ElIcon,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
ElTable,
|
ElTable,
|
||||||
@@ -128,12 +129,15 @@ const handleImport = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
width="min(980px, 92vw)"
|
width="min(980px, 92vw)"
|
||||||
:title="$t('documentCollection.faq.import.title')"
|
:title="$t('documentCollection.faq.import.title')"
|
||||||
:close-on-click-modal="true"
|
:close-on-click-modal="true"
|
||||||
@close="closeDialog"
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
@cancel="closeDialog"
|
||||||
>
|
>
|
||||||
<div class="faq-import-dialog">
|
<div class="faq-import-dialog">
|
||||||
<ElUpload
|
<ElUpload
|
||||||
@@ -148,12 +152,18 @@ const handleImport = async () => {
|
|||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
class="faq-upload-area"
|
class="faq-upload-area"
|
||||||
>
|
>
|
||||||
<div class="upload-box flex flex-col items-center justify-center p-8 text-center">
|
<div
|
||||||
<ElIcon class="text-4xl text-gray-400 mb-4"><UploadFilled /></ElIcon>
|
class="upload-box flex flex-col items-center justify-center p-8 text-center"
|
||||||
<div class="upload-title text-[15px] font-semibold text-[var(--el-text-color-primary)] mb-2">
|
>
|
||||||
|
<ElIcon class="mb-4 text-4xl text-gray-400"><UploadFilled /></ElIcon>
|
||||||
|
<div
|
||||||
|
class="upload-title mb-2 text-[15px] font-semibold text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ $t('documentCollection.faq.import.uploadTitle') }}
|
{{ $t('documentCollection.faq.import.uploadTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-desc text-[13px] text-[var(--el-text-color-secondary)]">
|
<div
|
||||||
|
class="upload-desc text-[13px] text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
{{ $t('documentCollection.faq.import.uploadDesc') }}
|
{{ $t('documentCollection.faq.import.uploadDesc') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +193,10 @@ const handleImport = async () => {
|
|||||||
>
|
>
|
||||||
<WarningFilled />
|
<WarningFilled />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<ElIcon v-else class="result-state-icon text-[var(--el-color-success)]">
|
<ElIcon
|
||||||
|
v-else
|
||||||
|
class="result-state-icon text-[var(--el-color-success)]"
|
||||||
|
>
|
||||||
<SuccessFilled />
|
<SuccessFilled />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<span class="result-title-text">
|
<span class="result-title-text">
|
||||||
@@ -193,15 +206,23 @@ const handleImport = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="result-stats">
|
<div class="result-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-label">{{ $t('documentCollection.faq.import.totalCount') }}</div>
|
<div class="stat-label">
|
||||||
|
{{ $t('documentCollection.faq.import.totalCount') }}
|
||||||
|
</div>
|
||||||
<div class="stat-value">{{ importResult.totalCount || 0 }}</div>
|
<div class="stat-value">{{ importResult.totalCount || 0 }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-label">{{ $t('documentCollection.faq.import.successCount') }}</div>
|
<div class="stat-label">
|
||||||
<div class="stat-value success-text">{{ importResult.successCount || 0 }}</div>
|
{{ $t('documentCollection.faq.import.successCount') }}
|
||||||
|
</div>
|
||||||
|
<div class="stat-value success-text">
|
||||||
|
{{ importResult.successCount || 0 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-label">{{ $t('documentCollection.faq.import.errorCount') }}</div>
|
<div class="stat-label">
|
||||||
|
{{ $t('documentCollection.faq.import.errorCount') }}
|
||||||
|
</div>
|
||||||
<div class="stat-value" :class="hasErrors ? 'danger-text' : ''">
|
<div class="stat-value" :class="hasErrors ? 'danger-text' : ''">
|
||||||
{{ importResult.errorCount || 0 }}
|
{{ importResult.errorCount || 0 }}
|
||||||
</div>
|
</div>
|
||||||
@@ -263,7 +284,7 @@ const handleImport = async () => {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
@@ -107,12 +108,14 @@ const refreshLoadingMap = ref<Record<number | string, boolean>>({});
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<McpModal ref="saveDialog" @reload="reset" />
|
<McpModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleHeaderButtonClick"
|
@button-click="handleHeaderButtonClick"
|
||||||
/>
|
/>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
</template>
|
||||||
<PageData ref="pageDataRef" page-url="/api/v1/mcp/page" :page-size="10">
|
<PageData ref="pageDataRef" page-url="/api/v1/mcp/page" :page-size="10">
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable :data="pageList" border>
|
<ElTable :data="pageList" border>
|
||||||
@@ -198,7 +201,7 @@ const refreshLoadingMap = ref<Record<number | string, boolean>>({});
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -149,21 +149,25 @@ const activeName = ref('config');
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="xl"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElTabs v-model="activeName" class="demo-tabs">
|
<ElTabs v-model="activeName" class="demo-tabs">
|
||||||
<ElTabPane :label="$t('mcp.modal.config')" name="config">
|
<ElTabPane :label="$t('mcp.modal.config')" name="config">
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="title" :label="$t('mcp.title')">
|
<ElFormItem prop="title" :label="$t('mcp.title')">
|
||||||
<ElInput v-model.trim="entity.title" />
|
<ElInput v-model.trim="entity.title" />
|
||||||
@@ -176,7 +180,8 @@ const activeName = ref('config');
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="15"
|
:rows="15"
|
||||||
v-model.trim="entity.configJson"
|
v-model.trim="entity.configJson"
|
||||||
:placeholder="$t('mcp.example') + jsonPlaceholder" />
|
:placeholder="$t('mcp.example') + jsonPlaceholder"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="status" :label="$t('mcp.status')">
|
<ElFormItem prop="status" :label="$t('mcp.status')">
|
||||||
<ElSwitch v-model="entity.status" />
|
<ElSwitch v-model="entity.status" />
|
||||||
@@ -249,20 +254,7 @@ const activeName = ref('config');
|
|||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
</div>
|
</div>
|
||||||
</ElTabs>
|
</ElTabs>
|
||||||
<template #footer>
|
</EasyFlowPanelModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability
|
|||||||
|
|
||||||
import { reactive, ref, watch } from 'vue';
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage, ElTag } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
ElTag,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -274,21 +268,25 @@ const showMoreFields = ref(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
align-center
|
|
||||||
width="482"
|
width="482"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="100px"
|
|
||||||
ref="formDataRef"
|
ref="formDataRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="title" :label="$t('llm.title')">
|
<ElFormItem prop="title" :label="$t('llm.title')">
|
||||||
<ElInput v-model.trim="formData.title" />
|
<ElInput v-model.trim="formData.title" />
|
||||||
@@ -340,20 +338,7 @@ const showMoreFields = ref(false);
|
|||||||
<ElInput v-model.trim="formData.requestPath" />
|
<ElInput v-model.trim="formData.requestPath" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -111,21 +111,25 @@ const handleChangeProvider = (val: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
align-center
|
|
||||||
width="482"
|
width="482"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="100px"
|
|
||||||
ref="formDataRef"
|
ref="formDataRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="icon"
|
prop="icon"
|
||||||
@@ -138,7 +142,10 @@ const handleChangeProvider = (val: string) => {
|
|||||||
<ElInput v-model.trim="formData.providerName" />
|
<ElInput v-model.trim="formData.providerName" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="provider" :label="$t('llmProvider.apiType')">
|
<ElFormItem prop="provider" :label="$t('llmProvider.apiType')">
|
||||||
<ElSelect v-model="formData.providerType" @change="handleChangeProvider">
|
<ElSelect
|
||||||
|
v-model="formData.providerType"
|
||||||
|
@change="handleChangeProvider"
|
||||||
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in providerOptions"
|
v-for="item in providerOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -163,20 +170,7 @@ const handleChangeProvider = (val: string) => {
|
|||||||
<ElInput v-model.trim="formData.embedPath" />
|
<ElInput v-model.trim="formData.embedPath" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, reactive, ref } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CirclePlus,
|
CirclePlus,
|
||||||
Loading,
|
Loading,
|
||||||
@@ -10,7 +12,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
ElCollapse,
|
ElCollapse,
|
||||||
ElCollapseItem,
|
ElCollapseItem,
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -215,14 +216,13 @@ const handleRefresh = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
:title="`${providerInfo?.providerName}${$t('llmProvider.model')}`"
|
:title="`${providerInfo?.providerName}${$t('llmProvider.model')}`"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
align-center
|
|
||||||
width="762"
|
width="762"
|
||||||
|
:show-footer="false"
|
||||||
>
|
>
|
||||||
<div class="manage-llm-container">
|
<div class="manage-llm-container">
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
@@ -311,7 +311,7 @@ const handleRefresh = () => {
|
|||||||
</ElTabs>
|
</ElTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
@@ -65,9 +65,6 @@ const save = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
const closeDialog = () => {
|
|
||||||
dialogVisible.value = false;
|
|
||||||
};
|
|
||||||
const getModelInfo = (id: string) => {
|
const getModelInfo = (id: string) => {
|
||||||
options.value.forEach((item: any) => {
|
options.value.forEach((item: any) => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
@@ -78,15 +75,25 @@ const getModelInfo = (id: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
|
:closable="!btnLoading"
|
||||||
:title="$t('llm.verifyLlmTitle')"
|
:title="$t('llm.verifyLlmTitle')"
|
||||||
:close-on-click-modal="false"
|
|
||||||
align-center
|
|
||||||
width="482"
|
width="482"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="formDataRef"
|
||||||
|
:model="formData"
|
||||||
|
status-icon
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElForm ref="formDataRef" :model="formData" status-icon :rules="rules">
|
|
||||||
<ElFormItem prop="llmId" :label="$t('llm.modelToBeTested')">
|
<ElFormItem prop="llmId" :label="$t('llm.modelToBeTested')">
|
||||||
<ElSelect v-model="formData.llmId" @change="getModelInfo">
|
<ElSelect v-model="formData.llmId" @change="getModelInfo">
|
||||||
<ElOption
|
<ElOption
|
||||||
@@ -105,20 +112,7 @@ const getModelInfo = (id: string) => {
|
|||||||
{{ vectorDimension }}
|
{{ vectorDimension }}
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import { Plus, Remove } from '@element-plus/icons-vue';
|
import { Plus, Remove } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -156,20 +156,24 @@ function removeHeader(index: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
align-center
|
align-center
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="lg"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="150px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="icon"
|
prop="icon"
|
||||||
@@ -251,20 +255,7 @@ function removeHeader(index: number) {
|
|||||||
<ElInput v-model.trim="entity.tokenValue" />
|
<ElInput v-model.trim="entity.tokenValue" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -150,21 +145,24 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="600px"
|
width="600px"
|
||||||
@closed="closeDialog"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="80px"
|
|
||||||
status-icon
|
status-icon
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem :label="$t('pluginItem.name')" prop="name">
|
<ElFormItem :label="$t('pluginItem.name')" prop="name">
|
||||||
<ElInput v-model.trim="entity.name" />
|
<ElInput v-model.trim="entity.name" />
|
||||||
@@ -176,21 +174,7 @@ defineExpose({
|
|||||||
<ElInput v-model.trim="entity.description" type="textarea" :rows="4" />
|
<ElInput v-model.trim="entity.description" type="textarea" :rows="4" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="closeDialog" :disabled="btnLoading">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
@click="save"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
@@ -87,16 +87,25 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
|
:closable="!btnLoading"
|
||||||
width="500px"
|
width="500px"
|
||||||
:title="$t('plugin.placeholder.categorize')"
|
:title="$t('plugin.placeholder.categorize')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
align-center
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="saveForm"
|
||||||
|
:model="entity"
|
||||||
|
status-icon
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElForm ref="saveForm" :model="entity" status-icon>
|
|
||||||
<ElFormItem prop="authType" :label="$t('plugin.category')">
|
<ElFormItem prop="authType" :label="$t('plugin.category')">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="entity.categoryIds"
|
v-model="entity.categoryIds"
|
||||||
@@ -114,20 +123,7 @@ function closeDialog() {
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import type { ActionButton } from '#/components/page/CardList.vue';
|
|||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import { Delete, Edit, Plus } from '@element-plus/icons-vue';
|
import { Delete, Edit, Plus } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -276,27 +275,24 @@ const handleClickCategory = (item: PluginCategory) => {
|
|||||||
</div>
|
</div>
|
||||||
<AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" />
|
<AddPluginModal ref="aiPluginModalRef" @reload="handleSearch" />
|
||||||
<CategoryPluginModal ref="categoryCategoryModal" @reload="handleSearch" />
|
<CategoryPluginModal ref="categoryCategoryModal" @reload="handleSearch" />
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="isEdit ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
width="500px"
|
width="500px"
|
||||||
:close-on-click-modal="false"
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
:model="formData"
|
||||||
|
status-icon
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElForm :model="formData" status-icon>
|
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<ElInput v-model.trim="formData.name" />
|
<ElInput v-model.trim="formData.name" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="dialogVisible = false">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit">
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
|
|
||||||
import { VideoPlay } from '@element-plus/icons-vue';
|
import { VideoPlay } from '@element-plus/icons-vue';
|
||||||
import { ElButton, ElDialog, ElMenu, ElMenuItem } from 'element-plus';
|
import { ElButton, ElMenu, ElMenuItem } from 'element-plus';
|
||||||
import { JsonViewer } from 'vue3-json-viewer';
|
import { JsonViewer } from 'vue3-json-viewer';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
@@ -77,14 +78,13 @@ function handleSubmitRun() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="80%"
|
width="80%"
|
||||||
align-center
|
align-center
|
||||||
class="run-test-dialog"
|
class="run-test-dialog"
|
||||||
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
||||||
|
:before-close="() => (dialogVisible = false)"
|
||||||
>
|
>
|
||||||
<div class="run-test-container">
|
<div class="run-test-container">
|
||||||
<div class="run-test-params">
|
<div class="run-test-params">
|
||||||
@@ -147,7 +147,7 @@ function handleSubmitRun() {
|
|||||||
{{ $t('pluginItem.pluginToolEdit.run') }}
|
{{ $t('pluginItem.pluginToolEdit.run') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
import { ElButton, ElDialog } from 'element-plus';
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -40,14 +42,11 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="$t('aiResource.choose')"
|
:title="$t('aiResource.choose')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="80%"
|
width="80%"
|
||||||
destroy-on-close
|
|
||||||
>
|
>
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
@@ -67,7 +66,7 @@ watch(
|
|||||||
{{ $t('button.confirm') }}
|
{{ $t('button.confirm') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
<ElButton @click="openDialog()">
|
<ElButton @click="openDialog()">
|
||||||
{{ $t('button.choose') }}
|
{{ $t('button.choose') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { ElDialog, ElImage } from 'element-plus';
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
|
import { ElImage } from 'element-plus';
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog,
|
openDialog,
|
||||||
@@ -18,13 +20,12 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="$t('message.preview')"
|
:title="$t('message.preview')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
width="md"
|
||||||
destroy-on-close
|
:show-footer="false"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<ElImage
|
<ElImage
|
||||||
@@ -41,7 +42,7 @@ function closeDialog() {
|
|||||||
{{ $t('message.notAudio') }}
|
{{ $t('message.notAudio') }}
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { formatBytes } from '@easyflow/utils';
|
import { formatBytes } from '@easyflow/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
ElAvatar,
|
ElAvatar,
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDialog,
|
|
||||||
ElDropdown,
|
ElDropdown,
|
||||||
ElDropdownItem,
|
ElDropdownItem,
|
||||||
ElDropdownMenu,
|
ElDropdownMenu,
|
||||||
@@ -418,16 +418,21 @@ const getSideList = async () => {
|
|||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="sideDialogVisible"
|
v-model:open="sideDialogVisible"
|
||||||
|
:closable="!sideSaveLoading"
|
||||||
:title="sideFormData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="sideFormData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="sideSaveLoading"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
|
:submitting="sideSaveLoading"
|
||||||
|
@confirm="handleSideSubmit"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="sideFormRef"
|
ref="sideFormRef"
|
||||||
:model="sideFormData"
|
:model="sideFormData"
|
||||||
:rules="sideFormRules"
|
:rules="sideFormRules"
|
||||||
label-width="120px"
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<!-- 动态生成表单项 -->
|
<!-- 动态生成表单项 -->
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
@@ -449,19 +454,6 @@ const getSideList = async () => {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="sideDialogVisible = false">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="handleSideSubmit"
|
|
||||||
:loading="sideSaveLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,16 +3,10 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { getResourceType } from '@easyflow/utils';
|
import { getResourceType } from '@easyflow/utils';
|
||||||
|
|
||||||
import {
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -116,19 +110,23 @@ function uploadSuccess(res: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="md"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="resourceUrl" :label="$t('aiResource.resourceUrl')">
|
<ElFormItem prop="resourceUrl" :label="$t('aiResource.resourceUrl')">
|
||||||
<Upload @before-upload="beforeUpload" @success="uploadSuccess" />
|
<Upload @before-upload="beforeUpload" @success="uploadSuccess" />
|
||||||
@@ -149,20 +147,7 @@ function uploadSuccess(res: any) {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { ActionButton } from '#/components/page/CardList.vue';
|
|||||||
|
|
||||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CopyDocument,
|
CopyDocument,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -16,8 +18,6 @@ import {
|
|||||||
VideoPlay,
|
VideoPlay,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -404,16 +404,21 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
|
:closable="!saveLoading"
|
||||||
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="saveLoading"
|
||||||
|
:confirm-text="$t('button.confirm')"
|
||||||
|
:submitting="saveLoading"
|
||||||
|
@confirm="handleSubmit"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="formRules"
|
:rules="formRules"
|
||||||
label-width="120px"
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<!-- 动态生成表单项 -->
|
<!-- 动态生成表单项 -->
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
@@ -435,16 +440,7 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="dialogVisible = false">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
|
|
||||||
{{ $t('button.confirm') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import type { FormInstance, UploadInstance, UploadProps } from 'element-plus';
|
|||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage, ElUpload } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
ElUpload,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -148,9 +142,8 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="
|
:title="
|
||||||
isImport
|
isImport
|
||||||
? $t('button.import')
|
? $t('button.import')
|
||||||
@@ -159,14 +152,19 @@ function closeDialog() {
|
|||||||
: $t('button.edit')
|
: $t('button.edit')
|
||||||
"
|
"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="xl"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="isImport ? { ...entity, jsonFile: jsonFileModel } : entity"
|
:model="isImport ? { ...entity, jsonFile: jsonFileModel } : entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem v-if="isImport" prop="jsonFile" label="JSON文件" required>
|
<ElFormItem v-if="isImport" prop="jsonFile" label="JSON文件" required>
|
||||||
<ElUpload
|
<ElUpload
|
||||||
@@ -212,20 +210,7 @@ function closeDialog() {
|
|||||||
<DictSelect v-model="entity.status" dict-code="showOrNot" />
|
<DictSelect v-model="entity.status" dict-code="showOrNot" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { ElDialog } from 'element-plus';
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import SysApiKeyList from '#/views/config/apikey/SysApiKeyList.vue';
|
import SysApiKeyList from '#/views/config/apikey/SysApiKeyList.vue';
|
||||||
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
||||||
@@ -11,14 +11,14 @@ const dialogVisible = ref(false);
|
|||||||
<template>
|
<template>
|
||||||
<div class="sys-apikey-container">
|
<div class="sys-apikey-container">
|
||||||
<SysApiKeyList />
|
<SysApiKeyList />
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:centered="true"
|
||||||
:close-on-click-modal="false"
|
width="xl"
|
||||||
align-center
|
:show-footer="false"
|
||||||
>
|
>
|
||||||
<SysApiKeyResourcePermissionList />
|
<SysApiKeyResourcePermissionList />
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { markRaw, ref } from 'vue';
|
import { markRaw, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDialog,
|
|
||||||
ElDropdown,
|
ElDropdown,
|
||||||
ElDropdownItem,
|
ElDropdownItem,
|
||||||
ElDropdownMenu,
|
ElDropdownMenu,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
import SysApiKeyResourcePermissionList from '#/views/config/apikey/SysApiKeyResourcePermissionList.vue';
|
||||||
@@ -112,13 +114,14 @@ function addNewApiKey() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysApiKeyModal ref="saveDialog" @reload="reset" />
|
<SysApiKeyModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="headerButtonClick"
|
@button-click="headerButtonClick"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysApiKey/page"
|
page-url="/api/v1/sysApiKey/page"
|
||||||
@@ -188,14 +191,15 @@ function addNewApiKey() {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
:title="$t('sysApiKeyResourcePermission.addPermission')"
|
:title="$t('sysApiKeyResourcePermission.addPermission')"
|
||||||
draggable
|
:centered="true"
|
||||||
:close-on-click-modal="false"
|
width="xl"
|
||||||
|
:show-footer="false"
|
||||||
>
|
>
|
||||||
<SysApiKeyResourcePermissionList />
|
<SysApiKeyResourcePermissionList />
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElAlert,
|
ElAlert,
|
||||||
ElButton,
|
|
||||||
ElCheckbox,
|
ElCheckbox,
|
||||||
ElCheckboxGroup,
|
ElCheckboxGroup,
|
||||||
ElDatePicker,
|
ElDatePicker,
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
@@ -157,21 +157,23 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
width="50%"
|
width="50%"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
class="form-container"
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact form-container"
|
||||||
>
|
>
|
||||||
<!-- 状态选择 -->
|
<!-- 状态选择 -->
|
||||||
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
||||||
@@ -217,21 +219,7 @@ defineExpose({
|
|||||||
</ElCheckboxGroup>
|
</ElCheckboxGroup>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</EasyFlowFormModal>
|
||||||
<template #footer>
|
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
@@ -77,7 +78,9 @@ function remove(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container !m-0 !pl-0">
|
<div class="page-container !m-0 !pl-0">
|
||||||
<SysApiKeyResourcePermissionModal ref="saveDialog" @reload="reset" />
|
<SysApiKeyResourcePermissionModal ref="saveDialog" @reload="reset" />
|
||||||
<div class="flex items-center justify-between">
|
<ListPageShell dense>
|
||||||
|
<template #filters>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
<ElForm ref="formRef" :inline="true" :model="formInline">
|
||||||
<ElFormItem prop="title" class="!mr-3">
|
<ElFormItem prop="title" class="!mr-3">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -94,7 +97,6 @@ function remove(row: any) {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<div class="mb-5">
|
|
||||||
<ElButton @click="showDialog({})" type="primary">
|
<ElButton @click="showDialog({})" type="primary">
|
||||||
<ElIcon class="mr-1">
|
<ElIcon class="mr-1">
|
||||||
<Plus />
|
<Plus />
|
||||||
@@ -102,7 +104,7 @@ function remove(row: any) {
|
|||||||
{{ $t('button.add') }}
|
{{ $t('button.add') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysApiKeyResourcePermission/page"
|
page-url="/api/v1/sysApiKeyResourcePermission/page"
|
||||||
@@ -152,6 +154,7 @@ function remove(row: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -73,19 +68,23 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="requestInterface"
|
prop="requestInterface"
|
||||||
@@ -97,20 +96,7 @@ function closeDialog() {
|
|||||||
<ElInput v-model.trim="entity.title" />
|
<ElInput v-model.trim="entity.title" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDatePicker,
|
import { ElDatePicker, ElForm, ElFormItem, ElMessage } from 'element-plus';
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
// vue
|
|
||||||
onMounted(() => {});
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog,
|
openDialog,
|
||||||
});
|
});
|
||||||
@@ -74,35 +67,26 @@ function closeDialog() {
|
|||||||
entity.value = {};
|
entity.value = {};
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
const resourcePermissionList = ref([]);
|
|
||||||
function getResourcePermissionList() {
|
|
||||||
api.get('/api/v1/sysApiKeyResourcePermission/list').then((res) => {
|
|
||||||
console.log('资源');
|
|
||||||
console.log(res);
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
resourcePermissionList.value = res.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
|
||||||
getResourcePermissionList();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
|
||||||
<DictSelect v-model="entity.status" dict-code="dataStatus" />
|
<DictSelect v-model="entity.status" dict-code="dataStatus" />
|
||||||
@@ -111,20 +95,7 @@ onMounted(() => {
|
|||||||
<ElDatePicker v-model="entity.expiredAt" type="datetime" />
|
<ElDatePicker v-model="entity.expiredAt" type="datetime" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,15 +3,10 @@ import type { UploadFile } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||||
|
|
||||||
import {
|
import { ElButton, ElMessage, ElMessageBox, ElUpload } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElUpload,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import uploadIcon from '#/assets/datacenter/upload.png';
|
import uploadIcon from '#/assets/datacenter/upload.png';
|
||||||
@@ -99,12 +94,13 @@ function handleUpload() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowPanelModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="props.title"
|
:title="props.title"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
>
|
>
|
||||||
<ElUpload
|
<ElUpload
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
@@ -148,7 +144,7 @@ function handleUpload() {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
@@ -95,13 +96,14 @@ function toDetailPage(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<DatacenterTableModal ref="saveDialog" @reload="reset" />
|
<DatacenterTableModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({})"
|
@button-click="showDialog({})"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/datacenterTable/page"
|
page-url="/api/v1/datacenterTable/page"
|
||||||
@@ -168,7 +170,7 @@ function toDetailPage(row: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import { Plus } from '@element-plus/icons-vue';
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -184,20 +185,24 @@ function getYesOrNo() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="800px"
|
width="800px"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="100px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="tableName" :label="$t('datacenterTable.tableName')">
|
<ElFormItem prop="tableName" :label="$t('datacenterTable.tableName')">
|
||||||
<ElInput :disabled="!isAdd" v-model.trim="entity.tableName" />
|
<ElInput :disabled="!isAdd" v-model.trim="entity.tableName" />
|
||||||
@@ -205,7 +210,7 @@ function getYesOrNo() {
|
|||||||
<ElFormItem prop="tableDesc" :label="$t('datacenterTable.tableDesc')">
|
<ElFormItem prop="tableDesc" :label="$t('datacenterTable.tableDesc')">
|
||||||
<ElInput v-model.trim="entity.tableDesc" />
|
<ElInput v-model.trim="entity.tableDesc" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="fields" label-width="0">
|
<ElFormItem prop="fields" :label="$t('datacenterTable.fields')">
|
||||||
<div v-loading="loadFields" class="w-full">
|
<div v-loading="loadFields" class="w-full">
|
||||||
<ElTable :data="fieldsData">
|
<ElTable :data="fieldsData">
|
||||||
<ElTableColumn :label="$t('datacenterTable.fieldName')">
|
<ElTableColumn :label="$t('datacenterTable.fieldName')">
|
||||||
@@ -261,20 +266,7 @@ function getYesOrNo() {
|
|||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDatePicker,
|
ElDatePicker,
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -80,20 +80,24 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="800px"
|
width="800px"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="100px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-for="item in props.formItems"
|
v-for="item in props.formItems"
|
||||||
@@ -135,20 +139,7 @@ function closeDialog() {
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import defaultAvatar from '#/assets/defaultUserAvatar.png';
|
import defaultAvatar from '#/assets/defaultUserAvatar.png';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
@@ -91,13 +92,14 @@ function isAdmin(data: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysAccountModal ref="saveDialog" @reload="reset" />
|
<SysAccountModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({})"
|
@button-click="showDialog({})"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysAccount/page"
|
page-url="/api/v1/sysAccount/page"
|
||||||
@@ -207,7 +209,7 @@ function isAdmin(data: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -101,19 +96,23 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="xl"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="avatar" :label="$t('sysAccount.avatar')">
|
<ElFormItem prop="avatar" :label="$t('sysAccount.avatar')">
|
||||||
<!-- <Cropper v-model="entity.avatar" crop /> -->
|
<!-- <Cropper v-model="entity.avatar" crop /> -->
|
||||||
@@ -151,23 +150,14 @@ function closeDialog() {
|
|||||||
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
||||||
<DictSelect multiple v-model="entity.positionIds" dict-code="sysPosition" />
|
<DictSelect
|
||||||
|
multiple
|
||||||
|
v-model="entity.positionIds"
|
||||||
|
dict-code="sysPosition"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -87,19 +82,23 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="parentId" :label="$t('sysDept.parentId')">
|
<ElFormItem prop="parentId" :label="$t('sysDept.parentId')">
|
||||||
<DictSelect
|
<DictSelect
|
||||||
@@ -125,20 +124,7 @@ function closeDialog() {
|
|||||||
<ElInput v-model.trim="entity.remark" />
|
<ElInput v-model.trim="entity.remark" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { tryit } from 'radash';
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
@@ -102,6 +103,8 @@ async function markStatus(row: any, status: number) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-1.5 p-6">
|
<div class="flex h-full flex-col gap-1.5 p-6">
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<ElForm ref="formRef" inline :model="formData">
|
<ElForm ref="formRef" inline :model="formData">
|
||||||
<ElFormItem prop="feedbackType" class="!mr-3">
|
<ElFormItem prop="feedbackType" class="!mr-3">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
@@ -135,8 +138,7 @@ async function markStatus(row: any, status: number) {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysUserFeedback/page"
|
page-url="/api/v1/sysUserFeedback/page"
|
||||||
@@ -236,6 +238,6 @@ async function markStatus(row: any, status: number) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
@@ -149,13 +150,14 @@ function toLogPage(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysJobModal ref="saveDialog" @reload="reset" />
|
<SysJobModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({})"
|
@button-click="showDialog({})"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysJob/page"
|
page-url="/api/v1/sysJob/page"
|
||||||
@@ -271,7 +273,7 @@ function toLogPage(row: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElAlert,
|
|
||||||
ElButton,
|
import { ElAlert, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import CronPicker from '#/components/cron/CronPicker.vue';
|
import CronPicker from '#/components/cron/CronPicker.vue';
|
||||||
@@ -139,20 +133,24 @@ const str = '"param"';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
v-loading="paramsLoading"
|
v-loading="paramsLoading"
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="jobName" :label="$t('sysJob.jobName')">
|
<ElFormItem prop="jobName" :label="$t('sysJob.jobName')">
|
||||||
<ElInput v-model.trim="entity.jobName" />
|
<ElInput v-model.trim="entity.jobName" />
|
||||||
@@ -225,20 +223,7 @@ const str = '"param"';
|
|||||||
<ElInput v-model.trim="entity.remark" />
|
<ElInput v-model.trim="entity.remark" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ref } from 'vue';
|
|||||||
import { ElTable, ElTableColumn } from 'element-plus';
|
import { ElTable, ElTableColumn } from 'element-plus';
|
||||||
|
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
@@ -25,9 +26,10 @@ function reset(formEl?: FormInstance) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysLogModal ref="saveDialog" @reload="reset" />
|
<SysLogModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch @search="handleSearch" />
|
<HeaderSearch @search="handleSearch" />
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysLog/page"
|
page-url="/api/v1/sysLog/page"
|
||||||
@@ -76,7 +78,7 @@ function reset(formEl?: FormInstance) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -79,19 +74,23 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="lg"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="accountId" :label="$t('sysLog.accountId')">
|
<ElFormItem prop="accountId" :label="$t('sysLog.accountId')">
|
||||||
<ElInput v-model.trim="entity.accountId" />
|
<ElInput v-model.trim="entity.accountId" />
|
||||||
@@ -124,20 +123,7 @@ function closeDialog() {
|
|||||||
<ElInput v-model.trim="entity.status" />
|
<ElInput v-model.trim="entity.status" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { IconPicker } from '@easyflow/common-ui';
|
import { EasyFlowFormModal, IconPicker } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -103,20 +101,24 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
:z-index="1999"
|
:z-index="1999"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="parentId" :label="$t('sysMenu.parentId')">
|
<ElFormItem prop="parentId" :label="$t('sysMenu.parentId')">
|
||||||
<DictSelect
|
<DictSelect
|
||||||
@@ -168,20 +170,7 @@ function closeDialog() {
|
|||||||
<ElInput v-model.trim="entity.remark" />
|
<ElInput v-model.trim="entity.remark" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
@@ -124,13 +125,14 @@ function remove(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysPositionModal ref="saveDialog" @reload="reset" />
|
<SysPositionModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({})"
|
@button-click="showDialog({})"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysPosition/page"
|
page-url="/api/v1/sysPosition/page"
|
||||||
@@ -231,7 +233,7 @@ function remove(row: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElInput,
|
ElInput,
|
||||||
@@ -94,20 +94,24 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="500px"
|
width="500px"
|
||||||
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="100px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="positionName"
|
prop="positionName"
|
||||||
@@ -142,20 +146,7 @@ function closeDialog() {
|
|||||||
<ElInput v-model.trim="entity.remark" type="textarea" :rows="3" />
|
<ElInput v-model.trim="entity.remark" type="textarea" :rows="3" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
@@ -89,13 +90,14 @@ function remove(row: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6 p-6">
|
<div class="flex h-full flex-col gap-6 p-6">
|
||||||
<SysRoleModal ref="saveDialog" @reload="reset" />
|
<SysRoleModal ref="saveDialog" @reload="reset" />
|
||||||
|
<ListPageShell>
|
||||||
|
<template #filters>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="showDialog({ menuCheckStrictly: true })"
|
@button-click="showDialog({ menuCheckStrictly: true })"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/sysRole/page"
|
page-url="/api/v1/sysRole/page"
|
||||||
@@ -160,7 +162,7 @@ function remove(row: any) {
|
|||||||
</ElTable>
|
</ElTable>
|
||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</ListPageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import type { FormInstance } from 'element-plus';
|
|||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
import { ElForm, ElFormItem, ElInput, ElMessage, ElSwitch } from 'element-plus';
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElMessage,
|
|
||||||
ElSwitch,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -95,19 +89,23 @@ function getDeptIds(roleId: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDialog
|
<EasyFlowFormModal
|
||||||
v-model="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
draggable
|
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:close-on-click-modal="false"
|
:confirm-loading="btnLoading"
|
||||||
|
:confirm-text="$t('button.save')"
|
||||||
|
:submitting="btnLoading"
|
||||||
|
width="xl"
|
||||||
|
@confirm="save"
|
||||||
>
|
>
|
||||||
<ElForm
|
<ElForm
|
||||||
label-width="120px"
|
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||||
>
|
>
|
||||||
<ElFormItem prop="roleName" :label="$t('sysRole.roleName')">
|
<ElFormItem prop="roleName" :label="$t('sysRole.roleName')">
|
||||||
<ElInput v-model.trim="entity.roleName" />
|
<ElInput v-model.trim="entity.roleName" />
|
||||||
@@ -157,20 +155,7 @@ function getDeptIds(roleId: any) {
|
|||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
</EasyFlowFormModal>
|
||||||
<ElButton @click="closeDialog">
|
|
||||||
{{ $t('button.cancel') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="btnLoading"
|
|
||||||
:disabled="btnLoading"
|
|
||||||
>
|
|
||||||
{{ $t('button.save') }}
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -149,6 +149,109 @@
|
|||||||
.card-box {
|
.card-box {
|
||||||
@apply bg-card text-card-foreground border-border rounded-xl border;
|
@apply bg-card text-card-foreground border-border rounded-xl border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form--compact {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-section {
|
||||||
|
background: hsl(var(--modal-content-surface-strong));
|
||||||
|
border: 1px solid hsl(var(--modal-divider));
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-grid--2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-field-tip {
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-form-item__label {
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-input__wrapper,
|
||||||
|
.easyflow-modal-form .el-select__wrapper,
|
||||||
|
.easyflow-modal-form .el-textarea__inner,
|
||||||
|
.easyflow-modal-form .el-date-editor.el-input__wrapper {
|
||||||
|
background: hsl(var(--input-background));
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--input) / 0.92);
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-input__wrapper,
|
||||||
|
.easyflow-modal-form .el-select__wrapper,
|
||||||
|
.easyflow-modal-form .el-date-editor.el-input__wrapper {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-textarea__inner {
|
||||||
|
min-height: 96px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-input__wrapper:hover,
|
||||||
|
.easyflow-modal-form .el-select__wrapper:hover,
|
||||||
|
.easyflow-modal-form .el-textarea__inner:hover,
|
||||||
|
.easyflow-modal-form .el-date-editor.el-input__wrapper:hover {
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-input__wrapper.is-focus,
|
||||||
|
.easyflow-modal-form .el-select__wrapper.is-focused,
|
||||||
|
.easyflow-modal-form .el-textarea__inner:focus,
|
||||||
|
.easyflow-modal-form .el-date-editor.el-input__wrapper.is-focus {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(var(--primary) / 0.72),
|
||||||
|
0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-modal-form .el-form-item.is-error .el-input__wrapper,
|
||||||
|
.easyflow-modal-form .el-form-item.is-error .el-select__wrapper,
|
||||||
|
.easyflow-modal-form .el-form-item.is-error .el-textarea__inner,
|
||||||
|
.easyflow-modal-form
|
||||||
|
.el-form-item.is-error
|
||||||
|
.el-date-editor.el-input__wrapper {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(var(--destructive) / 0.8),
|
||||||
|
0 0 0 4px hsl(var(--destructive) / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.easyflow-modal-grid--2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.invert-mode {
|
html.invert-mode {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
.dark[data-theme='custom'],
|
.dark[data-theme='custom'],
|
||||||
.dark[data-theme='default'] {
|
.dark[data-theme='default'] {
|
||||||
/* Default background color of <body />...etc */
|
/* Default background color of <body />...etc */
|
||||||
--background: 222.34deg 10.43% 12.27%;
|
--background: 222 16% 10.5%;
|
||||||
|
|
||||||
/* 主体区域背景色 */
|
/* 主体区域背景色 */
|
||||||
--background-deep: 220deg 13.06% 9%;
|
--background-deep: 220 18% 8.6%;
|
||||||
--foreground: 0 0% 95%;
|
--foreground: 210 18% 95%;
|
||||||
|
|
||||||
/* Background color for <Card /> */
|
/* Background color for <Card /> */
|
||||||
--card: 222.34deg 10.43% 12.27%;
|
--card: 222.34deg 10.43% 12.27%;
|
||||||
@@ -73,12 +73,12 @@
|
|||||||
--heavy-foreground: var(--accent-foreground);
|
--heavy-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
/* Default border color */
|
/* Default border color */
|
||||||
--border: 240 3.7% 22%;
|
--border: 220 10% 21%;
|
||||||
|
|
||||||
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
||||||
--input: 0deg 0% 100% / 10%;
|
--input: 220 10% 22%;
|
||||||
--input-placeholder: 218deg 11% 65%;
|
--input-placeholder: 218 10% 66%;
|
||||||
--input-background: 0deg 0% 100% / 5%;
|
--input-background: 220 10% 16%;
|
||||||
|
|
||||||
/* Used for focus ring */
|
/* Used for focus ring */
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
@@ -97,20 +97,75 @@
|
|||||||
|
|
||||||
/* =============component & UI============= */
|
/* =============component & UI============= */
|
||||||
|
|
||||||
--sidebar: 222.34deg 10.43% 12.27%;
|
--sidebar: 220 14% 12%;
|
||||||
--sidebar-deep: 220deg 13.06% 9%;
|
--sidebar-deep: 220 18% 9.5%;
|
||||||
--menu: var(--sidebar);
|
--menu: var(--sidebar);
|
||||||
|
|
||||||
/* header */
|
/* header */
|
||||||
--header: 222.34deg 10.43% 12.27%;
|
--header: 220 18% 12.5%;
|
||||||
|
--nav-surface: 220 18% 12.5%;
|
||||||
|
--nav-surface-subtle: 219 18% 15.4%;
|
||||||
|
--nav-border: 217 18% 23%;
|
||||||
|
--nav-item-hover: 217 28% 18.8%;
|
||||||
|
--nav-item-active: 216 46% 24%;
|
||||||
|
--nav-item-active-foreground: 210 96% 92%;
|
||||||
|
--nav-item-muted-foreground: 218 15% 78%;
|
||||||
|
--nav-indicator: var(--primary);
|
||||||
|
--nav-tool-bg: 217 34% 17%;
|
||||||
|
--nav-tool-hover: 216 42% 21%;
|
||||||
|
--breadcrumb-muted: 218 12% 72%;
|
||||||
|
--breadcrumb-current: 210 96% 92%;
|
||||||
|
--surface-canvas: 220 18% 9%;
|
||||||
|
--surface-panel: 220 14% 12.8%;
|
||||||
|
--surface-subtle: 220 11% 15.8%;
|
||||||
|
--surface-elevated: 220 14% 14.5%;
|
||||||
|
--surface-glass: 218 24% 16.2%;
|
||||||
|
--surface-contrast-soft: 220 11% 17.2%;
|
||||||
|
--line-subtle: 220 9% 21%;
|
||||||
|
--divider-faint: 220 10% 23%;
|
||||||
|
--table-row-hover: 218 16% 18%;
|
||||||
|
--table-row-border: 220 9% 22%;
|
||||||
|
--table-header-bg: 220 11% 15.5%;
|
||||||
|
--toolbar-bg: 220 16% 14%;
|
||||||
|
--toolbar-border: 220 9% 23%;
|
||||||
|
--text-strong: 0 0% 96%;
|
||||||
|
--text-muted: 218 10% 70%;
|
||||||
|
--glass-tint: 218 26% 16.2%;
|
||||||
|
--glass-border: 210 100% 98%;
|
||||||
|
--glass-blur: 22px;
|
||||||
|
--radius-modal: 20px;
|
||||||
|
--modal-surface: 220 14% 13%;
|
||||||
|
--modal-surface-strong: 220 14% 13%;
|
||||||
|
--modal-content-surface: 220 14% 13%;
|
||||||
|
--modal-content-surface-strong: 220 14% 13%;
|
||||||
|
--modal-footer-surface: 220 13% 12%;
|
||||||
|
--modal-border: 218 16% 24%;
|
||||||
|
--modal-divider: 218 18% 24%;
|
||||||
|
--modal-shadow: 0 24px 60px -30px hsl(212 60% 2% / 0.58);
|
||||||
|
--modal-shell-highlight: 0 0% 100% / 0;
|
||||||
|
--modal-shell-border-soft: 218 16% 24% / 0.96;
|
||||||
|
--modal-shell-shadow-near: 212 50% 2% / 0.38;
|
||||||
|
--modal-shell-shadow-far: 212 46% 2% / 0.42;
|
||||||
|
--modal-overlay: 220 18% 10%;
|
||||||
|
--modal-overlay-opacity: 0.48;
|
||||||
|
--modal-preview-surface: 217 28% 18.5%;
|
||||||
|
--modal-preview-surface-strong: 219 23% 14.5%;
|
||||||
|
--modal-preview-border: 214 44% 31%;
|
||||||
|
--modal-preview-glow: 211 100% 62%;
|
||||||
|
--nav-ambient: 214 100% 44%;
|
||||||
|
--nav-ambient-secondary: 194 82% 40%;
|
||||||
|
--nav-sheen: 210 100% 96%;
|
||||||
|
--shadow-subtle: 0 18px 42px -30px hsl(212 100% 5% / 0.48);
|
||||||
|
--shadow-toolbar: 0 28px 54px -36px hsl(212 100% 4% / 0.58);
|
||||||
|
--shadow-float: 0 34px 70px -38px hsl(212 100% 3% / 0.68);
|
||||||
--bot-back-item: hsl(0deg 0% 91.76%);
|
--bot-back-item: hsl(0deg 0% 91.76%);
|
||||||
--bot-collapse-itme-back: hsl(var(--background-deep));
|
--bot-collapse-itme-back: hsl(var(--background-deep));
|
||||||
--bot-chat-message-container: hsl(var(--background-deep));
|
--bot-chat-message-container: hsl(var(--background-deep));
|
||||||
--bot-chat-message-item-back: hsl(0deg 0% 100%);
|
--bot-chat-message-item-back: hsl(0deg 0% 100%);
|
||||||
--common-font-placeholder-color: hsl(220deg 1.45% 59.41%);
|
--common-font-placeholder-color: hsl(220deg 1.45% 59.41%);
|
||||||
--table-header-text-color: hsl(0deg 0% 91.76%);
|
--table-header-text-color: hsl(var(--text-strong));
|
||||||
--table-header-bg-color: hsl(231.43, 7.87%, 17.45%);
|
--table-header-bg-color: hsl(var(--table-header-bg));
|
||||||
--table-cell-shadow-color: hsl(231.43, 7.87%, 17.45%);
|
--table-cell-shadow-color: hsl(var(--table-row-border));
|
||||||
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
|
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
|
|
||||||
/* 主体区域背景色 */
|
/* 主体区域背景色 */
|
||||||
--background-deep: 220 33.33% 98.24%;
|
--background-deep: 220 32% 96.9%;
|
||||||
--foreground: 210 6% 21%;
|
--foreground: 215 20% 18%;
|
||||||
|
|
||||||
/* Background color for <Card /> */
|
/* Background color for <Card /> */
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
/* 主题颜色 */
|
/* 主题颜色 */
|
||||||
|
|
||||||
--primary: 216 100% 50%;
|
--primary: 211 100% 50%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
/* Used for destructive actions such as <Button variant="destructive"> */
|
/* Used for destructive actions such as <Button variant="destructive"> */
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
--heavy-foreground: var(--accent-foreground);
|
--heavy-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
/* Default border color */
|
/* Default border color */
|
||||||
--border: 0 0% 94.12%;
|
--border: 216 18% 90%;
|
||||||
|
|
||||||
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
||||||
--input: 240deg 5.88% 90%;
|
--input: 216 18% 90%;
|
||||||
--input-placeholder: 217 10.6% 65%;
|
--input-placeholder: 215 12% 58%;
|
||||||
--input-background: 0 0% 100%;
|
--input-background: 210 33% 98.4%;
|
||||||
|
|
||||||
/* Used for focus ring */
|
/* Used for focus ring */
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
@@ -97,12 +97,67 @@
|
|||||||
/* =============component & UI============= */
|
/* =============component & UI============= */
|
||||||
|
|
||||||
/* menu */
|
/* menu */
|
||||||
--sidebar: 0 0% 100%;
|
--sidebar: 210 33% 99.4%;
|
||||||
--sidebar-deep: 0 0% 100%;
|
--sidebar-deep: 210 28% 98%;
|
||||||
--menu: var(--sidebar);
|
--menu: var(--sidebar);
|
||||||
|
|
||||||
/* header */
|
/* header */
|
||||||
--header: 0 0% 100%;
|
--header: 212 100% 98.7%;
|
||||||
|
--nav-surface: 212 100% 98.7%;
|
||||||
|
--nav-surface-subtle: 211 84% 97.3%;
|
||||||
|
--nav-border: 214 34% 89%;
|
||||||
|
--nav-item-hover: 211 100% 96.3%;
|
||||||
|
--nav-item-active: 211 100% 93.8%;
|
||||||
|
--nav-item-active-foreground: 213 78% 41%;
|
||||||
|
--nav-item-muted-foreground: 215 18% 34%;
|
||||||
|
--nav-indicator: var(--primary);
|
||||||
|
--nav-tool-bg: 212 88% 96.1%;
|
||||||
|
--nav-tool-hover: 211 100% 94%;
|
||||||
|
--breadcrumb-muted: 215 12% 48%;
|
||||||
|
--breadcrumb-current: 213 78% 41%;
|
||||||
|
--surface-canvas: 216 32% 97%;
|
||||||
|
--surface-panel: 0 0% 100%;
|
||||||
|
--surface-subtle: 210 24% 98.4%;
|
||||||
|
--surface-elevated: 210 33% 99.5%;
|
||||||
|
--surface-glass: 212 100% 98.9%;
|
||||||
|
--surface-contrast-soft: 210 26% 95.8%;
|
||||||
|
--line-subtle: 214 18% 91%;
|
||||||
|
--divider-faint: 215 22% 90%;
|
||||||
|
--table-row-hover: 212 55% 97%;
|
||||||
|
--table-row-border: 214 16% 92%;
|
||||||
|
--table-header-bg: 210 28% 97%;
|
||||||
|
--toolbar-bg: 210 36% 98.5%;
|
||||||
|
--toolbar-border: 214 18% 90%;
|
||||||
|
--text-strong: 216 22% 19%;
|
||||||
|
--text-muted: 215 10% 49%;
|
||||||
|
--glass-tint: 212 100% 98.9%;
|
||||||
|
--glass-border: 0 0% 100%;
|
||||||
|
--glass-blur: 20px;
|
||||||
|
--radius-modal: 20px;
|
||||||
|
--modal-surface: 0 0% 100%;
|
||||||
|
--modal-surface-strong: 0 0% 100%;
|
||||||
|
--modal-content-surface: 0 0% 100%;
|
||||||
|
--modal-content-surface-strong: 0 0% 100%;
|
||||||
|
--modal-footer-surface: 210 20% 98.4%;
|
||||||
|
--modal-border: 214 22% 86%;
|
||||||
|
--modal-divider: 214 22% 89%;
|
||||||
|
--modal-shadow: 0 22px 54px -28px hsl(215 28% 18% / 0.18);
|
||||||
|
--modal-shell-highlight: 0 0% 100% / 0;
|
||||||
|
--modal-shell-border-soft: 214 22% 86% / 0.94;
|
||||||
|
--modal-shell-shadow-near: 216 30% 18% / 0.08;
|
||||||
|
--modal-shell-shadow-far: 216 34% 18% / 0.12;
|
||||||
|
--modal-overlay: 220 18% 8%;
|
||||||
|
--modal-overlay-opacity: 0.24;
|
||||||
|
--modal-preview-surface: 211 100% 97.3%;
|
||||||
|
--modal-preview-surface-strong: 0 0% 100%;
|
||||||
|
--modal-preview-border: 212 84% 88%;
|
||||||
|
--modal-preview-glow: 211 100% 56%;
|
||||||
|
--nav-ambient: 212 100% 84%;
|
||||||
|
--nav-ambient-secondary: 194 93% 82%;
|
||||||
|
--nav-sheen: 0 0% 100%;
|
||||||
|
--shadow-subtle: 0 18px 42px -30px hsl(211 78% 48% / 0.16);
|
||||||
|
--shadow-toolbar: 0 28px 54px -34px hsl(211 78% 48% / 0.2);
|
||||||
|
--shadow-float: 0 34px 68px -38px hsl(211 78% 48% / 0.24);
|
||||||
|
|
||||||
accent-color: var(--primary);
|
accent-color: var(--primary);
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
@@ -112,9 +167,9 @@
|
|||||||
--bot-chat-message-container: hsl(228deg 33.33% 97.06%);
|
--bot-chat-message-container: hsl(228deg 33.33% 97.06%);
|
||||||
--bot-chat-message-item-back: hsl(0deg 0% 100%);
|
--bot-chat-message-item-back: hsl(0deg 0% 100%);
|
||||||
--common-font-placeholder-color: hsl(220deg 1.45% 59.41%);
|
--common-font-placeholder-color: hsl(220deg 1.45% 59.41%);
|
||||||
--table-header-text-color: hsl(240, 0.99%, 19.8%);
|
--table-header-text-color: hsl(var(--text-strong));
|
||||||
--table-header-bg-color: hsl(220 60% 98.04%);
|
--table-header-bg-color: hsl(var(--table-header-bg));
|
||||||
--table-cell-shadow-color: hsl(225, 18.18%, 95.69%);
|
--table-cell-shadow-color: hsl(var(--table-row-border));
|
||||||
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
|
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const defaultPreferences: Preferences = {
|
|||||||
enable: true,
|
enable: true,
|
||||||
hideOnlyOne: false,
|
hideOnlyOne: false,
|
||||||
showHome: false,
|
showHome: false,
|
||||||
showIcon: true,
|
showIcon: false,
|
||||||
styleType: 'normal',
|
styleType: 'normal',
|
||||||
},
|
},
|
||||||
copyright: {
|
copyright: {
|
||||||
@@ -56,7 +56,7 @@ const defaultPreferences: Preferences = {
|
|||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
enable: true,
|
enable: true,
|
||||||
height: 50,
|
height: 56,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
menuAlign: 'start',
|
menuAlign: 'start',
|
||||||
mode: 'fixed',
|
mode: 'fixed',
|
||||||
@@ -99,7 +99,7 @@ const defaultPreferences: Preferences = {
|
|||||||
tabbar: {
|
tabbar: {
|
||||||
draggable: true,
|
draggable: true,
|
||||||
enable: true,
|
enable: true,
|
||||||
height: 38,
|
height: 32,
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
maxCount: 0,
|
maxCount: 0,
|
||||||
middleClickToClose: false,
|
middleClickToClose: false,
|
||||||
@@ -107,7 +107,7 @@ const defaultPreferences: Preferences = {
|
|||||||
showIcon: true,
|
showIcon: true,
|
||||||
showMaximize: true,
|
showMaximize: true,
|
||||||
showMore: true,
|
showMore: true,
|
||||||
styleType: 'chrome',
|
styleType: 'plain',
|
||||||
wheelable: true,
|
wheelable: true,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ const style = computed((): CSSProperties => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main ref="contentElement" :style="style" class="bg-background-deep relative">
|
<main
|
||||||
|
ref="contentElement"
|
||||||
|
:style="style"
|
||||||
|
class="relative overflow-hidden bg-[radial-gradient(circle_at_top,hsl(var(--glass-tint))/0.34,transparent_40%),linear-gradient(180deg,hsl(var(--surface-canvas)),hsl(var(--background-deep)))]"
|
||||||
|
>
|
||||||
<Slot :style="overlayStyle">
|
<Slot :style="overlayStyle">
|
||||||
<slot name="overlay"></slot>
|
<slot name="overlay"></slot>
|
||||||
</Slot>
|
</Slot>
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ const style = computed((): CSSProperties => {
|
|||||||
const right = !show || !fullWidth ? undefined : 0;
|
const right = !show || !fullWidth ? undefined : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
backgroundColor: 'hsl(var(--nav-surface) / 0.86)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-surface) / 0.96), hsl(var(--glass-tint) / 0.82))',
|
||||||
|
backdropFilter: 'blur(var(--glass-blur)) saturate(160%)',
|
||||||
|
boxShadow: 'var(--shadow-subtle)',
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
marginTop: show ? 0 : `-${height}px`,
|
marginTop: show ? 0 : `-${height}px`,
|
||||||
right,
|
right,
|
||||||
@@ -58,20 +63,30 @@ const logoStyle = computed((): CSSProperties => {
|
|||||||
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
|
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const ambientStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient) / 0.42) 0%, hsl(var(--nav-ambient) / 0.18) 22%, transparent 56%), radial-gradient(circle at 20% 0%, hsl(var(--nav-ambient-secondary) / 0.22) 0%, transparent 40%), linear-gradient(180deg, hsl(var(--nav-sheen) / 0.22), transparent 48%)',
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
:class="theme"
|
:class="theme"
|
||||||
:style="style"
|
:style="style"
|
||||||
class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
|
class="relative top-0 flex w-full flex-[0_0_auto] items-center gap-1 overflow-x-hidden overflow-y-visible px-3 transition-[margin-top,background-color,box-shadow] duration-200"
|
||||||
>
|
>
|
||||||
<div v-if="slots.logo" :style="logoStyle">
|
<div :style="ambientStyle" class="pointer-events-none absolute inset-0"></div>
|
||||||
|
|
||||||
|
<div v-if="slots.logo" :style="logoStyle" class="relative z-10">
|
||||||
<slot name="logo"></slot>
|
<slot name="logo"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
<slot name="toggle-button"> </slot>
|
<slot name="toggle-button"> </slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex min-w-0 flex-1 items-center">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
collapseHeight: 42,
|
collapseHeight: 96,
|
||||||
collapseWidth: 48,
|
collapseWidth: 48,
|
||||||
domVisible: true,
|
domVisible: true,
|
||||||
fixedExtra: false,
|
fixedExtra: false,
|
||||||
@@ -122,7 +122,13 @@ const style = computed((): CSSProperties => {
|
|||||||
const { isSidebarMixed, marginTop, paddingTop, zIndex } = props;
|
const { isSidebarMixed, marginTop, paddingTop, zIndex } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'--scroll-shadow': 'var(--sidebar)',
|
'--scroll-shadow': 'var(--surface-elevated)',
|
||||||
|
backgroundColor: 'hsl(var(--nav-surface-subtle) / 0.44)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-surface) / 0.66) 0%, hsl(var(--nav-surface-subtle) / 0.5) 28%, hsl(var(--glass-tint) / 0.36) 100%)',
|
||||||
|
backdropFilter: 'blur(calc(var(--glass-blur) * 1.5)) saturate(188%)',
|
||||||
|
boxShadow:
|
||||||
|
'0 26px 64px -42px hsl(var(--primary) / 0.28), inset 0 1px 0 hsl(var(--nav-sheen) / 0.42), inset -1px 0 0 hsl(var(--nav-sheen) / 0.14)',
|
||||||
...calcMenuWidthStyle(false),
|
...calcMenuWidthStyle(false),
|
||||||
height: `calc(100% - ${marginTop}px)`,
|
height: `calc(100% - ${marginTop}px)`,
|
||||||
marginTop: `${marginTop}px`,
|
marginTop: `${marginTop}px`,
|
||||||
@@ -136,17 +142,31 @@ const extraStyle = computed((): CSSProperties => {
|
|||||||
const { extraWidth, show, width, zIndex } = props;
|
const { extraWidth, show, width, zIndex } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
backgroundColor: 'hsl(var(--nav-surface-subtle) / 0.4)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-surface-subtle) / 0.62) 0%, hsl(var(--glass-tint) / 0.34) 100%)',
|
||||||
|
backdropFilter: 'blur(calc(var(--glass-blur) * 1.34)) saturate(182%)',
|
||||||
|
boxShadow:
|
||||||
|
'0 26px 64px -42px hsl(var(--primary) / 0.24), inset 0 1px 0 hsl(var(--nav-sheen) / 0.34), inset -1px 0 0 hsl(var(--nav-sheen) / 0.1)',
|
||||||
left: `${width}px`,
|
left: `${width}px`,
|
||||||
width: extraVisible.value && show ? `${extraWidth}px` : 0,
|
width: extraVisible.value && show ? `${extraWidth}px` : 0,
|
||||||
zIndex,
|
zIndex,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const shellAmbientStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient) / 0.58) 0%, hsl(var(--nav-ambient) / 0.22) 20%, transparent 56%), radial-gradient(circle at 14% 0%, hsl(var(--nav-ambient-secondary) / 0.24) 0%, transparent 34%), linear-gradient(180deg, hsl(var(--nav-sheen) / 0.34), transparent 16%)',
|
||||||
|
};
|
||||||
|
const extraAmbientStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient) / 0.42) 0%, hsl(var(--nav-ambient) / 0.16) 18%, transparent 44%), linear-gradient(180deg, hsl(var(--nav-sheen) / 0.28), transparent 16%)',
|
||||||
|
};
|
||||||
|
|
||||||
const extraTitleStyle = computed((): CSSProperties => {
|
const extraTitleStyle = computed((): CSSProperties => {
|
||||||
const { headerHeight } = props;
|
const { headerHeight } = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
height: `${headerHeight - 1}px`,
|
height: `${Math.max(headerHeight - 1, 0)}px`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +183,7 @@ const contentStyle = computed((): CSSProperties => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||||
paddingTop: '8px',
|
paddingTop: headerHeight > 0 ? '10px' : '52px',
|
||||||
...contentWidthStyle.value,
|
...contentWidthStyle.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -173,7 +193,7 @@ const headerStyle = computed((): CSSProperties => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),
|
...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),
|
||||||
height: `${headerHeight - 1}px`,
|
height: `${Math.max(headerHeight - 1, 0)}px`,
|
||||||
...contentWidthStyle.value,
|
...contentWidthStyle.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -182,12 +202,13 @@ const extraContentStyle = computed((): CSSProperties => {
|
|||||||
const { collapseHeight, headerHeight } = props;
|
const { collapseHeight, headerHeight } = props;
|
||||||
return {
|
return {
|
||||||
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
height: `calc(100% - ${headerHeight + collapseHeight}px)`,
|
||||||
|
paddingTop: headerHeight > 0 ? '10px' : '52px',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const collapseStyle = computed((): CSSProperties => {
|
const toolStyle = computed((): CSSProperties => {
|
||||||
return {
|
return {
|
||||||
height: `${props.collapseHeight}px`,
|
top: `${Math.max(10, (props.headerHeight - 36) / 2)}px`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,60 +284,70 @@ function handleMouseleave() {
|
|||||||
:class="[
|
:class="[
|
||||||
theme,
|
theme,
|
||||||
{
|
{
|
||||||
'bg-sidebar-deep': isSidebarMixed,
|
'bg-[hsl(var(--glass-tint))/0.72]': isSidebarMixed,
|
||||||
'bg-sidebar border-border border-r': !isSidebarMixed,
|
'bg-[hsl(var(--glass-tint))/0.82]': !isSidebarMixed,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:style="style"
|
:style="style"
|
||||||
class="fixed left-0 top-0 h-full transition-all duration-150"
|
class="fixed left-0 top-0 h-full overflow-hidden transition-all duration-150"
|
||||||
@mouseenter="handleMouseenter"
|
@mouseenter="handleMouseenter"
|
||||||
@mouseleave="handleMouseleave"
|
@mouseleave="handleMouseleave"
|
||||||
>
|
>
|
||||||
<SidebarFixedButton
|
<div :style="shellAmbientStyle" class="pointer-events-none absolute inset-0"></div>
|
||||||
v-if="!collapse && !isSidebarMixed && showFixedButton"
|
<div
|
||||||
v-model:expand-on-hover="expandOnHover"
|
v-if="slots.logo"
|
||||||
/>
|
:style="headerStyle"
|
||||||
<div v-if="slots.logo" :style="headerStyle">
|
class="relative z-10 px-2 pt-2"
|
||||||
|
>
|
||||||
<slot name="logo"></slot>
|
<slot name="logo"></slot>
|
||||||
</div>
|
</div>
|
||||||
<EasyFlowScrollbar :style="contentStyle" shadow shadow-border>
|
<EasyFlowScrollbar :style="contentStyle" class="relative z-10">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</EasyFlowScrollbar>
|
</EasyFlowScrollbar>
|
||||||
|
|
||||||
<div :style="collapseStyle"></div>
|
<div
|
||||||
<SidebarCollapseButton
|
v-if="!isSidebarMixed && !collapse && showFixedButton"
|
||||||
v-if="showCollapseButton && !isSidebarMixed"
|
:style="toolStyle"
|
||||||
v-model:collapsed="collapse"
|
class="absolute right-3 z-20 flex"
|
||||||
|
>
|
||||||
|
<SidebarFixedButton
|
||||||
|
v-model:expand-on-hover="expandOnHover"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isSidebarMixed"
|
v-if="isSidebarMixed"
|
||||||
ref="asideRef"
|
ref="asideRef"
|
||||||
:class="{
|
|
||||||
'border-l': extraVisible,
|
|
||||||
}"
|
|
||||||
:style="extraStyle"
|
:style="extraStyle"
|
||||||
class="border-border bg-sidebar fixed top-0 h-full overflow-hidden border-r transition-all duration-200"
|
class="fixed top-0 h-full overflow-hidden transition-all duration-200"
|
||||||
>
|
>
|
||||||
<SidebarCollapseButton
|
<div :style="extraAmbientStyle" class="pointer-events-none absolute inset-0"></div>
|
||||||
v-if="isSidebarMixed && expandOnHover"
|
<div
|
||||||
v-model:collapsed="extraCollapse"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarFixedButton
|
|
||||||
v-if="!extraCollapse"
|
v-if="!extraCollapse"
|
||||||
v-model:expand-on-hover="expandOnHover"
|
:style="extraTitleStyle"
|
||||||
/>
|
class="relative z-10 px-3"
|
||||||
<div v-if="!extraCollapse" :style="extraTitleStyle" class="pl-2">
|
>
|
||||||
<slot name="extra-title"></slot>
|
<slot name="extra-title"></slot>
|
||||||
</div>
|
</div>
|
||||||
<EasyFlowScrollbar
|
<EasyFlowScrollbar
|
||||||
:style="extraContentStyle"
|
:style="extraContentStyle"
|
||||||
class="border-border py-2"
|
class="relative z-10 py-2"
|
||||||
shadow
|
|
||||||
shadow-border
|
|
||||||
>
|
>
|
||||||
<slot name="extra"></slot>
|
<slot name="extra"></slot>
|
||||||
</EasyFlowScrollbar>
|
</EasyFlowScrollbar>
|
||||||
|
<div
|
||||||
|
v-if="expandOnHover || !extraCollapse"
|
||||||
|
:style="toolStyle"
|
||||||
|
class="absolute right-3 z-20 flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<SidebarCollapseButton
|
||||||
|
v-if="expandOnHover"
|
||||||
|
v-model:collapsed="extraCollapse"
|
||||||
|
/>
|
||||||
|
<SidebarFixedButton
|
||||||
|
v-if="!extraCollapse"
|
||||||
|
v-model:expand-on-hover="expandOnHover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,16 +15,28 @@ const props = withDefaults(defineProps<Props>(), {});
|
|||||||
const style = computed((): CSSProperties => {
|
const style = computed((): CSSProperties => {
|
||||||
const { height } = props;
|
const { height } = props;
|
||||||
return {
|
return {
|
||||||
|
backgroundColor: 'hsl(var(--glass-tint) / 0.68)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-surface-subtle) / 0.92), hsl(var(--glass-tint) / 0.74))',
|
||||||
|
backdropFilter: 'blur(calc(var(--glass-blur) * 0.9)) saturate(155%)',
|
||||||
|
boxShadow: '0 18px 36px -30px hsl(var(--primary) / 0.12)',
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const ambientStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient) / 0.24) 0%, transparent 38%), linear-gradient(180deg, hsl(var(--nav-sheen) / 0.14), transparent 62%)',
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
:style="style"
|
:style="style"
|
||||||
class="border-border bg-background flex w-full border-b transition-all"
|
class="relative flex w-full overflow-hidden transition-all"
|
||||||
>
|
>
|
||||||
|
<div :style="ambientStyle" class="pointer-events-none absolute inset-0"></div>
|
||||||
|
<div class="relative z-10 flex w-full">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import { ChevronsLeft, ChevronsRight } from '@easyflow-core/icons';
|
import { ChevronsLeft, ChevronsRight } from '@easyflow-core/icons';
|
||||||
|
|
||||||
const collapsed = defineModel<boolean>('collapsed');
|
const collapsed = defineModel<boolean>('collapsed');
|
||||||
|
const buttonStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.82))',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
|
||||||
|
};
|
||||||
|
|
||||||
function handleCollapsed() {
|
function handleCollapsed() {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
@@ -9,11 +18,13 @@ function handleCollapsed() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 left-3 z-10 cursor-pointer rounded-sm p-1"
|
aria-label="切换侧边栏折叠状态"
|
||||||
|
:style="buttonStyle"
|
||||||
|
class="flex-center h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
|
||||||
@click.stop="handleCollapsed"
|
@click.stop="handleCollapsed"
|
||||||
>
|
>
|
||||||
<ChevronsRight v-if="collapsed" class="size-4" />
|
<ChevronsRight v-if="collapsed" class="size-4" />
|
||||||
<ChevronsLeft v-else class="size-4" />
|
<ChevronsLeft v-else class="size-4" />
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import { Pin, PinOff } from '@easyflow-core/icons';
|
import { Pin, PinOff } from '@easyflow-core/icons';
|
||||||
|
|
||||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||||
|
const buttonStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.82))',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
|
||||||
|
};
|
||||||
|
|
||||||
function toggleFixed() {
|
function toggleFixed() {
|
||||||
expandOnHover.value = !expandOnHover.value;
|
expandOnHover.value = !expandOnHover.value;
|
||||||
@@ -9,11 +18,13 @@ function toggleFixed() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
class="flex-center hover:text-foreground text-foreground/60 hover:bg-accent-hover bg-accent absolute bottom-2 right-3 z-10 cursor-pointer rounded-sm p-[5px] transition-all duration-300"
|
aria-label="切换侧边栏固定状态"
|
||||||
|
:style="buttonStyle"
|
||||||
|
class="flex-center h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
|
||||||
@click="toggleFixed"
|
@click="toggleFixed"
|
||||||
>
|
>
|
||||||
<PinOff v-if="!expandOnHover" class="size-3.5" />
|
<PinOff v-if="!expandOnHover" class="size-3.5" />
|
||||||
<Pin v-else class="size-3.5" />
|
<Pin v-else class="size-3.5" />
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,6 +31,24 @@ defineOptions({
|
|||||||
name: 'EasyFlowLayout',
|
name: 'EasyFlowLayout',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const headerToggleButtonClass =
|
||||||
|
'my-0 mr-1 flex h-9 w-9 items-center justify-center rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]';
|
||||||
|
const headerToggleButtonStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.8))',
|
||||||
|
border: '1px solid hsl(var(--glass-border) / 0.2)',
|
||||||
|
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
|
||||||
|
};
|
||||||
|
const layoutAmbientLeftStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient) / 0.34) 0%, hsl(var(--nav-ambient) / 0.14) 28%, transparent 72%)',
|
||||||
|
};
|
||||||
|
const layoutAmbientRightStyle: CSSProperties = {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at left top, hsl(var(--nav-ambient-secondary) / 0.14) 0%, transparent 72%)',
|
||||||
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
contentCompactWidth: 1200,
|
contentCompactWidth: 1200,
|
||||||
@@ -482,6 +500,10 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative flex min-h-full w-full">
|
<div class="relative flex min-h-full w-full">
|
||||||
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
|
<div :style="layoutAmbientLeftStyle" class="absolute -left-16 top-0 h-[320px] w-[320px] rounded-full blur-3xl"></div>
|
||||||
|
<div :style="layoutAmbientRightStyle" class="absolute right-[-120px] top-[-40px] h-[260px] w-[320px] rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
<LayoutSidebar
|
<LayoutSidebar
|
||||||
v-if="sidebarEnableState"
|
v-if="sidebarEnableState"
|
||||||
v-model:collapse="sidebarCollapse"
|
v-model:collapse="sidebarCollapse"
|
||||||
@@ -526,12 +548,12 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
|
class="relative z-[1] flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,
|
'shadow-[var(--shadow-toolbar)]': scrollY > 20,
|
||||||
},
|
},
|
||||||
SCROLL_FIXED_CLASS,
|
SCROLL_FIXED_CLASS,
|
||||||
]"
|
]"
|
||||||
@@ -556,7 +578,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
|
|||||||
<template #toggle-button>
|
<template #toggle-button>
|
||||||
<EasyFlowIconButton
|
<EasyFlowIconButton
|
||||||
v-if="showHeaderToggleButton"
|
v-if="showHeaderToggleButton"
|
||||||
class="my-0 mr-1 rounded-md"
|
:class="headerToggleButtonClass"
|
||||||
|
:style="headerToggleButtonStyle"
|
||||||
@click="handleHeaderToggle"
|
@click="handleHeaderToggle"
|
||||||
>
|
>
|
||||||
<IconifyIcon v-if="showSidebar" icon="ep:fold" />
|
<IconifyIcon v-if="showSidebar" icon="ep:fold" />
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ function handleClick() {
|
|||||||
emit('click', item);
|
emit('click', item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
subMenu?.addSubMenu?.(item);
|
subMenu?.addSubMenu?.(item);
|
||||||
rootMenu?.addMenuItem?.(item);
|
rootMenu?.addMenuItem?.(item);
|
||||||
@@ -90,7 +99,9 @@ onBeforeUnmount(() => {
|
|||||||
is('collapse-show-title', collapseShowTitle),
|
is('collapse-show-title', collapseShowTitle),
|
||||||
]"
|
]"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
|
:tabindex="disabled ? -1 : 0"
|
||||||
@click.stop="handleClick"
|
@click.stop="handleClick"
|
||||||
|
@keydown="handleKeydown"
|
||||||
>
|
>
|
||||||
<EasyFlowTooltip
|
<EasyFlowTooltip
|
||||||
v-if="showTooltip"
|
v-if="showTooltip"
|
||||||
|
|||||||
@@ -377,12 +377,13 @@ $namespace: easyflow;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--menu-item-active-background-color);
|
background: var(--menu-item-active-background-color);
|
||||||
|
box-shadow: var(--menu-item-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin menu-item {
|
@mixin menu-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
// gap: 12px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--menu-item-height);
|
height: var(--menu-item-height);
|
||||||
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
||||||
@@ -401,7 +402,13 @@ $namespace: easyflow;
|
|||||||
background 0.15s ease,
|
background 0.15s ease,
|
||||||
color 0.15s ease,
|
color 0.15s ease,
|
||||||
padding 0.15s ease,
|
padding 0.15s ease,
|
||||||
border-color 0.15s ease;
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-disabled {
|
&.is-disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -425,6 +432,12 @@ $namespace: easyflow;
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px hsl(var(--glass-border) / 0.72),
|
||||||
|
0 0 0 4px hsl(var(--ring) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
@@ -445,68 +458,106 @@ $namespace: easyflow;
|
|||||||
.#{$namespace}-menu__popup-container,
|
.#{$namespace}-menu__popup-container,
|
||||||
.#{$namespace}-menu {
|
.#{$namespace}-menu {
|
||||||
--menu-title-width: 140px;
|
--menu-title-width: 140px;
|
||||||
--menu-item-icon-size: 16px;
|
--menu-item-icon-size: 18px;
|
||||||
--menu-item-height: 38px;
|
--menu-item-height: 40px;
|
||||||
--menu-item-padding-y: 21px;
|
--menu-item-padding-y: 0px;
|
||||||
--menu-item-padding-x: 12px;
|
--menu-item-padding-x: 14px;
|
||||||
--menu-item-popup-padding-y: 20px;
|
--menu-item-popup-padding-y: 0px;
|
||||||
--menu-item-popup-padding-x: 12px;
|
--menu-item-popup-padding-x: 14px;
|
||||||
--menu-item-margin-y: 2px;
|
--menu-item-margin-y: 4px;
|
||||||
--menu-item-margin-x: 0px;
|
--menu-item-margin-x: 8px;
|
||||||
--menu-item-collapse-padding-y: 23.5px;
|
--menu-item-collapse-padding-y: 0px;
|
||||||
--menu-item-collapse-padding-x: 0px;
|
--menu-item-collapse-padding-x: 0px;
|
||||||
--menu-item-collapse-margin-y: 4px;
|
--menu-item-collapse-margin-y: 4px;
|
||||||
--menu-item-collapse-margin-x: 0px;
|
--menu-item-collapse-margin-x: 8px;
|
||||||
--menu-item-radius: 0px;
|
--menu-item-radius: 14px;
|
||||||
--menu-item-indent: 16px;
|
--menu-item-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--nav-sheen) / 0.54),
|
||||||
|
0 22px 40px -30px hsl(var(--primary) / 0.24);
|
||||||
|
--menu-item-indent: 18px;
|
||||||
--menu-font-size: 14px;
|
--menu-font-size: 14px;
|
||||||
|
--menu-item-indicator-width: 0px;
|
||||||
|
|
||||||
&.is-dark {
|
&.is-dark {
|
||||||
--menu-background-color: hsl(var(--menu));
|
--menu-background-color: hsl(var(--nav-surface));
|
||||||
// --menu-submenu-opened-background-color: hsl(var(--menu-opened-dark));
|
--menu-item-background-color: transparent;
|
||||||
--menu-item-background-color: var(--menu-background-color);
|
--menu-item-color: hsl(var(--nav-item-muted-foreground));
|
||||||
--menu-item-color: hsl(216.92, 15.12%, 33.73% / 80%);
|
--menu-item-hover-color: hsl(var(--foreground));
|
||||||
--menu-item-hover-color: hsl(var(--accent-foreground));
|
--menu-item-hover-background-color: linear-gradient(
|
||||||
--menu-item-hover-background-color: hsl(var(--accent));
|
180deg,
|
||||||
--menu-item-active-color: hsl(var(--accent-foreground));
|
hsl(var(--nav-item-hover) / 0.78),
|
||||||
--menu-item-active-background-color: hsl(var(--accent));
|
hsl(var(--glass-tint) / 0.46)
|
||||||
|
);
|
||||||
|
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
|
--menu-item-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active) / 0.92) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.72) 54%,
|
||||||
|
hsl(var(--glass-tint) / 0.52) 100%
|
||||||
|
);
|
||||||
--menu-submenu-hover-color: hsl(var(--foreground));
|
--menu-submenu-hover-color: hsl(var(--foreground));
|
||||||
--menu-submenu-hover-background-color: hsl(var(--accent));
|
--menu-submenu-hover-background-color: linear-gradient(
|
||||||
--menu-submenu-active-color: hsl(var(--foreground));
|
180deg,
|
||||||
--menu-submenu-active-background-color: transparent;
|
hsl(var(--nav-item-hover) / 0.78),
|
||||||
|
hsl(var(--glass-tint) / 0.46)
|
||||||
|
);
|
||||||
|
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
|
--menu-submenu-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active) / 0.92) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.72) 54%,
|
||||||
|
hsl(var(--glass-tint) / 0.52) 100%
|
||||||
|
);
|
||||||
--menu-submenu-background-color: var(--menu-background-color);
|
--menu-submenu-background-color: var(--menu-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-light {
|
&.is-light {
|
||||||
--menu-background-color: hsl(var(--menu));
|
--menu-background-color: hsl(var(--nav-surface));
|
||||||
// --menu-submenu-opened-background-color: hsl(var(--menu-opened));
|
--menu-item-background-color: transparent;
|
||||||
--menu-item-background-color: var(--menu-background-color);
|
--menu-item-color: hsl(var(--nav-item-muted-foreground));
|
||||||
--menu-item-color: hsl(216.92, 15.12%, 33.73%);
|
--menu-item-hover-color: hsl(var(--foreground));
|
||||||
--menu-item-hover-color: var(--menu-item-color);
|
--menu-item-hover-background-color: linear-gradient(
|
||||||
--menu-item-hover-background-color: hsl(var(--accent));
|
180deg,
|
||||||
--menu-item-active-color: hsl(var(--primary));
|
hsl(var(--nav-item-hover) / 0.78),
|
||||||
--menu-item-active-background-color: hsl(var(--primary) / 15%);
|
hsl(var(--glass-tint) / 0.46)
|
||||||
--menu-submenu-hover-color: hsl(var(--primary));
|
);
|
||||||
--menu-submenu-hover-background-color: hsl(var(--accent));
|
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
--menu-submenu-active-color: hsl(var(--primary));
|
--menu-item-active-background-color: linear-gradient(
|
||||||
--menu-submenu-active-background-color: transparent;
|
135deg,
|
||||||
|
hsl(var(--nav-item-active) / 0.92) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.72) 54%,
|
||||||
|
hsl(var(--glass-tint) / 0.52) 100%
|
||||||
|
);
|
||||||
|
--menu-submenu-hover-color: hsl(var(--foreground));
|
||||||
|
--menu-submenu-hover-background-color: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--nav-item-hover) / 0.78),
|
||||||
|
hsl(var(--glass-tint) / 0.46)
|
||||||
|
);
|
||||||
|
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
|
--menu-submenu-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active) / 0.92) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.72) 54%,
|
||||||
|
hsl(var(--glass-tint) / 0.52) 100%
|
||||||
|
);
|
||||||
--menu-submenu-background-color: var(--menu-background-color);
|
--menu-submenu-background-color: var(--menu-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-rounded {
|
&.is-rounded {
|
||||||
--menu-item-margin-x: 8px;
|
--menu-item-margin-x: 8px;
|
||||||
--menu-item-collapse-margin-x: 6px;
|
--menu-item-collapse-margin-x: 8px;
|
||||||
--menu-item-radius: 8px;
|
--menu-item-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-horizontal:not(.is-rounded) {
|
&.is-horizontal:not(.is-rounded) {
|
||||||
--menu-item-height: 40px;
|
--menu-item-height: 40px;
|
||||||
--menu-item-radius: 6px;
|
--menu-item-radius: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-horizontal.is-rounded {
|
&.is-horizontal.is-rounded {
|
||||||
--menu-item-height: 40px;
|
--menu-item-height: 40px;
|
||||||
--menu-item-radius: 6px;
|
--menu-item-radius: 14px;
|
||||||
--menu-item-padding-x: 12px;
|
--menu-item-padding-x: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,25 +570,61 @@ $namespace: easyflow;
|
|||||||
--menu-background-color: transparent;
|
--menu-background-color: transparent;
|
||||||
|
|
||||||
&.is-dark {
|
&.is-dark {
|
||||||
--menu-item-hover-color: hsl(var(--accent-foreground));
|
--menu-item-hover-color: hsl(var(--foreground));
|
||||||
--menu-item-hover-background-color: hsl(var(--accent));
|
--menu-item-hover-background-color: linear-gradient(
|
||||||
--menu-item-active-color: hsl(var(--accent-foreground));
|
180deg,
|
||||||
--menu-item-active-background-color: hsl(var(--accent));
|
hsl(var(--nav-item-hover) / 0.98),
|
||||||
--menu-submenu-active-color: hsl(var(--foreground));
|
hsl(var(--nav-surface-subtle) / 0.84)
|
||||||
--menu-submenu-active-background-color: hsl(var(--accent));
|
);
|
||||||
--menu-submenu-hover-color: hsl(var(--accent-foreground));
|
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
--menu-submenu-hover-background-color: hsl(var(--accent));
|
--menu-item-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active)) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98) 55%,
|
||||||
|
hsl(var(--glass-tint) / 0.98) 100%
|
||||||
|
);
|
||||||
|
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
|
--menu-submenu-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active)) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98) 55%,
|
||||||
|
hsl(var(--glass-tint) / 0.98) 100%
|
||||||
|
);
|
||||||
|
--menu-submenu-hover-color: hsl(var(--foreground));
|
||||||
|
--menu-submenu-hover-background-color: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98),
|
||||||
|
hsl(var(--nav-surface-subtle) / 0.84)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-light {
|
&.is-light {
|
||||||
--menu-item-active-color: hsl(var(--primary));
|
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
--menu-item-active-background-color: hsl(var(--primary) / 15%);
|
--menu-item-active-background-color: linear-gradient(
|
||||||
--menu-item-hover-background-color: hsl(var(--accent));
|
135deg,
|
||||||
--menu-item-hover-color: hsl(var(--primary));
|
hsl(var(--nav-item-active)) 0%,
|
||||||
--menu-submenu-active-color: hsl(var(--primary));
|
hsl(var(--nav-item-hover) / 0.98) 55%,
|
||||||
--menu-submenu-active-background-color: hsl(var(--primary) / 15%);
|
hsl(var(--glass-tint) / 0.98) 100%
|
||||||
--menu-submenu-hover-color: hsl(var(--primary));
|
);
|
||||||
--menu-submenu-hover-background-color: hsl(var(--accent));
|
--menu-item-hover-background-color: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98),
|
||||||
|
hsl(var(--nav-surface-subtle) / 0.84)
|
||||||
|
);
|
||||||
|
--menu-item-hover-color: hsl(var(--foreground));
|
||||||
|
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
|
||||||
|
--menu-submenu-active-background-color: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active)) 0%,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98) 55%,
|
||||||
|
hsl(var(--glass-tint) / 0.98) 100%
|
||||||
|
);
|
||||||
|
--menu-submenu-hover-color: hsl(var(--foreground));
|
||||||
|
--menu-submenu-hover-background-color: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--nav-item-hover) / 0.98),
|
||||||
|
hsl(var(--nav-surface-subtle) / 0.84)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,6 +761,10 @@ $namespace: easyflow;
|
|||||||
&.is-active {
|
&.is-active {
|
||||||
background: var(--menu-item-active-background-color) !important;
|
background: var(--menu-item-active-background-color) !important;
|
||||||
border-radius: var(--menu-item-radius);
|
border-radius: var(--menu-item-radius);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,7 +810,7 @@ $namespace: easyflow;
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: var(--menu-item-icon-size);
|
width: var(--menu-item-icon-size);
|
||||||
height: var(--menu-item-icon-size);
|
height: var(--menu-item-icon-size);
|
||||||
margin-right: 8px;
|
margin-right: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -741,6 +832,7 @@ $namespace: easyflow;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--menu-item-height);
|
height: var(--menu-item-height);
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@include menu-title;
|
@include menu-title;
|
||||||
@@ -775,6 +867,8 @@ $namespace: easyflow;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--menu-item-hover-background-color) !important;
|
background: var(--menu-item-hover-background-color) !important;
|
||||||
|
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 0.18);
|
||||||
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$namespace}-menu-tooltip__trigger {
|
.#{$namespace}-menu-tooltip__trigger {
|
||||||
@@ -809,8 +903,20 @@ $namespace: easyflow;
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--menu-submenu-active-background-color);
|
background: var(--menu-submenu-active-background-color);
|
||||||
fill: var(--menu-submenu-active-color);
|
fill: var(--menu-submenu-active-color);
|
||||||
|
box-shadow: var(--menu-item-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible > .#{$namespace}-sub-menu-content,
|
||||||
|
&:focus-within > .#{$namespace}-sub-menu-content {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px hsl(var(--glass-border) / 0.72),
|
||||||
|
0 0 0 4px hsl(var(--ring) / 0.16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$namespace}-sub-menu-content {
|
.#{$namespace}-sub-menu-content {
|
||||||
@@ -833,6 +939,7 @@ $namespace: easyflow;
|
|||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
@include menu-title;
|
@include menu-title;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-collapse-show-title {
|
&.is-collapse-show-title {
|
||||||
@@ -866,10 +973,13 @@ $namespace: easyflow;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--menu-submenu-hover-background-color) !important;
|
background: var(--menu-submenu-hover-background-color) !important;
|
||||||
|
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 0.18);
|
||||||
// svg {
|
transform: translateX(2px);
|
||||||
// fill: var(--menu-submenu-hover-color);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$namespace}-sub-menu .#{$namespace}-menu-item,
|
||||||
|
.#{$namespace}-sub-menu .#{$namespace}-sub-menu-content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ function handleClick() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
|
||||||
function handleMouseenter(event: FocusEvent | MouseEvent, showTimeout = 300) {
|
function handleMouseenter(event: FocusEvent | MouseEvent, showTimeout = 300) {
|
||||||
if (event.type === 'focus') {
|
if (event.type === 'focus') {
|
||||||
return;
|
return;
|
||||||
@@ -197,9 +206,11 @@ onBeforeUnmount(() => {
|
|||||||
is('active', active),
|
is('active', active),
|
||||||
is('disabled', disabled),
|
is('disabled', disabled),
|
||||||
]"
|
]"
|
||||||
|
:tabindex="disabled ? -1 : 0"
|
||||||
@focus="handleMouseenter"
|
@focus="handleMouseenter"
|
||||||
@mouseenter="handleMouseenter"
|
@mouseenter="handleMouseenter"
|
||||||
@mouseleave="() => handleMouseleave()"
|
@mouseleave="() => handleMouseleave()"
|
||||||
|
@keydown="handleKeydown"
|
||||||
>
|
>
|
||||||
<template v-if="rootMenu.isMenuPopup">
|
<template v-if="rootMenu.isMenuPopup">
|
||||||
<EasyFlowHoverCard
|
<EasyFlowHoverCard
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ const getForceMount = computed(() => {
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
:append-to="getAppendTo"
|
:append-to="getAppendTo"
|
||||||
:class="
|
:class="
|
||||||
cn('flex w-[520px] flex-col', drawerClass, {
|
cn('flex w-[520px] flex-col overflow-hidden border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] shadow-[var(--shadow-float)] supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62]', drawerClass, {
|
||||||
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
|
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
|
||||||
'max-h-[100vh]': placement === 'bottom' || placement === 'top',
|
'max-h-[100vh]': placement === 'bottom' || placement === 'top',
|
||||||
hidden: isClosed,
|
hidden: isClosed,
|
||||||
@@ -209,7 +209,7 @@ const getForceMount = computed(() => {
|
|||||||
v-if="showHeader"
|
v-if="showHeader"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'!flex flex-row items-center justify-between border-b px-6 py-5',
|
'!flex flex-row items-center justify-between border-b border-[hsl(var(--divider-faint))/0.32] px-6 py-5',
|
||||||
headerClass,
|
headerClass,
|
||||||
{
|
{
|
||||||
'px-4 py-3': closable,
|
'px-4 py-3': closable,
|
||||||
@@ -223,7 +223,7 @@ const getForceMount = computed(() => {
|
|||||||
v-if="closable && closeIconPlacement === 'left'"
|
v-if="closable && closeIconPlacement === 'left'"
|
||||||
as-child
|
as-child
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
class="data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92] ml-[2px] cursor-pointer rounded-full opacity-84 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
<slot name="close-icon">
|
<slot name="close-icon">
|
||||||
<EasyFlowIconButton>
|
<EasyFlowIconButton>
|
||||||
@@ -264,7 +264,7 @@ const getForceMount = computed(() => {
|
|||||||
v-if="closable && closeIconPlacement === 'right'"
|
v-if="closable && closeIconPlacement === 'right'"
|
||||||
as-child
|
as-child
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
class="data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92] ml-[2px] cursor-pointer rounded-full opacity-84 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
<slot name="close-icon">
|
<slot name="close-icon">
|
||||||
<EasyFlowIconButton>
|
<EasyFlowIconButton>
|
||||||
@@ -295,7 +295,7 @@ const getForceMount = computed(() => {
|
|||||||
v-if="showFooter"
|
v-if="showFooter"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'w-full flex-row items-center justify-end border-t p-2 px-3',
|
'w-full flex-row items-center justify-end border-t border-[hsl(var(--divider-faint))/0.32] p-2 px-3',
|
||||||
footerClass,
|
footerClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export class ModalApi {
|
|||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
showConfirmButton: true,
|
showConfirmButton: true,
|
||||||
title: '',
|
title: '',
|
||||||
animationType: 'slide',
|
animationType: 'scale',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.store = new Store<ModalState>(
|
this.store = new Store<ModalState>(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ModalApi } from './modal-api';
|
|||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
/**
|
/**
|
||||||
* 动画类型
|
* 动画类型
|
||||||
* @default 'slide'
|
* @default 'scale'
|
||||||
*/
|
*/
|
||||||
animationType?: 'scale' | 'slide';
|
animationType?: 'scale' | 'slide';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -231,12 +231,13 @@ function handleClosed() {
|
|||||||
:append-to="getAppendTo"
|
:append-to="getAppendTo"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
|
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col overflow-hidden border border-[hsl(var(--modal-shell-border-soft))] bg-[hsl(var(--modal-surface))] p-0',
|
||||||
shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',
|
shouldFullscreen
|
||||||
|
? 'sm:rounded-none'
|
||||||
|
: 'sm:rounded-[var(--radius-modal)]',
|
||||||
modalClass,
|
modalClass,
|
||||||
{
|
{
|
||||||
'border-border border': bordered,
|
'border-[hsl(var(--modal-shell-border-soft))]': bordered,
|
||||||
'shadow-3xl': !bordered,
|
|
||||||
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
||||||
shouldFullscreen,
|
shouldFullscreen,
|
||||||
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
|
||||||
@@ -248,6 +249,16 @@ function handleClosed() {
|
|||||||
:force-mount="getForceMount"
|
:force-mount="getForceMount"
|
||||||
:modal="modal"
|
:modal="modal"
|
||||||
:open="state?.isOpen"
|
:open="state?.isOpen"
|
||||||
|
:overlay-style="{
|
||||||
|
backgroundColor:
|
||||||
|
'hsl(var(--modal-overlay) / var(--modal-overlay-opacity))',
|
||||||
|
}"
|
||||||
|
:content-style="{
|
||||||
|
backdropFilter: 'none',
|
||||||
|
WebkitBackdropFilter: 'none',
|
||||||
|
backgroundColor: 'hsl(var(--modal-surface))',
|
||||||
|
boxShadow: 'var(--modal-shadow)',
|
||||||
|
}"
|
||||||
:show-close="closable"
|
:show-close="closable"
|
||||||
:animation-type="animationType"
|
:animation-type="animationType"
|
||||||
:z-index="zIndex"
|
:z-index="zIndex"
|
||||||
@@ -267,9 +278,8 @@ function handleClosed() {
|
|||||||
ref="headerRef"
|
ref="headerRef"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'px-5 py-4',
|
'px-5 py-4 sm:px-6 sm:py-5',
|
||||||
{
|
{
|
||||||
'border-b': bordered,
|
|
||||||
hidden: !header,
|
hidden: !header,
|
||||||
'cursor-move select-none': shouldDraggable,
|
'cursor-move select-none': shouldDraggable,
|
||||||
},
|
},
|
||||||
@@ -301,9 +311,13 @@ function handleClosed() {
|
|||||||
<div
|
<div
|
||||||
ref="wrapperRef"
|
ref="wrapperRef"
|
||||||
:class="
|
:class="
|
||||||
cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
|
cn(
|
||||||
|
'relative min-h-0 flex-1 overflow-y-auto bg-[hsl(var(--modal-content-surface))] px-5 pb-5 pt-4 sm:px-6 sm:pb-6',
|
||||||
|
contentClass,
|
||||||
|
{
|
||||||
'pointer-events-none': showLoading || submitting,
|
'pointer-events-none': showLoading || submitting,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -311,7 +325,7 @@ function handleClosed() {
|
|||||||
<EasyFlowLoading v-if="showLoading || submitting" spinning />
|
<EasyFlowLoading v-if="showLoading || submitting" spinning />
|
||||||
<EasyFlowIconButton
|
<EasyFlowIconButton
|
||||||
v-if="fullscreenButton"
|
v-if="fullscreenButton"
|
||||||
class="hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-3 hidden size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block"
|
class="text-foreground/80 flex-center opacity-78 absolute right-11 top-4 hidden size-8 rounded-full bg-[hsl(var(--modal-preview-surface-strong))/0.76] px-1 text-lg shadow-[0_16px_28px_-24px_hsl(var(--foreground)/0.4)] transition-all hover:-translate-y-0.5 hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block"
|
||||||
@click="handleFullscreen"
|
@click="handleFullscreen"
|
||||||
>
|
>
|
||||||
<Shrink v-if="fullscreen" class="size-3.5" />
|
<Shrink v-if="fullscreen" class="size-3.5" />
|
||||||
@@ -323,9 +337,9 @@ function handleClosed() {
|
|||||||
ref="footerRef"
|
ref="footerRef"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex-row items-center justify-end p-2',
|
'flex-row items-center justify-end gap-3 bg-[hsl(var(--modal-footer-surface))] px-5 py-4 sm:px-6',
|
||||||
{
|
{
|
||||||
'border-t': bordered,
|
'border-t border-[hsl(var(--modal-divider))]': bordered,
|
||||||
},
|
},
|
||||||
footerClass,
|
footerClass,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function handleClick(path?: string) {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList class="easyflow-breadcrumb flex-nowrap">
|
||||||
<TransitionGroup name="breadcrumb-transition">
|
<TransitionGroup name="breadcrumb-transition">
|
||||||
<template
|
<template
|
||||||
v-for="(item, index) in breadcrumbs"
|
v-for="(item, index) in breadcrumbs"
|
||||||
@@ -44,10 +44,10 @@ function handleClick(path?: string) {
|
|||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<div v-if="item.items?.length ?? 0 > 0">
|
<div v-if="item.items?.length ?? 0 > 0">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger class="flex items-center gap-1">
|
<DropdownMenuTrigger class="easyflow-breadcrumb__link">
|
||||||
<EasyFlowIcon v-if="showIcon" :icon="item.icon" class="size-5" />
|
<EasyFlowIcon v-if="showIcon" :icon="item.icon" class="size-4" />
|
||||||
{{ item.title }}
|
<span class="max-w-[180px] truncate">{{ item.title }}</span>
|
||||||
<ChevronDown class="size-4" />
|
<ChevronDown class="size-3.5" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<template
|
<template
|
||||||
@@ -63,32 +63,37 @@ function handleClick(path?: string) {
|
|||||||
</div>
|
</div>
|
||||||
<BreadcrumbLink
|
<BreadcrumbLink
|
||||||
v-else-if="index !== breadcrumbs.length - 1"
|
v-else-if="index !== breadcrumbs.length - 1"
|
||||||
|
class="easyflow-breadcrumb__link"
|
||||||
href="javascript:void 0"
|
href="javascript:void 0"
|
||||||
@click.stop="handleClick(item.path)"
|
@click.stop="handleClick(item.path)"
|
||||||
>
|
>
|
||||||
<div class="flex-center">
|
<div class="flex items-center">
|
||||||
<EasyFlowIcon
|
<EasyFlowIcon
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:class="{ 'size-5': item.isHome }"
|
:class="{ 'size-5': item.isHome }"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
class="mr-1 size-4"
|
class="mr-1 size-4"
|
||||||
/>
|
/>
|
||||||
{{ item.title }}
|
<span class="max-w-[180px] truncate">{{ item.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
<BreadcrumbPage v-else>
|
<BreadcrumbPage
|
||||||
<div class="flex-center">
|
v-else
|
||||||
|
class="easyflow-breadcrumb__current"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
<EasyFlowIcon
|
<EasyFlowIcon
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:class="{ 'size-5': item.isHome }"
|
:class="{ 'size-5': item.isHome }"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
class="mr-1 size-4"
|
class="mr-1 size-4"
|
||||||
/>
|
/>
|
||||||
{{ item.title }}
|
<span class="max-w-[220px] truncate">{{ item.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</BreadcrumbPage>
|
</BreadcrumbPage>
|
||||||
<BreadcrumbSeparator
|
<BreadcrumbSeparator
|
||||||
v-if="index < breadcrumbs.length - 1 && !item.isHome"
|
v-if="index < breadcrumbs.length - 1 && !item.isHome"
|
||||||
|
class="easyflow-breadcrumb__separator"
|
||||||
/>
|
/>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,3 +101,60 @@ function handleClick(path?: string) {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.easyflow-breadcrumb {
|
||||||
|
gap: 4px;
|
||||||
|
color: hsl(var(--breadcrumb-muted));
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-breadcrumb__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: hsl(var(--breadcrumb-muted));
|
||||||
|
transition:
|
||||||
|
color 0.16s ease,
|
||||||
|
background-color 0.16s ease,
|
||||||
|
transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-breadcrumb__link:hover {
|
||||||
|
background: hsl(var(--nav-item-hover) / 0.7);
|
||||||
|
color: hsl(var(--nav-item-active-foreground));
|
||||||
|
transform: translateY(-0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-breadcrumb__current {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: hsl(var(--breadcrumb-current));
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(var(--nav-item-active) / 0.88),
|
||||||
|
hsl(var(--glass-tint) / 0.92)
|
||||||
|
);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--nav-sheen) / 0.42),
|
||||||
|
0 10px 22px -18px hsl(var(--primary) / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-breadcrumb__separator {
|
||||||
|
color: hsl(var(--breadcrumb-muted) / 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easyflow-breadcrumb__separator :deep(svg) {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -72,12 +72,16 @@ defineExpose({
|
|||||||
</Transition>
|
</Transition>
|
||||||
<AlertDialogContent
|
<AlertDialogContent
|
||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
|
:style="{
|
||||||
|
...(zIndex ? { zIndex } : {}),
|
||||||
|
position: 'fixed',
|
||||||
|
backdropFilter: 'blur(var(--glass-blur)) saturate(170%)',
|
||||||
|
}"
|
||||||
@animationend="onAnimationEnd"
|
@animationend="onAnimationEnd"
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
|
'z-popup border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] p-6 shadow-[var(--shadow-float)] outline-none supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] sm:rounded-[22px]',
|
||||||
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
|
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
|
||||||
|
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import type { ClassType } from '@easyflow-core/typings';
|
import type { ClassType } from '@easyflow-core/typings';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
@@ -20,16 +22,19 @@ const props = withDefaults(
|
|||||||
class?: ClassType;
|
class?: ClassType;
|
||||||
closeClass?: ClassType;
|
closeClass?: ClassType;
|
||||||
closeDisabled?: boolean;
|
closeDisabled?: boolean;
|
||||||
|
contentStyle?: CSSProperties;
|
||||||
modal?: boolean;
|
modal?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
overlayBlur?: number;
|
overlayBlur?: number;
|
||||||
|
overlayClass?: ClassType;
|
||||||
|
overlayStyle?: CSSProperties;
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
}
|
}
|
||||||
>(),
|
>(),
|
||||||
{
|
{
|
||||||
appendTo: 'body',
|
appendTo: 'body',
|
||||||
animationType: 'slide',
|
animationType: 'scale',
|
||||||
closeDisabled: false,
|
closeDisabled: false,
|
||||||
showClose: true,
|
showClose: true,
|
||||||
},
|
},
|
||||||
@@ -41,8 +46,11 @@ const emits = defineEmits<
|
|||||||
const delegatedProps = computed(() => {
|
const delegatedProps = computed(() => {
|
||||||
const {
|
const {
|
||||||
class: _,
|
class: _,
|
||||||
|
contentStyle: _contentStyle,
|
||||||
modal: _modal,
|
modal: _modal,
|
||||||
open: _open,
|
open: _open,
|
||||||
|
overlayClass: _overlayClass,
|
||||||
|
overlayStyle: _overlayStyle,
|
||||||
showClose: __,
|
showClose: __,
|
||||||
animationType: ___,
|
animationType: ___,
|
||||||
...delegated
|
...delegated
|
||||||
@@ -86,9 +94,11 @@ defineExpose({
|
|||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
v-if="open && modal"
|
v-if="open && modal"
|
||||||
|
:class="props.overlayClass"
|
||||||
:style="{
|
:style="{
|
||||||
...(zIndex ? { zIndex } : {}),
|
...(zIndex ? { zIndex } : {}),
|
||||||
position,
|
position,
|
||||||
|
...props.overlayStyle,
|
||||||
backdropFilter:
|
backdropFilter:
|
||||||
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
|
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
|
||||||
}"
|
}"
|
||||||
@@ -97,14 +107,18 @@ defineExpose({
|
|||||||
</Transition>
|
</Transition>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
:style="{ ...(zIndex ? { zIndex } : {}), position }"
|
:style="{
|
||||||
|
...(zIndex ? { zIndex } : {}),
|
||||||
|
position,
|
||||||
|
...props.contentStyle,
|
||||||
|
}"
|
||||||
@animationend="onAnimationEnd"
|
@animationend="onAnimationEnd"
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-none sm:rounded-xl',
|
'z-popup data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-220 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] p-6 shadow-[var(--shadow-float)] outline-none data-[state=closed]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] sm:rounded-[22px]',
|
||||||
{
|
{
|
||||||
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
|
'data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-4':
|
||||||
animationType === 'slide',
|
animationType === 'slide',
|
||||||
},
|
},
|
||||||
props.class,
|
props.class,
|
||||||
@@ -118,7 +132,7 @@ defineExpose({
|
|||||||
:disabled="closeDisabled"
|
:disabled="closeDisabled"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none',
|
'data-[state=open]:text-muted-foreground hover:text-accent-foreground text-foreground/80 flex-center opacity-78 absolute right-3 top-3 h-7 w-7 rounded-full px-1 text-lg shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.34)] transition-opacity hover:bg-[hsl(var(--surface-contrast-soft))/0.98] hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92]',
|
||||||
props.closeClass,
|
props.closeClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject } from 'vue';
|
import { computed, inject, useAttrs } from 'vue';
|
||||||
|
|
||||||
import { useScrollLock } from '@easyflow-core/composables';
|
import { useScrollLock } from '@easyflow-core/composables';
|
||||||
|
import { cn } from '@easyflow-core/shared/utils';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
useScrollLock();
|
useScrollLock();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
const id = inject('DISMISSABLE_MODAL_ID');
|
const id = inject('DISMISSABLE_MODAL_ID');
|
||||||
|
const overlayClass = computed(() => {
|
||||||
|
const customClass = attrs.class as string | undefined;
|
||||||
|
return cn(
|
||||||
|
customClass ? 'z-popup inset-0' : 'bg-overlay z-popup inset-0',
|
||||||
|
customClass,
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div :data-dismissable-modal="id" class="bg-overlay z-popup inset-0"></div>
|
<div
|
||||||
|
:data-dismissable-modal="id"
|
||||||
|
:class="overlayClass"
|
||||||
|
:style="$attrs.style"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ function onAnimationEnd(event: AnimationEvent) {
|
|||||||
:style="{
|
:style="{
|
||||||
...(zIndex ? { zIndex } : {}),
|
...(zIndex ? { zIndex } : {}),
|
||||||
position,
|
position,
|
||||||
|
backdropFilter: 'blur(var(--glass-blur)) saturate(170%)',
|
||||||
}"
|
}"
|
||||||
@animationend="onAnimationEnd"
|
@animationend="onAnimationEnd"
|
||||||
v-bind="{ ...forwarded, ...$attrs }"
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { VariantProps } from 'class-variance-authority';
|
|||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
export const sheetVariants = cva(
|
export const sheetVariants = cva(
|
||||||
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
|
'border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] shadow-[var(--shadow-float)] transition ease-in-out supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
{
|
{
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
side: 'right',
|
side: 'right',
|
||||||
@@ -11,11 +11,11 @@ export const sheetVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
bottom:
|
bottom:
|
||||||
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
'inset-x-0 bottom-0 border-t border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
|
||||||
right:
|
right:
|
||||||
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
'inset-y-0 right-0 w-3/4 border-l border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
||||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
top: 'inset-x-0 top-0 border-b border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ defineOptions({
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
contentClass: 'easyflow-tabs-content',
|
contentClass: 'easyflow-tabs-content',
|
||||||
contextMenus: () => [],
|
contextMenus: () => [],
|
||||||
gap: 7,
|
gap: 8,
|
||||||
tabs: () => [],
|
tabs: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
:class="contentClass"
|
:class="contentClass"
|
||||||
:style="style"
|
:style="style"
|
||||||
class="tabs-chrome !flex h-full w-max overflow-y-hidden pr-6"
|
class="tabs-chrome !flex h-full w-max items-center gap-1 overflow-y-hidden px-2"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="slide-left">
|
<TransitionGroup name="slide-left">
|
||||||
<div
|
<div
|
||||||
@@ -92,7 +92,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
]"
|
]"
|
||||||
:data-active-tab="active"
|
:data-active-tab="active"
|
||||||
:data-index="i"
|
:data-index="i"
|
||||||
class="tabs-chrome__item draggable translate-all group relative -mr-3 flex h-full select-none items-center"
|
class="tabs-chrome__item draggable translate-all group relative flex h-full min-w-0 select-none items-center"
|
||||||
data-tab-item="true"
|
data-tab-item="true"
|
||||||
@click="active = tab.key"
|
@click="active = tab.key"
|
||||||
@mousedown="onMouseDown($event, tab)"
|
@mousedown="onMouseDown($event, tab)"
|
||||||
@@ -103,63 +103,45 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
:modal="false"
|
:modal="false"
|
||||||
item-class="pr-6"
|
item-class="pr-6"
|
||||||
>
|
>
|
||||||
<div class="relative size-full px-1">
|
<div class="relative size-full">
|
||||||
<!-- divider -->
|
<!-- divider -->
|
||||||
<div
|
|
||||||
v-if="i !== 0 && tab.key !== active"
|
|
||||||
class="tabs-chrome__divider bg-border absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
|
|
||||||
></div>
|
|
||||||
<!-- background -->
|
<!-- background -->
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
|
class="tabs-chrome__background absolute inset-0 z-[-1] px-0 py-0 transition-opacity duration-150"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
|
class="tabs-chrome__background-content h-[calc(100%-8px)] rounded-2xl bg-transparent transition-all duration-150 group-[.is-active]:bg-[hsl(var(--glass-tint))/0.74] group-[.is-active]:shadow-[0_16px_28px_-24px_hsl(var(--foreground)/0.34)]"
|
||||||
></div>
|
></div>
|
||||||
<svg
|
|
||||||
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
|
|
||||||
height="7"
|
|
||||||
width="7"
|
|
||||||
>
|
|
||||||
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
|
|
||||||
height="7"
|
|
||||||
width="7"
|
|
||||||
>
|
|
||||||
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- extra -->
|
<!-- extra -->
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
|
class="tabs-chrome__extra absolute right-2 top-1/2 z-[3] size-4 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<!-- close-icon -->
|
<!-- close-icon -->
|
||||||
<X
|
<X
|
||||||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
|
class="mt-[2px] size-3 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
|
||||||
@click.stop="() => emit('close', tab.key)"
|
@click.stop="() => emit('close', tab.key)"
|
||||||
/>
|
/>
|
||||||
<Pin
|
<Pin
|
||||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
|
class="mt-[1px] size-3.5 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
|
||||||
@click.stop="() => emit('unpin', tab)"
|
@click.stop="() => emit('unpin', tab)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- tab-item-main -->
|
<!-- tab-item-main -->
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150"
|
class="tabs-chrome__item-main z-[2] mx-3 my-0 flex h-full min-w-0 items-center overflow-hidden pl-2 pr-5 text-[hsl(var(--nav-item-muted-foreground))] duration-150 group-[.is-active]:font-medium group-[.is-active]:text-[hsl(var(--nav-item-active-foreground))]"
|
||||||
>
|
>
|
||||||
<EasyFlowIcon
|
<EasyFlowIcon
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:icon="tab.icon"
|
:icon="tab.icon"
|
||||||
class="mr-1 flex size-4 items-center overflow-hidden"
|
class="mr-2 flex size-[15px] items-center overflow-hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
<span class="flex-1 overflow-hidden whitespace-nowrap text-[13px]">
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,10 +169,8 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabs-chrome__background {
|
.tabs-chrome__background {
|
||||||
@apply pb-[2px];
|
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@apply bg-accent mx-[2px] rounded-md;
|
@apply bg-[hsl(var(--nav-item-hover))];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const typeWithClass = computed(() => {
|
|||||||
},
|
},
|
||||||
plain: {
|
plain: {
|
||||||
content:
|
content:
|
||||||
'h-full [&:not(:first-child)]:border-l last:border-r border-border',
|
"h-[calc(100%-8px)] rounded-2xl border border-transparent bg-transparent transition-all duration-150 [&.is-active]:bg-[hsl(var(--glass-tint))/0.72] [&.is-active]:shadow-[0_16px_28px_-24px_hsl(var(--foreground)/0.34)] [&:not(.is-active)]:hover:bg-[hsl(var(--nav-item-hover))/0.9]",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="contentClass"
|
:class="contentClass"
|
||||||
class="relative !flex h-full w-max items-center overflow-hidden pr-6"
|
class="relative !flex h-full w-max items-center gap-1 overflow-hidden pr-3"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="slide-left">
|
<TransitionGroup name="slide-left">
|
||||||
<div
|
<div
|
||||||
@@ -89,14 +89,14 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'is-active dark:bg-accent bg-primary/15': tab.key === active,
|
'is-active': tab.key === active,
|
||||||
draggable: !tab.affixTab,
|
draggable: !tab.affixTab,
|
||||||
'affix-tab': tab.affixTab,
|
'affix-tab': tab.affixTab,
|
||||||
},
|
},
|
||||||
typeWithClass.content,
|
typeWithClass.content,
|
||||||
]"
|
]"
|
||||||
:data-index="i"
|
:data-index="i"
|
||||||
class="tab-item [&:not(.is-active)]:hover:bg-accent translate-all group relative flex cursor-pointer select-none"
|
class="tab-item translate-all group relative flex min-w-0 cursor-pointer select-none"
|
||||||
data-tab-item="true"
|
data-tab-item="true"
|
||||||
@click="active = tab.key"
|
@click="active = tab.key"
|
||||||
@mousedown="onMouseDown($event, tab)"
|
@mousedown="onMouseDown($event, tab)"
|
||||||
@@ -115,28 +115,28 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
|
|||||||
<!-- close-icon -->
|
<!-- close-icon -->
|
||||||
<X
|
<X
|
||||||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
|
class="size-3 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
|
||||||
@click.stop="() => emit('close', tab.key)"
|
@click.stop="() => emit('close', tab.key)"
|
||||||
/>
|
/>
|
||||||
<Pin
|
<Pin
|
||||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
|
class="mt-[1px] size-3.5 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
|
||||||
@click.stop="() => emit('unpin', tab)"
|
@click.stop="() => emit('unpin', tab)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- tab-item-main -->
|
<!-- tab-item-main -->
|
||||||
<div
|
<div
|
||||||
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
|
class="mx-3 mr-4 flex h-full items-center overflow-hidden pr-3 text-[hsl(var(--nav-item-muted-foreground))] transition-all duration-150 group-[.is-active]:font-medium group-[.is-active]:text-[hsl(var(--nav-item-active-foreground))]"
|
||||||
>
|
>
|
||||||
<EasyFlowIcon
|
<EasyFlowIcon
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:icon="tab.icon"
|
:icon="tab.icon"
|
||||||
class="mr-2 flex size-4 items-center overflow-hidden"
|
class="mr-2 flex size-[15px] items-center overflow-hidden"
|
||||||
fallback
|
fallback
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
<span class="flex-1 overflow-hidden whitespace-nowrap text-[13px]">
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defineProps<DropdownMenuProps>();
|
|||||||
<template>
|
<template>
|
||||||
<EasyFlowDropdownMenu :menus="menus" :modal="false">
|
<EasyFlowDropdownMenu :menus="menus" :modal="false">
|
||||||
<div
|
<div
|
||||||
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
class="flex-center mr-1 h-8 w-8 cursor-pointer rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.52] text-[hsl(var(--nav-item-muted-foreground))] shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform] hover:-translate-y-0.5 hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ChevronDown class="size-4" />
|
<ChevronDown class="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function toggleScreen() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
|
class="flex-center h-8 w-8 cursor-pointer rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.52] text-[hsl(var(--nav-item-muted-foreground))] shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform] hover:-translate-y-0.5 hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:text-foreground"
|
||||||
@click="toggleScreen"
|
@click="toggleScreen"
|
||||||
>
|
>
|
||||||
<Minimize2 v-if="screen" class="size-4" />
|
<Minimize2 v-if="screen" class="size-4" />
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ useTabsDrag(props, emit);
|
|||||||
<span
|
<span
|
||||||
v-show="showScrollButton"
|
v-show="showScrollButton"
|
||||||
:class="{
|
:class="{
|
||||||
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,
|
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtLeft,
|
||||||
'pointer-events-none opacity-30': scrollIsAtLeft,
|
'pointer-events-none opacity-30': scrollIsAtLeft,
|
||||||
}"
|
}"
|
||||||
class="border-r px-2"
|
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.44] px-2 shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform]"
|
||||||
@click="scrollDirection('left')"
|
@click="scrollDirection('left')"
|
||||||
>
|
>
|
||||||
<ChevronLeft class="size-4 h-full" />
|
<ChevronLeft class="size-4 h-full" />
|
||||||
@@ -64,7 +64,7 @@ useTabsDrag(props, emit);
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
'pt-[3px]': styleType === 'chrome',
|
'pt-[2px]': styleType === 'chrome',
|
||||||
}"
|
}"
|
||||||
class="size-full flex-1 overflow-hidden"
|
class="size-full flex-1 overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -94,10 +94,10 @@ useTabsDrag(props, emit);
|
|||||||
<span
|
<span
|
||||||
v-show="showScrollButton"
|
v-show="showScrollButton"
|
||||||
:class="{
|
:class="{
|
||||||
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,
|
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtRight,
|
||||||
'pointer-events-none opacity-30': scrollIsAtRight,
|
'pointer-events-none opacity-30': scrollIsAtRight,
|
||||||
}"
|
}"
|
||||||
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
|
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.44] px-2 shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform]"
|
||||||
@click="scrollDirection('right')"
|
@click="scrollDirection('right')"
|
||||||
>
|
>
|
||||||
<ChevronRight class="size-4 h-full" />
|
<ChevronRight class="size-4 h-full" />
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { TabsProps } from './types';
|
import type { TabsProps } from './types';
|
||||||
|
|
||||||
|
import type { ComponentPublicInstance } from 'vue';
|
||||||
|
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
|
import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
|
||||||
@@ -7,12 +9,13 @@ import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
|
|||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
type DomElement = Element | null | undefined;
|
type DomElement = Element | null | undefined;
|
||||||
|
type ScrollbarInstance = ComponentPublicInstance & { $el: HTMLElement };
|
||||||
|
|
||||||
export function useTabsViewScroll(props: TabsProps) {
|
export function useTabsViewScroll(props: TabsProps) {
|
||||||
let resizeObserver: null | ResizeObserver = null;
|
let resizeObserver: null | ResizeObserver = null;
|
||||||
let mutationObserver: MutationObserver | null = null;
|
let mutationObserver: MutationObserver | null = null;
|
||||||
let tabItemCount = 0;
|
let tabItemCount = 0;
|
||||||
const scrollbarRef = ref<InstanceType<typeof EasyFlowScrollbar> | null>(null);
|
const scrollbarRef = ref<ScrollbarInstance | null>(null);
|
||||||
const scrollViewportEl = ref<DomElement>(null);
|
const scrollViewportEl = ref<DomElement>(null);
|
||||||
const showScrollButton = ref(false);
|
const showScrollButton = ref(false);
|
||||||
const scrollIsAtLeft = ref(true);
|
const scrollIsAtLeft = ref(true);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { EasyFlowFormModal } from '..';
|
||||||
|
|
||||||
|
describe('easyFlowFormModal', () => {
|
||||||
|
it('renders title, preview and body content when opened', async () => {
|
||||||
|
const wrapper = mount(EasyFlowFormModal, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
teleport: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '保存',
|
||||||
|
description: '描述信息',
|
||||||
|
open: true,
|
||||||
|
showPreview: true,
|
||||||
|
title: '测试弹窗',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<div>表单内容</div>',
|
||||||
|
preview: '<div>预览内容</div>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('测试弹窗');
|
||||||
|
expect(wrapper.text()).toContain('描述信息');
|
||||||
|
expect(wrapper.text()).toContain('预览内容');
|
||||||
|
expect(wrapper.text()).toContain('表单内容');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits confirm and cancel actions', async () => {
|
||||||
|
const wrapper = mount(EasyFlowFormModal, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
teleport: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '保存',
|
||||||
|
description: '描述信息',
|
||||||
|
open: true,
|
||||||
|
title: '测试弹窗',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<div>表单内容</div>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('button');
|
||||||
|
const cancelButton = buttons.find((button) => button.text() === '取消');
|
||||||
|
const confirmButton = buttons.find((button) => button.text() === '保存');
|
||||||
|
|
||||||
|
await cancelButton?.trigger('click');
|
||||||
|
await confirmButton?.trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('cancel')).toHaveLength(1);
|
||||||
|
expect(wrapper.emitted('confirm')).toHaveLength(1);
|
||||||
|
expect(wrapper.emitted('update:open')?.at(-1)).toEqual([false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
|
import { useEasyFlowModal } from '@easyflow-core/popup-ui';
|
||||||
|
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
|
||||||
|
import { cn } from '@easyflow-core/shared/utils';
|
||||||
|
|
||||||
|
type BeforeClose = () => boolean | Promise<boolean | void> | void;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
beforeClose?: BeforeClose;
|
||||||
|
cancelText?: string;
|
||||||
|
centered?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
closeOnClickModal?: boolean;
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
confirmText?: string;
|
||||||
|
description?: string;
|
||||||
|
draggable?: boolean;
|
||||||
|
modelValue?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
showCancelButton?: boolean;
|
||||||
|
showConfirmButton?: boolean;
|
||||||
|
showFooter?: boolean;
|
||||||
|
showPreview?: boolean;
|
||||||
|
submitting?: boolean;
|
||||||
|
title: string;
|
||||||
|
width?: 'lg' | 'md' | 'xl' | number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EasyFlowFormModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
beforeClose: undefined,
|
||||||
|
cancelText: '',
|
||||||
|
centered: false,
|
||||||
|
closable: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
confirmText: '',
|
||||||
|
description: '',
|
||||||
|
draggable: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
open: undefined,
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: true,
|
||||||
|
showFooter: true,
|
||||||
|
showPreview: false,
|
||||||
|
submitting: false,
|
||||||
|
width: 'lg',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
cancel: [];
|
||||||
|
confirm: [];
|
||||||
|
'update:modelValue': [boolean];
|
||||||
|
'update:open': [boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useEasyFlowModal({
|
||||||
|
async onBeforeClose() {
|
||||||
|
const result = await props.beforeClose?.();
|
||||||
|
return result !== false;
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
emit('update:modelValue', isOpen);
|
||||||
|
emit('update:open', isOpen);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const widthClassMap: Record<string, string> = {
|
||||||
|
'482': 'sm:w-[482px]',
|
||||||
|
'482px': 'sm:w-[482px]',
|
||||||
|
'50%': 'sm:w-[min(50vw,720px)]',
|
||||||
|
'500px': 'sm:w-[500px]',
|
||||||
|
'520px': 'sm:w-[520px]',
|
||||||
|
'550px': 'sm:w-[550px]',
|
||||||
|
'600px': 'sm:w-[600px]',
|
||||||
|
'762': 'sm:w-[762px]',
|
||||||
|
'762px': 'sm:w-[762px]',
|
||||||
|
'800px': 'sm:w-[800px]',
|
||||||
|
'80%': 'sm:w-[min(80vw,1120px)]',
|
||||||
|
'min(920px, 92vw)': 'sm:w-[min(92vw,920px)]',
|
||||||
|
'min(980px, 92vw)': 'sm:w-[min(92vw,980px)]',
|
||||||
|
md: 'sm:w-[560px]',
|
||||||
|
lg: 'sm:w-[720px]',
|
||||||
|
xl: 'sm:w-[960px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalClass = computed(() => {
|
||||||
|
const widthKey = String(props.width);
|
||||||
|
return cn(
|
||||||
|
'w-[calc(100vw-24px)] max-w-[calc(100vw-24px)] sm:max-w-[min(calc(100vw-48px),960px)]',
|
||||||
|
widthClassMap[widthKey] || widthClassMap.lg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open ?? props.modelValue ?? false,
|
||||||
|
(value) => {
|
||||||
|
modalApi.setState({ isOpen: value });
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:bordered="false"
|
||||||
|
:class="modalClass"
|
||||||
|
:centered="centered"
|
||||||
|
:closable="closable"
|
||||||
|
:close-on-click-modal="closeOnClickModal"
|
||||||
|
:confirm-loading="confirmLoading"
|
||||||
|
content-class="p-0"
|
||||||
|
:description="description"
|
||||||
|
:draggable="draggable"
|
||||||
|
:footer="showFooter"
|
||||||
|
:fullscreen-button="false"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
:submitting="submitting"
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<section
|
||||||
|
v-if="showPreview && $slots.preview"
|
||||||
|
class="bg-[linear-gradient(135deg,hsl(var(--modal-preview-surface))/0.96),hsl(var(--modal-preview-surface-strong))/0.88)] border-b border-[hsl(var(--modal-divider))/0.9] px-5 py-4 sm:px-6"
|
||||||
|
>
|
||||||
|
<slot name="preview"></slot>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="px-5 pb-5 pt-4 sm:px-6 sm:pb-6 sm:pt-5">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="showFooter" #footer>
|
||||||
|
<div class="flex w-full items-center justify-between gap-3">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<slot name="footer-extra"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<EasyFlowButton
|
||||||
|
v-if="showCancelButton"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
{{ cancelText || $t('button.cancel') }}
|
||||||
|
</EasyFlowButton>
|
||||||
|
<EasyFlowButton
|
||||||
|
v-if="showConfirmButton"
|
||||||
|
:disabled="submitting"
|
||||||
|
:loading="confirmLoading || submitting"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ confirmText || $t('button.confirm') }}
|
||||||
|
</EasyFlowButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as EasyFlowFormModal } from './form-modal.vue';
|
||||||
@@ -2,4 +2,6 @@ export * from './about';
|
|||||||
export * from './authentication';
|
export * from './authentication';
|
||||||
export * from './dashboard';
|
export * from './dashboard';
|
||||||
export * from './fallback';
|
export * from './fallback';
|
||||||
|
export * from './form-modal';
|
||||||
|
export * from './panel-modal';
|
||||||
export * from './profile';
|
export * from './profile';
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { EasyFlowPanelModal } from '..';
|
||||||
|
|
||||||
|
describe('easyFlowPanelModal', () => {
|
||||||
|
it('renders title, description and body content when opened', async () => {
|
||||||
|
const wrapper = mount(EasyFlowPanelModal, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
teleport: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '确认',
|
||||||
|
description: '描述信息',
|
||||||
|
open: true,
|
||||||
|
title: '面板弹窗',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<div>弹窗内容</div>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('面板弹窗');
|
||||||
|
expect(wrapper.text()).toContain('描述信息');
|
||||||
|
expect(wrapper.text()).toContain('弹窗内容');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports default footer actions and hide footer', async () => {
|
||||||
|
const wrapper = mount(EasyFlowPanelModal, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
teleport: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '确认',
|
||||||
|
description: '描述信息',
|
||||||
|
open: true,
|
||||||
|
title: '面板弹窗',
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<div>弹窗内容</div>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('button');
|
||||||
|
const cancelButton = buttons.find((button) => button.text() === '取消');
|
||||||
|
const confirmButton = buttons.find((button) => button.text() === '确认');
|
||||||
|
|
||||||
|
await cancelButton?.trigger('click');
|
||||||
|
await confirmButton?.trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('cancel')).toHaveLength(1);
|
||||||
|
expect(wrapper.emitted('confirm')).toHaveLength(1);
|
||||||
|
expect(wrapper.emitted('update:open')?.at(-1)).toEqual([false]);
|
||||||
|
|
||||||
|
await wrapper.setProps({ showFooter: false });
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.text()).not.toContain('取消');
|
||||||
|
expect(wrapper.text()).not.toContain('确认');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as EasyFlowPanelModal } from './panel-modal.vue';
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, useAttrs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
|
import { useEasyFlowModal } from '@easyflow-core/popup-ui';
|
||||||
|
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
|
||||||
|
import { cn } from '@easyflow-core/shared/utils';
|
||||||
|
|
||||||
|
type BeforeClose = () => boolean | Promise<boolean | void> | void;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
alignCenter?: boolean;
|
||||||
|
beforeClose?: BeforeClose;
|
||||||
|
cancelText?: string;
|
||||||
|
centered?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
closeOnClickModal?: boolean;
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
confirmText?: string;
|
||||||
|
description?: string;
|
||||||
|
destroyOnClose?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
modelValue?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
showCancelButton?: boolean;
|
||||||
|
showConfirmButton?: boolean;
|
||||||
|
showFooter?: boolean;
|
||||||
|
submitting?: boolean;
|
||||||
|
title: string;
|
||||||
|
width?: 'lg' | 'md' | 'xl' | number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EasyFlowPanelModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
alignCenter: false,
|
||||||
|
beforeClose: undefined,
|
||||||
|
cancelText: '',
|
||||||
|
centered: false,
|
||||||
|
closable: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
confirmText: '',
|
||||||
|
description: '',
|
||||||
|
destroyOnClose: true,
|
||||||
|
draggable: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
open: undefined,
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: true,
|
||||||
|
showFooter: true,
|
||||||
|
submitting: false,
|
||||||
|
width: 'lg',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
cancel: [];
|
||||||
|
confirm: [];
|
||||||
|
'update:modelValue': [boolean];
|
||||||
|
'update:open': [boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useEasyFlowModal({
|
||||||
|
async onBeforeClose() {
|
||||||
|
const result = await props.beforeClose?.();
|
||||||
|
return result !== false;
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
emit('update:modelValue', isOpen);
|
||||||
|
emit('update:open', isOpen);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const widthClassMap: Record<string, string> = {
|
||||||
|
'482': 'sm:w-[482px]',
|
||||||
|
'482px': 'sm:w-[482px]',
|
||||||
|
'50%': 'sm:w-[min(50vw,720px)]',
|
||||||
|
'500px': 'sm:w-[500px]',
|
||||||
|
'520px': 'sm:w-[520px]',
|
||||||
|
'550px': 'sm:w-[550px]',
|
||||||
|
'600px': 'sm:w-[600px]',
|
||||||
|
'762': 'sm:w-[762px]',
|
||||||
|
'762px': 'sm:w-[762px]',
|
||||||
|
'800px': 'sm:w-[800px]',
|
||||||
|
'80%': 'sm:w-[min(80vw,1120px)]',
|
||||||
|
'min(920px, 92vw)': 'sm:w-[min(92vw,920px)]',
|
||||||
|
'min(980px, 92vw)': 'sm:w-[min(92vw,980px)]',
|
||||||
|
md: 'sm:w-[560px]',
|
||||||
|
lg: 'sm:w-[720px]',
|
||||||
|
xl: 'sm:w-[960px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalClass = computed(() => {
|
||||||
|
const widthKey = String(props.width);
|
||||||
|
return cn(
|
||||||
|
'w-[calc(100vw-24px)] max-w-[calc(100vw-24px)] sm:max-w-[min(calc(100vw-48px),1120px)]',
|
||||||
|
widthClassMap[widthKey] || widthClassMap.lg,
|
||||||
|
attrs.class,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open ?? props.modelValue ?? false,
|
||||||
|
(value) => {
|
||||||
|
modalApi.setState({ isOpen: value });
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:class="modalClass"
|
||||||
|
:centered="centered || alignCenter"
|
||||||
|
:closable="closable"
|
||||||
|
:close-on-click-modal="closeOnClickModal"
|
||||||
|
:confirm-loading="confirmLoading"
|
||||||
|
:description="description"
|
||||||
|
:destroy-on-close="destroyOnClose"
|
||||||
|
:draggable="draggable"
|
||||||
|
:footer="showFooter"
|
||||||
|
:fullscreen-button="false"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
:submitting="submitting"
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
|
<template v-if="showFooter" #footer>
|
||||||
|
<slot name="footer">
|
||||||
|
<div class="flex w-full items-center justify-between gap-3">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<slot name="footer-extra"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<EasyFlowButton
|
||||||
|
v-if="showCancelButton"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
{{ cancelText || $t('button.cancel') }}
|
||||||
|
</EasyFlowButton>
|
||||||
|
<EasyFlowButton
|
||||||
|
v-if="showConfirmButton"
|
||||||
|
:disabled="submitting"
|
||||||
|
:loading="confirmLoading || submitting"
|
||||||
|
@click="handleConfirm"
|
||||||
|
>
|
||||||
|
{{ confirmText || $t('button.confirm') }}
|
||||||
|
</EasyFlowButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
import { useRefresh } from '@easyflow/hooks';
|
import { useRefresh } from '@easyflow/hooks';
|
||||||
@@ -34,6 +36,26 @@ withDefaults(defineProps<Props>(), {
|
|||||||
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||||
|
|
||||||
const REFERENCE_VALUE = 50;
|
const REFERENCE_VALUE = 50;
|
||||||
|
const toolbarButtonClass =
|
||||||
|
'mr-1 flex h-9 w-9 items-center justify-center rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]';
|
||||||
|
const toolbarButtonStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.8))',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
|
||||||
|
};
|
||||||
|
const breadcrumbShellStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--glass-tint) / 0.66)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--glass-tint) / 0.8), hsl(var(--nav-surface-subtle) / 0.58))',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
boxShadow:
|
||||||
|
'inset 0 1px 0 hsl(var(--nav-sheen) / 0.42), 0 18px 36px -30px hsl(var(--primary) / 0.14)',
|
||||||
|
};
|
||||||
|
const rightShellStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--glass-tint) / 0.28)',
|
||||||
|
};
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences();
|
const { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences();
|
||||||
@@ -120,61 +142,61 @@ function clearPreferencesAndLogout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template
|
<div
|
||||||
v-for="slot in leftSlots.filter((item) => item.index < REFERENCE_VALUE)"
|
v-if="$slots.breadcrumb"
|
||||||
:key="slot.name"
|
:style="breadcrumbShellStyle"
|
||||||
|
class="hidden min-w-0 max-w-[min(520px,46vw)] items-center rounded-full px-3 py-1.5 lg:flex"
|
||||||
>
|
>
|
||||||
|
<slot name="breadcrumb"></slot>
|
||||||
|
</div>
|
||||||
|
<template v-for="slot in leftSlots" :key="slot.name">
|
||||||
<slot :name="slot.name">
|
<slot :name="slot.name">
|
||||||
<template v-if="slot.name === 'refresh'">
|
<template v-if="slot.name === 'refresh'">
|
||||||
<EasyFlowIconButton class="my-0 mr-1 rounded-md" @click="refresh">
|
<EasyFlowIconButton
|
||||||
|
:class="toolbarButtonClass"
|
||||||
|
:style="toolbarButtonStyle"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
<RotateCw class="size-4" />
|
<RotateCw class="size-4" />
|
||||||
</EasyFlowIconButton>
|
</EasyFlowIconButton>
|
||||||
</template>
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-center hidden lg:block">
|
|
||||||
<slot name="breadcrumb"></slot>
|
|
||||||
</div>
|
|
||||||
<template
|
|
||||||
v-for="slot in leftSlots.filter((item) => item.index > REFERENCE_VALUE)"
|
|
||||||
:key="slot.name"
|
|
||||||
>
|
|
||||||
<slot :name="slot.name"></slot>
|
|
||||||
</template>
|
|
||||||
<div
|
<div
|
||||||
:class="`menu-align-${preferences.header.menuAlign}`"
|
:class="`menu-align-${preferences.header.menuAlign}`"
|
||||||
class="flex h-full min-w-0 flex-1 items-center"
|
class="flex h-full min-w-0 flex-1 items-center px-3"
|
||||||
>
|
>
|
||||||
<slot name="menu"></slot>
|
<slot name="menu"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
|
<div :style="rightShellStyle" class="flex h-full min-w-0 flex-shrink-0 items-center gap-1 rounded-full px-2 py-1 pl-2">
|
||||||
<template v-for="slot in rightSlots" :key="slot.name">
|
<template v-for="slot in rightSlots" :key="slot.name">
|
||||||
<slot :name="slot.name">
|
<slot :name="slot.name">
|
||||||
<template v-if="slot.name === 'global-search'">
|
<template v-if="slot.name === 'global-search'">
|
||||||
<GlobalSearch
|
<GlobalSearch
|
||||||
:enable-shortcut-key="globalSearchShortcutKey"
|
:enable-shortcut-key="globalSearchShortcutKey"
|
||||||
:menus="accessStore.accessMenus"
|
:menus="accessStore.accessMenus"
|
||||||
class="mr-1 sm:mr-4"
|
class="mr-0"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="slot.name === 'preferences'">
|
<template v-else-if="slot.name === 'preferences'">
|
||||||
<PreferencesButton
|
<PreferencesButton
|
||||||
class="mr-1"
|
:class="toolbarButtonClass"
|
||||||
|
:style="toolbarButtonStyle"
|
||||||
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'theme-toggle'">
|
<template v-else-if="slot.name === 'theme-toggle'">
|
||||||
<ThemeToggle class="mr-1 mt-[2px]" />
|
<ThemeToggle :class="toolbarButtonClass" :style="toolbarButtonStyle" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'language-toggle'">
|
<template v-else-if="slot.name === 'language-toggle'">
|
||||||
<LanguageToggle class="mr-1" />
|
<LanguageToggle :class="toolbarButtonClass" :style="toolbarButtonStyle" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'fullscreen'">
|
<template v-else-if="slot.name === 'fullscreen'">
|
||||||
<EasyFlowFullScreen class="mr-1" />
|
<EasyFlowFullScreen :class="toolbarButtonClass" :style="toolbarButtonStyle" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="slot.name === 'timezone'">
|
<template v-else-if="slot.name === 'timezone'">
|
||||||
<TimezoneButton class="mr-1 mt-[2px]" />
|
<TimezoneButton :class="toolbarButtonClass" :style="toolbarButtonStyle" />
|
||||||
</template>
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ const headerSlots = computed(() => {
|
|||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
:hide-when-only-one="preferences.breadcrumb.hideOnlyOne"
|
:hide-when-only-one="preferences.breadcrumb.hideOnlyOne"
|
||||||
:show-home="preferences.breadcrumb.showHome"
|
:show-home="preferences.breadcrumb.showHome"
|
||||||
:show-icon="preferences.breadcrumb.showIcon"
|
:show-icon="false"
|
||||||
:type="preferences.breadcrumb.styleType"
|
:type="preferences.breadcrumb.styleType"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ if (!preferences.tabbar.persist) {
|
|||||||
@unpin="unpinTab"
|
@unpin="unpinTab"
|
||||||
@update:active="handleClick"
|
@update:active="handleClick"
|
||||||
/>
|
/>
|
||||||
<div class="flex-center h-full">
|
<div class="flex-center h-full gap-1 pr-2">
|
||||||
<TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" />
|
<TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" />
|
||||||
<TabsToolScreen
|
<TabsToolScreen
|
||||||
v-if="preferences.tabbar.showMaximize"
|
v-if="preferences.tabbar.showMaximize"
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const breadcrumbs = computed((): IBreadcrumb[] => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultBreadcrumb;
|
return resultBreadcrumb.slice(-2);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSelect(path: string) {
|
function handleSelect(path: string) {
|
||||||
@@ -68,7 +68,7 @@ function handleSelect(path: string) {
|
|||||||
:breadcrumbs="breadcrumbs"
|
:breadcrumbs="breadcrumbs"
|
||||||
:show-icon="showIcon"
|
:show-icon="showIcon"
|
||||||
:style-type="type"
|
:style-type="type"
|
||||||
class="ml-2"
|
class="min-w-0"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import type { MenuRecordRaw } from '@easyflow/types';
|
import type { MenuRecordRaw } from '@easyflow/types';
|
||||||
|
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
import { Search, X } from '@easyflow/icons';
|
||||||
ArrowDown,
|
|
||||||
ArrowUp,
|
|
||||||
CornerDownLeft,
|
|
||||||
MdiKeyboardEsc,
|
|
||||||
Search,
|
|
||||||
} from '@easyflow/icons';
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import { isWindowsOs } from '@easyflow/utils';
|
import { isWindowsOs } from '@easyflow/utils';
|
||||||
|
|
||||||
import { useEasyFlowModal } from '@easyflow-core/popup-ui';
|
import { onClickOutside, useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
|
||||||
|
|
||||||
import SearchPanel from './search-panel.vue';
|
import SearchPanel from './search-panel.vue';
|
||||||
|
|
||||||
@@ -32,39 +26,127 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const hasResults = ref(false);
|
||||||
|
const searchRootRef = ref<HTMLElement>();
|
||||||
|
const searchPanelRef = ref<HTMLElement>();
|
||||||
const searchInputRef = ref<HTMLInputElement>();
|
const searchInputRef = ref<HTMLInputElement>();
|
||||||
|
const panelStyle = ref<CSSProperties>({});
|
||||||
|
const shouldMountPanel = ref(false);
|
||||||
|
|
||||||
const [Modal, modalApi] = useEasyFlowModal({
|
const searchShellStyle: CSSProperties = {
|
||||||
onCancel() {
|
backgroundColor: 'hsl(var(--surface-panel) / 0.18)',
|
||||||
modalApi.close();
|
backgroundImage:
|
||||||
},
|
'linear-gradient(180deg, hsl(var(--glass-border) / 0.4), hsl(var(--glass-tint) / 0.2) 18%, hsl(var(--surface-panel) / 0.12) 100%)',
|
||||||
onOpenChange(isOpen: boolean) {
|
backdropFilter: 'blur(calc(var(--glass-blur) * 1.25)) saturate(178%)',
|
||||||
if (!isOpen) {
|
border: '1px solid hsl(var(--glass-border) / 0.42)',
|
||||||
keyword.value = '';
|
boxShadow:
|
||||||
|
'inset 0 1px 0 hsl(var(--glass-border) / 0.44), var(--shadow-toolbar), 0 12px 24px -18px hsl(var(--nav-ambient) / 0.18)',
|
||||||
|
};
|
||||||
|
const searchPanelStyle: CSSProperties = {
|
||||||
|
backgroundColor: 'hsl(var(--surface-panel) / 0.22)',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(180deg, hsl(var(--glass-border) / 0.42), hsl(var(--glass-tint) / 0.18) 16%, hsl(var(--surface-panel) / 0.16) 100%)',
|
||||||
|
backdropFilter: 'blur(calc(var(--glass-blur) * 1.9)) saturate(190%)',
|
||||||
|
border: '1px solid hsl(var(--glass-border) / 0.44)',
|
||||||
|
boxShadow:
|
||||||
|
'inset 0 1px 0 hsl(var(--glass-border) / 0.42), var(--shadow-float), 0 16px 32px -24px hsl(var(--nav-ambient) / 0.14)',
|
||||||
|
zIndex: 'calc(var(--popup-z-index) + 10)',
|
||||||
|
};
|
||||||
|
const expandedWidth = 'clamp(10rem, 14vw, 12rem)';
|
||||||
|
|
||||||
|
function updatePanelPosition() {
|
||||||
|
const root = searchRootRef.value;
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
const rect = root.getBoundingClientRect();
|
||||||
});
|
const viewportWidth = window.innerWidth;
|
||||||
const open = modalApi.useStore((state) => state.isOpen);
|
const desiredWidth = Math.min(420, Math.max(300, rect.width + 120));
|
||||||
|
const width = Math.min(desiredWidth, viewportWidth - 16);
|
||||||
|
const right = Math.max(8, viewportWidth - rect.right);
|
||||||
|
const left = Math.max(8, viewportWidth - right - width);
|
||||||
|
|
||||||
|
panelStyle.value = {
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${rect.bottom + 10}px`,
|
||||||
|
width: `${Math.min(width, viewportWidth - left - 8)}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearchInput() {
|
||||||
|
nextTick(() => {
|
||||||
|
searchInputRef.value?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSearch() {
|
||||||
|
if (isOpen.value) {
|
||||||
|
focusSearchInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
modalApi.close();
|
isOpen.value = false;
|
||||||
|
hasResults.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOpen() {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isOpen, (open) => {
|
||||||
|
if (open) {
|
||||||
|
shouldMountPanel.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
updatePanelPosition();
|
||||||
|
focusSearchInput();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
|
shouldMountPanel.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onClickOutside(
|
||||||
|
searchRootRef,
|
||||||
|
() => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ ignore: [searchPanelRef] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleViewportChange = () => {
|
||||||
|
if (isOpen.value && hasResults.value) {
|
||||||
|
updatePanelPosition();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(keyword, () => {
|
||||||
|
if (isOpen.value && hasResults.value) {
|
||||||
|
nextTick(updatePanelPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResultsChange(count: number) {
|
||||||
|
hasResults.value = count > 0;
|
||||||
|
if (isOpen.value && shouldMountPanel.value) {
|
||||||
|
nextTick(updatePanelPosition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = useMagicKeys();
|
const keys = useMagicKeys();
|
||||||
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
|
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
|
||||||
whenever(cmd!, () => {
|
whenever(cmd!, () => {
|
||||||
if (props.enableShortcutKey) {
|
if (props.enableShortcutKey) {
|
||||||
modalApi.open();
|
openSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
whenever(open, () => {
|
|
||||||
nextTick(() => {
|
|
||||||
searchInputRef.value?.focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const preventDefaultBrowserSearchHotKey = (event: KeyboardEvent) => {
|
const preventDefaultBrowserSearchHotKey = (event: KeyboardEvent) => {
|
||||||
if (event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
|
if (event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -79,79 +161,91 @@ const toggleKeydownListener = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleOpen = () => {
|
|
||||||
open.value ? modalApi.close() : modalApi.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(() => props.enableShortcutKey, toggleKeydownListener);
|
watch(() => props.enableShortcutKey, toggleKeydownListener);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
toggleKeydownListener();
|
toggleKeydownListener();
|
||||||
|
window.addEventListener('resize', handleViewportChange);
|
||||||
|
window.addEventListener('scroll', handleViewportChange, true);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
|
window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
|
||||||
|
window.removeEventListener('resize', handleViewportChange);
|
||||||
|
window.removeEventListener('scroll', handleViewportChange, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div ref="searchRootRef" class="relative flex items-center justify-end">
|
||||||
<Modal
|
<div
|
||||||
:fullscreen-button="false"
|
:style="[searchShellStyle, { width: isOpen ? expandedWidth : '2.25rem' }]"
|
||||||
class="w-[600px]"
|
class="group flex h-9 items-center overflow-hidden rounded-2xl transition-[width,background-color,box-shadow,transform,border-color] duration-200 ease-out"
|
||||||
header-class="py-2 border-b"
|
:class="
|
||||||
|
isOpen
|
||||||
|
? '-translate-y-0.5 cursor-text border-[hsl(var(--glass-border))/0.5]'
|
||||||
|
: 'cursor-pointer hover:-translate-y-0.5 hover:border-[hsl(var(--glass-border))/0.52]'
|
||||||
|
"
|
||||||
|
@click="openSearch"
|
||||||
>
|
>
|
||||||
<template #title>
|
<button
|
||||||
<div class="flex items-center">
|
type="button"
|
||||||
<Search class="text-muted-foreground mr-2 size-4" />
|
class="hover:text-foreground flex h-full w-9 flex-shrink-0 items-center justify-center text-[hsl(var(--nav-item-muted-foreground))] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
|
||||||
|
:aria-expanded="isOpen"
|
||||||
|
:aria-label="$t('ui.widgets.search.searchNavigate')"
|
||||||
|
@click.stop="toggleOpen"
|
||||||
|
>
|
||||||
|
<Search class="size-4" />
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
:placeholder="$t('ui.widgets.search.searchNavigate')"
|
:placeholder="$t('ui.widgets.search.searchNavigate')"
|
||||||
class="ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 pl-0 text-sm font-normal outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent"
|
class="placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent pr-2 text-sm font-medium text-[hsl(var(--text-strong))] outline-none transition-opacity duration-150"
|
||||||
|
:class="isOpen ? 'opacity-100' : 'pointer-events-none w-0 opacity-0'"
|
||||||
|
@keydown.esc.prevent.stop="handleClose"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="isOpen"
|
||||||
|
type="button"
|
||||||
|
class="hover:text-foreground mr-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-xl text-[hsl(var(--text-muted))] outline-none transition-colors hover:bg-[hsl(var(--nav-item-hover))/0.92] focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
|
||||||
|
:aria-label="$t('ui.widgets.search.close')"
|
||||||
|
@click.stop="handleClose"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<SearchPanel :keyword="keyword" :menus="menus" @close="handleClose" />
|
<Teleport to="body">
|
||||||
<template #footer>
|
<transition
|
||||||
<div class="flex w-full justify-start text-xs">
|
enter-active-class="transition duration-200 ease-out"
|
||||||
<div class="mr-2 flex items-center">
|
enter-from-class="translate-y-1.5 opacity-0"
|
||||||
<CornerDownLeft class="mr-1 size-3" />
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
{{ $t('ui.widgets.search.select') }}
|
leave-active-class="transition duration-150 ease-in"
|
||||||
</div>
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
<div class="mr-2 flex items-center">
|
leave-to-class="translate-y-1 opacity-0"
|
||||||
<ArrowUp class="mr-1 size-3" />
|
>
|
||||||
<ArrowDown class="mr-1 size-3" />
|
|
||||||
{{ $t('ui.widgets.search.navigate') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<MdiKeyboardEsc class="mr-1 size-3" />
|
|
||||||
{{ $t('ui.widgets.search.close') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
<div
|
<div
|
||||||
class="md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none"
|
v-if="isOpen && shouldMountPanel"
|
||||||
@click="toggleOpen()"
|
ref="searchPanelRef"
|
||||||
|
:style="[searchPanelStyle, panelStyle]"
|
||||||
|
class="duration-180 fixed overflow-hidden rounded-[28px] transition-[opacity,transform] ease-out"
|
||||||
|
:class="
|
||||||
|
hasResults
|
||||||
|
? 'pointer-events-auto opacity-100'
|
||||||
|
: 'pointer-events-none opacity-0'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<Search
|
<div class="max-h-[420px] px-2 py-2">
|
||||||
class="text-muted-foreground group-hover:text-foreground size-4 group-hover:opacity-100"
|
<SearchPanel
|
||||||
|
:keyword="keyword"
|
||||||
|
:menus="menus"
|
||||||
|
@close="handleClose"
|
||||||
|
@results-change="handleResultsChange"
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
class="text-muted-foreground group-hover:text-foreground hidden text-xs duration-300 md:block"
|
|
||||||
>
|
|
||||||
{{ $t('ui.widgets.search.title') }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="enableShortcutKey"
|
|
||||||
class="bg-background border-foreground/60 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block"
|
|
||||||
>
|
|
||||||
{{ isWindowsOs() ? 'Ctrl' : '⌘' }}
|
|
||||||
<kbd>K</kbd>
|
|
||||||
</span>
|
|
||||||
<span v-else></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MenuRecordRaw } from '@easyflow/types';
|
import type { MenuRecordRaw } from '@easyflow/types';
|
||||||
|
|
||||||
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { SearchX, X } from '@easyflow/icons';
|
import { SearchX } from '@easyflow/icons';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import { mapTree, traverseTreeValues, uniqueByField } from '@easyflow/utils';
|
import { mapTree, traverseTreeValues, uniqueByField } from '@easyflow/utils';
|
||||||
|
|
||||||
import { EasyFlowIcon, EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
|
import { EasyFlowIcon, EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
|
||||||
import { isHttpUrl } from '@easyflow-core/shared/utils';
|
import { isHttpUrl } from '@easyflow-core/shared/utils';
|
||||||
|
|
||||||
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
|
import { onKeyStroke, useThrottleFn } from '@vueuse/core';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'SearchPanel',
|
name: 'SearchPanel',
|
||||||
@@ -24,58 +24,39 @@ const props = withDefaults(
|
|||||||
menus: () => [],
|
menus: () => [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const emit = defineEmits<{ close: [] }>();
|
const emit = defineEmits<{ close: []; resultsChange: [count: number] }>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchHistory = useLocalStorage<MenuRecordRaw[]>(
|
|
||||||
`__search-history-${location.hostname}__`,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const activeIndex = ref(-1);
|
const activeIndex = ref(-1);
|
||||||
const searchItems = shallowRef<MenuRecordRaw[]>([]);
|
const searchItems = shallowRef<MenuRecordRaw[]>([]);
|
||||||
const searchResults = ref<MenuRecordRaw[]>([]);
|
const searchResults = ref<MenuRecordRaw[]>([]);
|
||||||
|
const displayResults = computed(() =>
|
||||||
|
uniqueByField(searchResults.value, 'path'),
|
||||||
|
);
|
||||||
|
|
||||||
const handleSearch = useThrottleFn(search, 200);
|
const handleSearch = useThrottleFn(search, 200);
|
||||||
|
|
||||||
// 搜索函数,用于根据搜索关键词查找匹配的菜单项
|
|
||||||
function search(searchKey: string) {
|
function search(searchKey: string) {
|
||||||
// 去除搜索关键词的前后空格
|
const normalizedKey = searchKey.trim();
|
||||||
searchKey = searchKey.trim();
|
if (!normalizedKey) {
|
||||||
|
|
||||||
// 如果搜索关键词为空,清空搜索结果并返回
|
|
||||||
if (!searchKey) {
|
|
||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
|
activeIndex.value = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用搜索关键词创建正则表达式
|
const reg = createSearchReg(normalizedKey);
|
||||||
const reg = createSearchReg(searchKey);
|
|
||||||
|
|
||||||
// 初始化结果数组
|
|
||||||
const results: MenuRecordRaw[] = [];
|
const results: MenuRecordRaw[] = [];
|
||||||
|
|
||||||
// 遍历搜索项
|
|
||||||
traverseTreeValues(searchItems.value, (item) => {
|
traverseTreeValues(searchItems.value, (item) => {
|
||||||
// 如果菜单项的名称匹配正则表达式,将其添加到结果数组中
|
|
||||||
if (reg.test(item.name?.toLowerCase())) {
|
if (reg.test(item.name?.toLowerCase())) {
|
||||||
results.push(item);
|
results.push(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新搜索结果
|
searchResults.value = uniqueByField(results, 'path');
|
||||||
searchResults.value = results;
|
activeIndex.value = searchResults.value.length > 0 ? 0 : -1;
|
||||||
|
|
||||||
// 如果有搜索结果,设置索引为 0
|
|
||||||
if (results.length > 0) {
|
|
||||||
activeIndex.value = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 赋值索引为 0
|
|
||||||
activeIndex.value = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the keyboard up and down keys move to an invisible place
|
|
||||||
// the scroll bar needs to scroll automatically
|
|
||||||
function scrollIntoView() {
|
function scrollIntoView() {
|
||||||
const element = document.querySelector(
|
const element = document.querySelector(
|
||||||
`[data-search-item="${activeIndex.value}"]`,
|
`[data-search-item="${activeIndex.value}"]`,
|
||||||
@@ -86,76 +67,87 @@ function scrollIntoView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter keyboard event
|
|
||||||
async function handleEnter() {
|
async function handleEnter() {
|
||||||
if (searchResults.value.length === 0) {
|
if (displayResults.value.length === 0 || activeIndex.value < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = searchResults.value;
|
await handleSelect(activeIndex.value);
|
||||||
const index = activeIndex.value;
|
}
|
||||||
if (result.length === 0 || index < 0) {
|
|
||||||
|
async function handleSelect(index: number) {
|
||||||
|
const to = displayResults.value[index];
|
||||||
|
if (!to) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const to = result[index];
|
emit('close');
|
||||||
if (to) {
|
|
||||||
searchHistory.value = uniqueByField([...searchHistory.value, to], 'path');
|
|
||||||
handleClose();
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (isHttpUrl(to.path)) {
|
if (isHttpUrl(to.path)) {
|
||||||
window.open(to.path, '_blank');
|
window.open(to.path, '_blank');
|
||||||
} else {
|
} else {
|
||||||
router.push({ path: to.path, replace: true });
|
router.push({ path: to.path, replace: true });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow key up
|
|
||||||
function handleUp() {
|
function handleUp() {
|
||||||
if (searchResults.value.length === 0) {
|
if (displayResults.value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeIndex.value--;
|
activeIndex.value--;
|
||||||
if (activeIndex.value < 0) {
|
if (activeIndex.value < 0) {
|
||||||
activeIndex.value = searchResults.value.length - 1;
|
activeIndex.value = displayResults.value.length - 1;
|
||||||
}
|
}
|
||||||
scrollIntoView();
|
scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow key down
|
|
||||||
function handleDown() {
|
function handleDown() {
|
||||||
if (searchResults.value.length === 0) {
|
if (displayResults.value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeIndex.value++;
|
activeIndex.value++;
|
||||||
if (activeIndex.value > searchResults.value.length - 1) {
|
if (activeIndex.value > displayResults.value.length - 1) {
|
||||||
activeIndex.value = 0;
|
activeIndex.value = 0;
|
||||||
}
|
}
|
||||||
scrollIntoView();
|
scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// close search modal
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
searchResults.value = [];
|
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate when the mouse moves to a certain line
|
function handleMouseenter(index: number) {
|
||||||
function handleMouseenter(e: MouseEvent) {
|
activeIndex.value = index;
|
||||||
const index = (e.target as HTMLElement)?.dataset.index;
|
|
||||||
activeIndex.value = Number(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeItem(index: number) {
|
function getHighlightedChunks(text: string, keyword: string) {
|
||||||
if (props.keyword) {
|
const sourceChars = [...text];
|
||||||
searchResults.value.splice(index, 1);
|
const lowerChars = [...text.toLowerCase()];
|
||||||
} else {
|
const queryChars = [...keyword.trim().toLowerCase()];
|
||||||
searchHistory.value.splice(index, 1);
|
|
||||||
|
if (queryChars.length === 0) {
|
||||||
|
return [{ text, matched: false }];
|
||||||
}
|
}
|
||||||
activeIndex.value = Math.max(activeIndex.value - 1, 0);
|
|
||||||
scrollIntoView();
|
const chunks: Array<{ matched: boolean; text: string }> = [];
|
||||||
|
let queryIndex = 0;
|
||||||
|
|
||||||
|
sourceChars.forEach((char, index) => {
|
||||||
|
const matched =
|
||||||
|
queryIndex < queryChars.length &&
|
||||||
|
lowerChars[index] === queryChars[queryIndex];
|
||||||
|
if (matched) {
|
||||||
|
queryIndex++;
|
||||||
|
}
|
||||||
|
const previousChunk = chunks[chunks.length - 1];
|
||||||
|
if (previousChunk && previousChunk.matched === matched) {
|
||||||
|
previousChunk.text += char;
|
||||||
|
} else {
|
||||||
|
chunks.push({ matched, text: char });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储所有需要转义的特殊字符
|
|
||||||
const code = new Set([
|
const code = new Set([
|
||||||
'$',
|
'$',
|
||||||
'(',
|
'(',
|
||||||
@@ -173,20 +165,12 @@ const code = new Set([
|
|||||||
'}',
|
'}',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 转换函数,用于转义特殊字符
|
|
||||||
function transform(c: string) {
|
function transform(c: string) {
|
||||||
// 如果字符在特殊字符列表中,返回转义后的字符
|
|
||||||
// 如果不在,返回字符本身
|
|
||||||
return code.has(c) ? `\\${c}` : c;
|
return code.has(c) ? `\\${c}` : c;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建搜索正则表达式
|
|
||||||
function createSearchReg(key: string) {
|
function createSearchReg(key: string) {
|
||||||
// 将输入的字符串拆分为单个字符
|
|
||||||
// 对每个字符进行转义
|
|
||||||
// 然后用'.*'连接所有字符,创建正则表达式
|
|
||||||
const keys = [...key].map((item) => transform(item)).join('.*');
|
const keys = [...key].map((item) => transform(item)).join('.*');
|
||||||
// 返回创建的正则表达式
|
|
||||||
return new RegExp(`.*${keys}.*`);
|
return new RegExp(`.*${keys}.*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,11 +180,20 @@ watch(
|
|||||||
if (val) {
|
if (val) {
|
||||||
handleSearch(val);
|
handleSearch(val);
|
||||||
} else {
|
} else {
|
||||||
searchResults.value = [...searchHistory.value];
|
searchResults.value = [];
|
||||||
|
activeIndex.value = -1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => displayResults.value.length,
|
||||||
|
(count) => {
|
||||||
|
emit('resultsChange', count);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
searchItems.value = mapTree(props.menus, (item) => {
|
searchItems.value = mapTree(props.menus, (item) => {
|
||||||
return {
|
return {
|
||||||
@@ -208,81 +201,67 @@ onMounted(() => {
|
|||||||
name: $t(item?.name),
|
name: $t(item?.name),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (searchHistory.value.length > 0) {
|
|
||||||
searchResults.value = searchHistory.value;
|
|
||||||
}
|
|
||||||
// enter search
|
|
||||||
onKeyStroke('Enter', handleEnter);
|
onKeyStroke('Enter', handleEnter);
|
||||||
// Monitor keyboard arrow keys
|
|
||||||
onKeyStroke('ArrowUp', handleUp);
|
onKeyStroke('ArrowUp', handleUp);
|
||||||
onKeyStroke('ArrowDown', handleDown);
|
onKeyStroke('ArrowDown', handleDown);
|
||||||
// esc close
|
|
||||||
onKeyStroke('Escape', handleClose);
|
onKeyStroke('Escape', handleClose);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<EasyFlowScrollbar>
|
<EasyFlowScrollbar>
|
||||||
<div class="!flex h-full justify-center px-2 sm:max-h-[450px]">
|
<div class="px-2 py-1 sm:max-h-[420px]">
|
||||||
<!-- 无搜索结果 -->
|
<ul v-if="displayResults.length > 0" class="space-y-2">
|
||||||
<div
|
|
||||||
v-if="keyword && searchResults.length === 0"
|
|
||||||
class="text-muted-foreground text-center"
|
|
||||||
>
|
|
||||||
<SearchX class="mx-auto mt-4 size-12" />
|
|
||||||
<p class="mb-10 mt-6 text-xs">
|
|
||||||
{{ $t('ui.widgets.search.noResults') }}
|
|
||||||
<span class="text-foreground text-sm font-medium">
|
|
||||||
"{{ keyword }}"
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- 历史搜索记录 & 没有搜索结果 -->
|
|
||||||
<div
|
|
||||||
v-if="!keyword && searchResults.length === 0"
|
|
||||||
class="text-muted-foreground text-center"
|
|
||||||
>
|
|
||||||
<p class="my-10 text-xs">
|
|
||||||
{{ $t('ui.widgets.search.noRecent') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-show="searchResults.length > 0" class="w-full">
|
|
||||||
<li
|
<li
|
||||||
v-if="searchHistory.length > 0 && !keyword"
|
v-for="(item, index) in displayResults"
|
||||||
class="text-muted-foreground mb-2 text-xs"
|
|
||||||
>
|
|
||||||
{{ $t('ui.widgets.search.recent') }}
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-for="(item, index) in uniqueByField(searchResults, 'path')"
|
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:class="
|
:class="
|
||||||
activeIndex === index
|
activeIndex === index
|
||||||
? 'active bg-primary text-primary-foreground'
|
? 'bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.22,hsl(var(--glass-tint))/0.1_18%,hsl(var(--surface-panel))/0.12)] shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.34,0_18px_30px_-24px_hsl(var(--primary)/0.12)] ring-1 ring-[hsl(var(--glass-border))/0.26]'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
:data-search-item="index"
|
:data-search-item="index"
|
||||||
class="bg-accent flex-center group mb-3 w-full cursor-pointer rounded-lg px-4 py-4"
|
class="group flex w-full cursor-pointer items-center gap-3 rounded-2xl bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.14,hsl(var(--glass-tint))/0.08_18%,hsl(var(--surface-panel))/0.08)] px-4 py-3.5 backdrop-blur-2xl transition-[transform,background-color,box-shadow] hover:-translate-y-0.5 hover:bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.18,hsl(var(--glass-tint))/0.12_18%,hsl(var(--surface-panel))/0.1)] hover:shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.28,0_16px_28px_-24px_hsl(var(--primary)/0.1)]"
|
||||||
@click="handleEnter"
|
@click="handleSelect(index)"
|
||||||
@mouseenter="handleMouseenter"
|
@mouseenter="handleMouseenter(index)"
|
||||||
>
|
>
|
||||||
<EasyFlowIcon
|
|
||||||
:icon="item.icon"
|
|
||||||
class="mr-2 size-5 flex-shrink-0"
|
|
||||||
fallback
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span class="flex-1">{{ item.name }}</span>
|
|
||||||
<div
|
<div
|
||||||
class="flex-center dark:hover:bg-accent hover:text-primary-foreground rounded-full p-1 hover:scale-110"
|
class="flex size-10 flex-shrink-0 items-center justify-center rounded-2xl bg-[linear-gradient(180deg,hsl(var(--glass-border))/0.24,hsl(var(--nav-item-active))/0.09_22%,hsl(var(--glass-tint))/0.14)] text-[hsl(var(--nav-item-active-foreground))] shadow-[inset_0_1px_0_hsl(var(--glass-border))/0.36]"
|
||||||
@click.stop="removeItem(index)"
|
|
||||||
>
|
>
|
||||||
<X class="size-4" />
|
<EasyFlowIcon :icon="item.icon" class="size-5" fallback />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<span
|
||||||
|
class="block truncate text-sm font-medium leading-6 text-[hsl(var(--text-strong))]"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(chunk, chunkIndex) in getHighlightedChunks(
|
||||||
|
item.name || '',
|
||||||
|
keyword,
|
||||||
|
)"
|
||||||
|
:key="`${item.path}-${chunkIndex}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
chunk.matched
|
||||||
|
? activeIndex === index
|
||||||
|
? 'rounded-md bg-[hsl(var(--primary))/0.1] px-0.5 font-semibold text-current'
|
||||||
|
: 'rounded-md bg-[hsl(var(--primary))/0.08] px-0.5 font-semibold text-[hsl(var(--primary))]'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ chunk.text }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div v-else class="hidden">
|
||||||
|
<SearchX class="size-0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EasyFlowScrollbar>
|
</EasyFlowScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user