refactor(audit): reuse list shells and split models

This commit is contained in:
caoxiaozhu
2026-05-29 10:13:49 +08:00
parent 99e90798d2
commit 64cc76c970
12 changed files with 1500 additions and 1468 deletions

View File

@@ -1,191 +1,166 @@
<template>
<article class="skill-list panel">
<nav class="status-tabs" aria-label="能力类型">
<button
v-for="tab in tabs"
:key="tab.id"
type="button"
:class="{ active: activeType === tab.id }"
@click="emit('update:activeType', tab.id)"
>
{{ tab.label }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
:placeholder="searchPlaceholder"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
<AuditPickerFilter
id="domain"
title="选择业务域"
close-label="关闭业务域选择"
:active-filter-popover="activeFilterPopover"
:label="selectedDomainLabel"
:options="domainOptions"
:selected-value="selectedDomain"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('domain', $event)"
<EnterpriseListPage
variant="skill-list"
:tabs="shellTabs"
:active-tab="activeType"
tabs-label="能力类型"
:loading="loading"
:error="errorMessage"
:empty="!visibleSkills.length"
:empty-state="auditEmptyState"
:loading-title="`${activeTabLabel}资产同步中`"
:loading-message="`正在加载${activeTabLabel}资产`"
loading-icon="mdi mdi-view-list-outline"
:hint="hintText"
@update:active-tab="emit('update:activeType', $event)"
@empty-action="emit('empty-action')"
>
<template #filters>
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
:placeholder="searchPlaceholder"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
<AuditPickerFilter
v-if="showOwnerFilter"
id="owner"
title="选择负责人"
close-label="关闭负责人选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOwnerLabel"
:options="ownerOptions"
:selected-value="selectedOwner"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('owner', $event)"
/>
<AuditPickerFilter
v-if="showRiskLevelFilter"
id="riskLevel"
title="选择风险等级"
close-label="关闭风险等级选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskLevelLabel"
:options="riskLevelOptions"
:selected-value="selectedRiskLevel"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskLevel', $event)"
/>
<AuditPickerFilter
v-if="showRiskScenarioFilter"
id="riskScenario"
title="选择使用场景"
close-label="关闭使用场景选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskScenarioLabel"
:options="riskScenarioOptions"
:selected-value="selectedRiskScenario"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskScenario', $event)"
/>
<AuditPickerFilter
v-if="showOnlineFilter"
id="online"
title="选择上线状态"
close-label="关闭上线状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOnlineStateLabel"
:options="onlineStateOptions"
:selected-value="selectedOnlineState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('online', $event)"
/>
<AuditPickerFilter
v-if="showEnabledFilter"
id="enabled"
title="选择启用状态"
close-label="关闭启用状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledStateLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
v-if="showStatusFilter"
id="status"
title="选择状态"
close-label="关闭状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('status', $event)"
/>
</div>
<div class="toolbar-actions">
<button
v-if="activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="emit('reset-filters')"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn"
type="button"
:disabled="!canCreateRiskRule"
@click="emit('create-risk-rule')"
>
<i class="mdi mdi-plus"></i>
<span>{{ createButtonLabel }}</span>
</button>
</div>
</div>
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
<div
class="table-wrap"
:class="{ 'is-empty': !loading && !errorMessage && !visibleSkills.length }"
>
<div v-if="loading" class="table-state">
<TableLoadingState
variant="panel"
:title="`${activeTabLabel}资产同步中`"
:message="`正在加载${activeTabLabel}资产`"
icon="mdi mdi-view-list-outline"
/>
</div>
<div v-else-if="errorMessage" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
</div>
<TableEmptyState
v-else-if="!visibleSkills.length"
:eyebrow="auditEmptyState.eyebrow"
:title="auditEmptyState.title"
:description="auditEmptyState.desc"
:icon="auditEmptyState.icon"
:action-label="auditEmptyState.actionLabel"
:action-icon="auditEmptyState.actionIcon"
:tone="auditEmptyState.tone"
:art-label="auditEmptyState.artLabel"
:tips="auditEmptyState.tips"
@action="emit('empty-action')"
<AuditPickerFilter
id="domain"
title="选择业务域"
close-label="关闭业务域选择"
:active-filter-popover="activeFilterPopover"
:label="selectedDomainLabel"
:options="domainOptions"
:selected-value="selectedDomain"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('domain', $event)"
/>
<table v-else>
<AuditPickerFilter
v-if="showOwnerFilter"
id="owner"
title="选择负责人"
close-label="关闭负责人选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOwnerLabel"
:options="ownerOptions"
:selected-value="selectedOwner"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('owner', $event)"
/>
<AuditPickerFilter
v-if="showRiskLevelFilter"
id="riskLevel"
title="选择风险等级"
close-label="关闭风险等级选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskLevelLabel"
:options="riskLevelOptions"
:selected-value="selectedRiskLevel"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskLevel', $event)"
/>
<AuditPickerFilter
v-if="showRiskScenarioFilter"
id="riskScenario"
title="选择使用场景"
close-label="关闭使用场景选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskScenarioLabel"
:options="riskScenarioOptions"
:selected-value="selectedRiskScenario"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskScenario', $event)"
/>
<AuditPickerFilter
v-if="showOnlineFilter"
id="online"
title="选择上线状态"
close-label="关闭上线状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOnlineStateLabel"
:options="onlineStateOptions"
:selected-value="selectedOnlineState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('online', $event)"
/>
<AuditPickerFilter
v-if="showEnabledFilter"
id="enabled"
title="选择启用状态"
close-label="关闭启用状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledStateLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
v-if="showStatusFilter"
id="status"
title="选择状态"
close-label="关闭状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('status', $event)"
/>
</template>
<template #actions>
<button
v-if="activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="emit('reset-filters')"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn"
type="button"
:disabled="!canCreateRiskRule"
@click="emit('create-risk-rule')"
>
<i class="mdi mdi-plus"></i>
<span>{{ createButtonLabel }}</span>
</button>
</template>
<template #meta>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
</template>
<template #error>
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
</template>
<template #table>
<table>
<thead>
<tr>
<th>{{ tableColumns.name }}</th>
@@ -245,24 +220,27 @@
</tr>
</tbody>
</table>
</div>
</template>
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
</footer>
</article>
<template #footer>
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
</footer>
</template>
</EnterpriseListPage>
</template>
<script setup>
import { computed } from 'vue'
import AuditPickerFilter from './AuditPickerFilter.vue'
import TableEmptyState from '../shared/TableEmptyState.vue'
import TableLoadingState from '../shared/TableLoadingState.vue'
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
defineOptions({
name: 'AuditAssetList'
})
defineProps({
const props = defineProps({
tabs: { type: Array, default: () => [] },
activeType: { type: String, default: '' },
activeTabLabel: { type: String, default: '' },
@@ -325,6 +303,13 @@ const emit = defineEmits([
'open-asset-detail'
])
const shellTabs = computed(() =>
props.tabs.map((tab) => ({
value: tab.id,
label: tab.label
}))
)
function selectFilter(type, value) {
emit('select-filter', type, value)
}

View File

@@ -1,120 +1,103 @@
<template>
<section class="digital-employee-list-panel">
<div class="list-toolbar">
<div class="filter-set">
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
placeholder="搜索数字员工技能、编号、执行计划或维护人"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
<AuditPickerFilter
id="status"
title="选择资产状态"
close-label="关闭资产状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('status', $event)"
<EnterpriseListPage
variant="digital-employee-list-panel"
:panel="false"
:loading="loading"
:error="errorMessage"
:empty="!visibleEmployees.length"
:empty-state="emptyState"
loading-title="数字员工资产同步中"
loading-message="正在加载数字员工资产"
loading-icon="mdi mdi-view-list-outline"
hint="归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。"
>
<template #filters>
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
placeholder="搜索数字员工技能、编号、执行计划或维护人"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
<AuditPickerFilter
id="enabled"
title="选择启动状态"
close-label="关闭启动状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
id="executionMode"
title="选择执行方式"
close-label="关闭执行方式选择"
:active-filter-popover="activeFilterPopover"
:label="selectedExecutionModeLabel"
:options="executionModeOptions"
:selected-value="selectedExecutionMode"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('executionMode', $event)"
/>
</div>
<div class="toolbar-actions">
<button
v-if="keyword || activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="emit('reset-filters')"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn digital-refresh-action"
type="button"
:disabled="loading"
@click="emit('load-employees')"
>
<i class="mdi mdi-refresh"></i>
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
</button>
</div>
</div>
<p class="hint">
<i class="mdi mdi-information-outline"></i>
归集后台自动执行的数字员工技能可查看技能内容执行计划启动状态和最近版本
</p>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
<div
class="table-wrap digital-table-wrap"
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
>
<div v-if="loading" class="table-state">
<TableLoadingState
variant="panel"
title="数字员工资产同步中"
message="正在加载数字员工资产"
icon="mdi mdi-view-list-outline"
/>
</div>
<div v-else-if="errorMessage" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
</div>
<TableEmptyState
v-else-if="!visibleEmployees.length"
eyebrow="数字员工"
title="暂无匹配的数字员工"
description="当前没有符合搜索条件的后台执行技能。"
icon="mdi mdi-account-cog-outline"
tone="theme"
art-label="STAFF"
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
<AuditPickerFilter
id="status"
title="选择资产状态"
close-label="关闭资产状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('status', $event)"
/>
<table v-else class="digital-employees-table">
<AuditPickerFilter
id="enabled"
title="选择启动状态"
close-label="关闭启动状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
id="executionMode"
title="选择执行方式"
close-label="关闭执行方式选择"
:active-filter-popover="activeFilterPopover"
:label="selectedExecutionModeLabel"
:options="executionModeOptions"
:selected-value="selectedExecutionMode"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('executionMode', $event)"
/>
</template>
<template #actions>
<button
v-if="keyword || activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="emit('reset-filters')"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn digital-refresh-action"
type="button"
:disabled="loading"
@click="emit('load-employees')"
>
<i class="mdi mdi-refresh"></i>
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
</button>
</template>
<template #meta>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
</template>
<template #error>
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
</template>
<template #table>
<table class="digital-employees-table">
<colgroup>
<col class="col-skill">
<col class="col-skill-type">
@@ -164,38 +147,39 @@
</tr>
</tbody>
</table>
</div>
</template>
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
<span class="page-summary"> {{ visibleEmployees.length }} 目前第 {{ currentPage }} / {{ totalPages }} </span>
<div class="pager" aria-label="员工技能分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in pageNumbers"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</footer>
</section>
<template #footer>
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
<span class="page-summary"> {{ visibleEmployees.length }} 目前第 {{ currentPage }} / {{ totalPages }} </span>
<div class="pager" aria-label="员工技能分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in pageNumbers"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</footer>
</template>
</EnterpriseListPage>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import AuditPickerFilter from './AuditPickerFilter.vue'
import TableEmptyState from '../shared/TableEmptyState.vue'
import TableLoadingState from '../shared/TableLoadingState.vue'
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
defineOptions({
name: 'DigitalEmployeeListPanel'
@@ -244,6 +228,15 @@ const pageNumbers = computed(() => {
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
return Array.from({ length: 7 }, (_, index) => start + index)
})
const emptyState = {
eyebrow: '数字员工',
title: '暂无匹配的数字员工',
description: '当前没有符合搜索条件的后台执行技能。',
icon: 'mdi mdi-account-cog-outline',
tone: 'theme',
artLabel: 'STAFF',
tips: ['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']
}
watch(
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
@@ -279,7 +272,7 @@ function selectFilter(type, value) {
overflow: hidden;
}
.digital-employee-list-panel .digital-table-wrap {
.digital-employee-list-panel :deep(.table-wrap) {
flex: 1 1 0;
min-height: 0;
}
@@ -302,7 +295,7 @@ function selectFilter(type, value) {
gap: 6px;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 12px;
border-radius: 4px;
background: #f8fafc;
}
@@ -310,7 +303,7 @@ function selectFilter(type, value) {
width: 32px;
height: 32px;
border: 0;
border-radius: 9px;
border-radius: 4px;
background: transparent;
color: #334155;
font-size: 14px;
@@ -321,7 +314,7 @@ function selectFilter(type, value) {
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
background: #fff;
color: var(--theme-primary-active);
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
}
.digital-employee-list-panel .pager button.active {

View File

@@ -1,17 +1,7 @@
<template>
<article class="enterprise-detail-page" :class="variant">
<div class="detail-scroll">
<TableLoadingState
v-if="loading"
class="detail-loading-state panel"
variant="panel"
:title="loadingTitle"
:message="loadingMessage"
:icon="loadingIcon"
:show-skeleton="false"
/>
<section v-else-if="error" class="detail-inline-state panel error">
<section v-if="error" class="detail-inline-state panel error">
<slot name="error" :error="error">
<i class="mdi mdi-alert-circle-outline"></i>
<div>
@@ -21,6 +11,16 @@
</slot>
</section>
<TableLoadingState
v-else-if="loading"
class="detail-loading-state panel"
variant="panel"
:title="loadingTitle"
:message="loadingMessage"
:icon="loadingIcon"
:show-skeleton="false"
/>
<template v-else>
<section v-if="$slots.hero" class="detail-hero panel">
<slot name="hero"></slot>
@@ -40,7 +40,7 @@
</template>
</div>
<footer v-if="backLabel || $slots.actions" class="detail-actions">
<footer v-if="backLabel || $slots.actions" class="detail-actions" :class="actionsClass">
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
<i class="mdi mdi-arrow-left"></i>
<span>{{ backLabel }}</span>
@@ -57,6 +57,7 @@
import TableLoadingState from './TableLoadingState.vue'
defineProps({
actionsClass: { type: [String, Array, Object], default: '' },
backLabel: { type: String, default: '' },
error: { type: String, default: '' },
errorTitle: { type: String, default: '详情加载失败' },
@@ -64,7 +65,7 @@ defineProps({
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
loadingMessage: { type: String, default: '' },
loadingTitle: { type: String, default: '正在加载详情' },
variant: { type: String, default: '' }
variant: { type: [String, Array, Object], default: '' }
})
const emit = defineEmits(['back'])

View File

@@ -1,5 +1,5 @@
<template>
<article class="enterprise-list-page panel" :class="variant">
<article class="enterprise-list-page" :class="[variant, { panel }]">
<slot name="before"></slot>
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
@@ -51,12 +51,15 @@
</slot>
</p>
<slot name="meta"></slot>
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
<div v-if="loading" class="table-state">
<TableLoadingState
:title="loadingTitle"
:message="loadingMessage"
:icon="loadingIcon"
floating
/>
</div>
@@ -135,6 +138,7 @@ const props = defineProps({
type: Array,
default: () => []
},
panel: { type: Boolean, default: true },
pages: {
type: Array,
default: () => []