feat: add interactive component examples

This commit is contained in:
QiuSW
2026-07-09 01:16:47 +08:00
parent 357747dd17
commit 76cda6b49c
8 changed files with 337 additions and 64 deletions
@@ -1,21 +1,141 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import {
LeftOutlined,
ReloadOutlined,
RightOutlined,
SearchOutlined,
} from '@ant-design/icons-vue'
import ModulePageShell from '../../components/template/ModulePageShell.vue'
const checked = ref(true)
type ComponentStatus = 'Ready' | 'Draft' | 'Blocked'
type ComponentRow = {
id: number
name: string
category: string
owner: string
status: ComponentStatus
updatedAt: string
}
const productName = ref('商品主图优化')
const selectedChannels = ref(['desktop', 'audit'])
const category = ref('image')
const rows = [
{ name: '输入框', category: 'Form', status: 'Ready' },
{ name: '多选框', category: 'Form', status: 'Ready' },
{ name: '表格', category: 'Data', status: 'Ready' },
const note = ref('用于记录任务备注、失败原因或人工审核意见。')
const keyword = ref('')
const categoryFilter = ref('')
const statusFilter = ref('')
const currentPage = ref(1)
const pageSize = 5
const channelOptions = [
{ value: 'desktop', label: '桌面端' },
{ value: 'audit', label: '审计日志' },
{ value: 'statistics', label: '数据统计' },
]
const summary = [
{ label: '表单控件', value: '4', tone: 'blue' as const },
{ label: '表格', value: '1', tone: 'green' as const },
{ label: '筛选', value: '1', tone: 'amber' as const },
{ label: '状态标签', value: '3', tone: 'violet' as const },
const categoryOptions = [
{ value: 'image', label: '图片任务' },
{ value: 'logs', label: '日志查询' },
{ value: 'settings', label: '设置项' },
]
const rows: ComponentRow[] = [
{ id: 1, name: '文本输入', category: 'Form', owner: 'Settings', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 2, name: '多选框组', category: 'Form', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 3, name: '下拉选择', category: 'Form', owner: 'Settings', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 4, name: '多行文本', category: 'Form', owner: 'Logs', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 5, name: '任务表格', category: 'Data', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 6, name: '筛选栏', category: 'Data', owner: 'Logs', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 7, name: '分页控件', category: 'Data', owner: 'Dashboard', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 8, name: '成功状态标签', category: 'Status', owner: 'Business', status: 'Ready', updatedAt: '2026-07-09' },
{ id: 9, name: '草稿状态标签', category: 'Status', owner: 'Settings', status: 'Draft', updatedAt: '2026-07-09' },
{ id: 10, name: '阻塞状态标签', category: 'Status', owner: 'Logs', status: 'Blocked', updatedAt: '2026-07-09' },
{ id: 11, name: '只读字段', category: 'Form', owner: 'Settings', status: 'Draft', updatedAt: '2026-07-09' },
{ id: 12, name: '表格空状态', category: 'Data', owner: 'Dashboard', status: 'Blocked', updatedAt: '2026-07-09' },
]
const summary = computed(() => [
{ label: '表单控件', value: String(rows.filter((row) => row.category === 'Form').length), tone: 'blue' as const },
{ label: '表格行数', value: String(filteredRows.value.length), tone: 'green' as const },
{ label: '当前页', value: `${visiblePage.value}/${totalPages.value}`, tone: 'amber' as const },
{ label: '状态标签', value: String(rows.filter((row) => row.category === 'Status').length), tone: 'violet' as const },
])
const filteredRows = computed(() => {
const term = keyword.value.trim().toLowerCase()
return rows.filter((row) => {
const matchesKeyword = !term
|| row.name.toLowerCase().includes(term)
|| row.owner.toLowerCase().includes(term)
|| row.category.toLowerCase().includes(term)
const matchesCategory = !categoryFilter.value || row.category === categoryFilter.value
const matchesStatus = !statusFilter.value || row.status === statusFilter.value
return matchesKeyword && matchesCategory && matchesStatus
})
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize)))
const visiblePage = computed(() => Math.min(currentPage.value, totalPages.value))
const pagedRows = computed(() => {
const start = (visiblePage.value - 1) * pageSize
return filteredRows.value.slice(start, start + pageSize)
})
const formPreview = computed(() => {
const channelLabels = channelOptions
.filter((option) => selectedChannels.value.includes(option.value))
.map((option) => option.label)
.join('、')
const selectedCategory = categoryOptions.find((option) => option.value === category.value)?.label || '-'
return [
{ label: '名称', value: productName.value || '-' },
{ label: '模块', value: selectedCategory },
{ label: '场景', value: channelLabels || '-' },
{ label: '备注', value: note.value || '-' },
]
})
function applyFilters() {
currentPage.value = 1
}
function resetFilters() {
keyword.value = ''
categoryFilter.value = ''
statusFilter.value = ''
currentPage.value = 1
}
function previousPage() {
currentPage.value = Math.max(1, currentPage.value - 1)
}
function nextPage() {
currentPage.value = Math.min(totalPages.value, currentPage.value + 1)
}
function statusClass(status: ComponentStatus) {
if (status === 'Ready') {
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200'
}
if (status === 'Blocked') {
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-200'
}
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-200'
}
function statusLabel(status: ComponentStatus) {
if (status === 'Ready') {
return '可用'
}
if (status === 'Blocked') {
return '阻塞'
}
return '草稿'
}
</script>
<template>
@@ -23,72 +143,200 @@ const summary = [
eyebrow="Components"
title="组件示例"
description="常用业务控件的运行示例,后续按真实模块沉淀组合组件。"
status="路由可访问"
status="示例可运行"
:items="summary"
>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_1fr]">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold">
表单
</h2>
<div class="space-y-4">
<label class="block">
<span class="mb-1 block text-sm font-medium">输入框</span>
<input class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" value="商品主图" />
</label>
<label class="flex items-center gap-2 text-sm">
<input v-model="checked" class="h-4 w-4 rounded border-slate-300" type="checkbox" />
<span>多选框</span>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">下拉框</span>
<select v-model="category" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option value="image">图片任务</option>
<option value="logs">日志查询</option>
<option value="settings">设置项</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium">多行文本</span>
<textarea class="min-h-[96px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-950">用于记录任务备注。</textarea>
</label>
</div>
</section>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_minmax(0,1fr)]">
<aside class="space-y-4">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
表单控件
</h2>
<div class="space-y-4">
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">输入框</span>
<input
v-model="productName"
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
>
</label>
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="grid grid-cols-1 gap-3 border-b border-slate-200 p-4 dark:border-slate-800 md:grid-cols-[1fr_180px_auto]">
<input class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" placeholder="筛选关键字" />
<select class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option>全部分类</option>
<option>Form</option>
<option>Data</option>
<fieldset class="space-y-2">
<legend class="text-sm font-medium text-slate-700 dark:text-slate-200">
多选框
</legend>
<label
v-for="option in channelOptions"
:key="option.value"
class="flex min-h-[28px] items-center gap-2 text-sm text-slate-700 dark:text-slate-200"
>
<input
v-model="selectedChannels"
class="h-4 w-4 rounded border-slate-300"
type="checkbox"
:value="option.value"
>
<span>{{ option.label }}</span>
</label>
</fieldset>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">下拉框</span>
<select
v-model="category"
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
>
<option v-for="option in categoryOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">多行文本</span>
<textarea
v-model="note"
class="min-h-[108px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-950"
/>
</label>
</div>
</section>
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
当前值
</h2>
<dl class="space-y-3 text-sm">
<div v-for="item in formPreview" :key="item.label">
<dt class="text-xs font-medium text-slate-500 dark:text-slate-400">
{{ item.label }}
</dt>
<dd class="mt-1 break-all text-slate-900 dark:text-white">
{{ item.value }}
</dd>
</div>
</dl>
</section>
</aside>
<section class="min-w-0 rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="grid grid-cols-1 gap-3 border-b border-slate-200 p-4 dark:border-slate-800 lg:grid-cols-[minmax(180px,1fr)_160px_160px_auto_auto]">
<input
v-model="keyword"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
placeholder="筛选关键字"
@keyup.enter="applyFilters"
>
<select
v-model="categoryFilter"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
@change="applyFilters"
>
<option value="">全部分类</option>
<option value="Form">Form</option>
<option value="Data">Data</option>
<option value="Status">Status</option>
</select>
<button class="h-10 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950" type="button">
筛选
<select
v-model="statusFilter"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
@change="applyFilters"
>
<option value="">全部状态</option>
<option value="Ready">可用</option>
<option value="Draft">草稿</option>
<option value="Blocked">阻塞</option>
</select>
<button
class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950"
type="button"
@click="applyFilters"
>
<SearchOutlined />
<span>筛选</span>
</button>
<button
class="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:bg-slate-900"
type="button"
@click="resetFilters"
>
<ReloadOutlined />
<span>重置</span>
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-950 dark:text-slate-400">
<tr>
<th class="px-4 py-3 font-medium">名称</th>
<th class="px-4 py-3 font-medium">分类</th>
<th class="px-4 py-3 font-medium">状态</th>
<th class="w-[90px] px-4 py-3 font-medium">编号</th>
<th class="min-w-[180px] px-4 py-3 font-medium">名称</th>
<th class="w-[130px] px-4 py-3 font-medium">分类</th>
<th class="w-[140px] px-4 py-3 font-medium">归属</th>
<th class="w-[120px] px-4 py-3 font-medium">状态</th>
<th class="w-[140px] px-4 py-3 font-medium">更新时间</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
<tr v-for="row in rows" :key="row.name">
<td class="px-4 py-3">{{ row.name }}</td>
<td class="px-4 py-3">{{ row.category }}</td>
<tr v-if="pagedRows.length === 0">
<td class="px-4 py-8 text-center text-slate-500 dark:text-slate-400" colspan="6">
暂无数据
</td>
</tr>
<tr v-for="row in pagedRows" :key="row.id" class="hover:bg-slate-50 dark:hover:bg-slate-950">
<td class="px-4 py-3 font-medium text-slate-900 dark:text-white">
#{{ row.id }}
</td>
<td class="px-4 py-3 text-slate-900 dark:text-white">
{{ row.name }}
</td>
<td class="px-4 py-3 text-slate-600 dark:text-slate-300">
{{ row.category }}
</td>
<td class="px-4 py-3 text-slate-600 dark:text-slate-300">
{{ row.owner }}
</td>
<td class="px-4 py-3">
<span class="rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200">
{{ row.status }}
<span class="inline-flex rounded-md border px-2 py-1 text-xs font-medium" :class="statusClass(row.status)">
{{ statusLabel(row.status) }}
</span>
</td>
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">
{{ row.updatedAt }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex flex-col gap-3 border-t border-slate-200 p-4 dark:border-slate-800 md:flex-row md:items-center md:justify-between">
<div class="text-sm text-slate-500 dark:text-slate-400">
{{ filteredRows.length }} 条结果
</div>
<div class="flex items-center gap-2">
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 text-slate-700 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
type="button"
:disabled="currentPage === 1"
title="上一页"
@click="previousPage"
>
<LeftOutlined />
</button>
<span class="min-w-[72px] text-center text-sm text-slate-700 dark:text-slate-200">
{{ visiblePage }} / {{ totalPages }}
</span>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 text-slate-700 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
type="button"
:disabled="currentPage === totalPages"
title="下一页"
@click="nextPage"
>
<RightOutlined />
</button>
</div>
</div>
</section>
</div>
</ModulePageShell>