优化pagetype列表页基础组件,支持列表页工具栏两种覆盖接口props和context
This commit is contained in:
parent
e1b379971e
commit
5113228bc8
@ -10,7 +10,7 @@
|
||||
<h2>{{ t(title) }}</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- 工具栏覆盖组件 -->
|
||||
<!-- 工具栏覆盖组件 - 支持两种接口:context 或 props -->
|
||||
<component
|
||||
v-if="toolbarComponent"
|
||||
:is="toolbarComponent"
|
||||
@ -28,6 +28,16 @@
|
||||
router,
|
||||
t
|
||||
}"
|
||||
:entity="entity"
|
||||
:search-query="searchQuery"
|
||||
:view-mode="viewMode"
|
||||
:selected-keys="selectedKeys"
|
||||
:loading="loading"
|
||||
@update:search-query="searchQuery = $event"
|
||||
@update:view-mode="viewMode = $event"
|
||||
@reload="reload"
|
||||
@create="createRecordHandler"
|
||||
@delete-selected="handleDeleteSelected"
|
||||
/>
|
||||
<GenericListPageToolBar
|
||||
v-else
|
||||
|
||||
@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="toolbar">
|
||||
<div class="filters">
|
||||
<n-input
|
||||
v-model:value="searchQueryModel"
|
||||
:placeholder="t('Search')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: viewModeModel === 'list' }"
|
||||
@click="viewModeModel = 'list'"
|
||||
:title="t('List View')"
|
||||
>
|
||||
<i class="fa fa-list"></i>
|
||||
</button>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: viewModeModel === 'card' }"
|
||||
@click="viewModeModel = 'card'"
|
||||
:title="t('Card View')"
|
||||
>
|
||||
<i class="fa fa-th-large"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="refresh-btn" @click="reload" :disabled="loading">
|
||||
<i :class="loading ? 'fa fa-spinner fa-spin' : 'fa fa-refresh'"></i>
|
||||
</button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="menuOptions"
|
||||
@select="onMenuSelect"
|
||||
>
|
||||
<button class="refresh-btn" :disabled="loading || importing" :title="t('More')">
|
||||
<i :class="importing ? 'fa fa-ellipsis-h fa-spin' : 'fa fa-ellipsis-h'"></i>
|
||||
</button>
|
||||
</n-dropdown>
|
||||
<button
|
||||
v-if="selectedKeys.length === 0"
|
||||
class="create-btn"
|
||||
@click="createRecordHandler"
|
||||
:disabled="loading"
|
||||
>
|
||||
<i class="fa fa-plus"></i>
|
||||
{{ t('Create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="delete-btn"
|
||||
@click="handleDeleteSelected"
|
||||
:disabled="loading"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
{{ t('Delete Selected') }} ({{ selectedKeys.length }})
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { NInput, NDropdown, useDialog } from 'naive-ui'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { importLocalNodes } from '@/shared/api/nodes'
|
||||
|
||||
interface Props {
|
||||
entity: string
|
||||
searchQuery: string
|
||||
viewMode: 'card' | 'list'
|
||||
selectedKeys: string[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:searchQuery', value: string): void
|
||||
(e: 'update:viewMode', value: 'card' | 'list'): void
|
||||
(e: 'reload'): void
|
||||
(e: 'create'): void
|
||||
(e: 'delete-selected'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const dialog = useDialog()
|
||||
const importing = ref(false)
|
||||
|
||||
// 使用 computed 来处理双向绑定
|
||||
const searchQueryModel = computed({
|
||||
get: () => props.searchQuery,
|
||||
set: (value) => emit('update:searchQuery', value)
|
||||
})
|
||||
|
||||
const viewModeModel = computed({
|
||||
get: () => props.viewMode,
|
||||
set: (value) => emit('update:viewMode', value)
|
||||
})
|
||||
|
||||
function reload() {
|
||||
emit('reload')
|
||||
}
|
||||
|
||||
function createRecordHandler() {
|
||||
emit('create')
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
emit('delete-selected')
|
||||
}
|
||||
|
||||
// 顶部菜单
|
||||
const menuOptions = computed(() => [
|
||||
{ label: t('Import local nodes'), key: 'import-local-nodes' }
|
||||
])
|
||||
|
||||
function onMenuSelect(key: string) {
|
||||
if (key === 'import-local-nodes') {
|
||||
handleImportLocalNodes()
|
||||
}
|
||||
}
|
||||
|
||||
// 一键导入本地节点
|
||||
async function handleImportLocalNodes() {
|
||||
if (importing.value) return
|
||||
importing.value = true
|
||||
try {
|
||||
const res = await importLocalNodes()
|
||||
emit('reload')
|
||||
if (!res?.success) {
|
||||
dialog.error({ title: t('Error'), content: t('Import failed') })
|
||||
return
|
||||
}
|
||||
const matchedCount = res.matched || 0
|
||||
const importedCount = res.imported || 0
|
||||
const skippedExisting = res.skipped_existing || 0
|
||||
if (matchedCount === 0) {
|
||||
dialog.info({ title: t('Info'), content: t('No local node definitions found') })
|
||||
} else if (importedCount === 0 && skippedExisting > 0) {
|
||||
dialog.info({ title: t('Info'), content: t('All local nodes already exist') })
|
||||
} else if (importedCount > 0) {
|
||||
dialog.success({ title: t('Success'), content: `${t('Imported')} ${importedCount} ${t('nodes')}` })
|
||||
} else {
|
||||
dialog.info({ title: t('Info'), content: t('No new local nodes found') })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Import local nodes error:', e)
|
||||
dialog.error({ title: t('Error'), content: t('Import failed') })
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 视图切换按钮 */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* 切换按钮 - 使用灰色系 */
|
||||
.toggle-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: #e2e8f0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* 刷新按钮 */
|
||||
.refresh-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.refresh-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled:hover {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 新建按钮 - 使用柔和的品牌色系,与整体风格协调 */
|
||||
.create-btn {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #1fc76f;
|
||||
border-radius: 8px;
|
||||
background: #e6f8f0;
|
||||
color: #0d684b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background: #dcfce7;
|
||||
border-color: #1fc76f;
|
||||
color: #166534;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15);
|
||||
}
|
||||
|
||||
.create-btn:active {
|
||||
background: #1fc76f;
|
||||
border-color: #1fc76f;
|
||||
color: white;
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2);
|
||||
}
|
||||
|
||||
.create-btn:disabled {
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.create-btn:disabled:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.create-btn i {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 删除选中按钮 */
|
||||
.delete-btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.delete-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.delete-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.delete-btn i {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user