677 lines
22 KiB
Vue
677 lines
22 KiB
Vue
<template>
|
|
<div class="form-center-page">
|
|
<h1 class="page-title-sr-only">表单中心</h1>
|
|
<header class="page-header page-header--actions-only" aria-label="页面操作">
|
|
<t-button variant="outline" :loading="loading" @click="reloadAll">
|
|
<template #icon><refresh-icon /></template>
|
|
刷新
|
|
</t-button>
|
|
</header>
|
|
|
|
<t-alert v-if="errorMessage" theme="error" :close-btn="false">
|
|
<div class="alert-content">
|
|
<span>{{ errorMessage }}</span>
|
|
<t-link theme="primary" @click="reloadAll">重新加载</t-link>
|
|
</div>
|
|
</t-alert>
|
|
|
|
<section class="template-section" aria-label="来源表单模板">
|
|
<div class="section-heading">
|
|
<div>
|
|
<h2>业务模板</h2>
|
|
<span>当前启用 {{ templates.length }} 类表单</span>
|
|
</div>
|
|
</div>
|
|
<t-tabs v-model="activeGroup" theme="card" @change="groupChanged">
|
|
<t-tab-panel v-for="group in groups" :key="group.code" :value="group.code" :label="group.name">
|
|
<div class="template-grid">
|
|
<t-card v-for="template in groupedTemplates[group.code] || []" :key="template.publicId" :bordered="true">
|
|
<template #title>
|
|
<div class="template-title">
|
|
<span>{{ template.formType }}</span>
|
|
<t-tag theme="primary" variant="light">V{{ template.templateVersion }}</t-tag>
|
|
</div>
|
|
</template>
|
|
<div class="template-body">
|
|
<h3>{{ template.name }}</h3>
|
|
<p>{{ template.purpose }}</p>
|
|
<div class="template-roles" aria-label="适用角色">
|
|
<span>适用角色</span>
|
|
<t-tag v-for="role in template.initiatorRoles" :key="role" variant="light">
|
|
{{ roleLabel(role) }}
|
|
</t-tag>
|
|
</div>
|
|
<div class="template-metrics">
|
|
<span
|
|
>我的草稿 <strong>{{ template.myDraftCount }}</strong></span
|
|
>
|
|
<span
|
|
>待我处理 <strong>{{ template.myPendingCount }}</strong></span
|
|
>
|
|
</div>
|
|
<div class="template-actions">
|
|
<t-space size="small" break-line>
|
|
<t-button v-if="template.canCreate" size="small" theme="primary" @click="openCreate(template)">
|
|
<template #icon><add-icon /></template>
|
|
在线填写
|
|
</t-button>
|
|
<t-button v-if="template.canCreate" size="small" variant="outline" @click="openImport(template)">
|
|
<template #icon><upload-icon /></template>
|
|
导入
|
|
</t-button>
|
|
<t-button size="small" variant="text" @click="downloadTemplate(template)">
|
|
<template #icon><download-icon /></template>
|
|
模板
|
|
</t-button>
|
|
<t-button size="small" variant="text" @click="showTemplateHistory(template)">历史</t-button>
|
|
</t-space>
|
|
</div>
|
|
</div>
|
|
</t-card>
|
|
</div>
|
|
</t-tab-panel>
|
|
</t-tabs>
|
|
</section>
|
|
|
|
<section class="history-section" aria-label="来源表单记录">
|
|
<div class="section-heading">
|
|
<div>
|
|
<h2>表单记录</h2>
|
|
<span>共 {{ pagination.total }} 条</span>
|
|
</div>
|
|
</div>
|
|
<t-form :data="filters" label-align="top" @submit="search">
|
|
<div class="filter-grid">
|
|
<t-form-item label="关键词" name="keyword">
|
|
<t-input v-model="filters.keyword" clearable placeholder="业务编号、来源单号或项目" @enter="search" />
|
|
</t-form-item>
|
|
<t-form-item label="表单类型" name="formType">
|
|
<t-select v-model="filters.formType" clearable :options="templateOptions" placeholder="全部类型" />
|
|
</t-form-item>
|
|
<t-form-item label="模板版本" name="templateVersion">
|
|
<t-input-number
|
|
v-model="filters.templateVersion"
|
|
clearable
|
|
:min="1"
|
|
:decimal-places="0"
|
|
theme="normal"
|
|
placeholder="全部版本"
|
|
/>
|
|
</t-form-item>
|
|
<t-form-item label="来源方式" name="sourceSystem">
|
|
<t-select v-model="filters.sourceSystem" clearable :options="sourceOptions" placeholder="全部来源" />
|
|
</t-form-item>
|
|
<t-form-item label="状态" name="status">
|
|
<t-select v-model="filters.status" clearable :options="statusOptions" placeholder="全部状态" />
|
|
</t-form-item>
|
|
<t-form-item label="创建日期" name="createdDateRange">
|
|
<t-date-range-picker v-model="createdDateRange" clearable value-type="YYYY-MM-DD" />
|
|
</t-form-item>
|
|
<t-form-item label="排序" name="sort">
|
|
<t-select v-model="filters.sort" :options="sortOptions" placeholder="选择排序方式" />
|
|
</t-form-item>
|
|
<div class="filter-actions">
|
|
<t-checkbox v-model="filters.createdByMe">只看我发起</t-checkbox>
|
|
<t-button theme="primary" type="submit">
|
|
<template #icon><search-icon /></template>
|
|
查询
|
|
</t-button>
|
|
<t-button variant="outline" type="button" @click="resetFilters">重置</t-button>
|
|
</div>
|
|
</div>
|
|
</t-form>
|
|
|
|
<t-table
|
|
v-if="loading || rows.length"
|
|
:data="rows"
|
|
:columns="columns"
|
|
row-key="publicId"
|
|
:loading="loading"
|
|
:hover="true"
|
|
table-layout="fixed"
|
|
:horizontal-scroll-affixed-bottom="true"
|
|
>
|
|
<template #businessNo="{ row }">
|
|
<t-link theme="primary" @click="openDocument(row)">{{ row.businessNo }}</t-link>
|
|
</template>
|
|
<template #form="{ row }">
|
|
<div class="form-name-cell">
|
|
<strong>{{ row.formName }}</strong
|
|
><span>{{ row.formType }} · V{{ row.templateVersion }}</span>
|
|
</div>
|
|
</template>
|
|
<template #project="{ row }">
|
|
<t-link
|
|
v-if="row.project"
|
|
theme="primary"
|
|
role="link"
|
|
tabindex="0"
|
|
@click="openProject(row.project.publicId)"
|
|
@keydown.enter="openProject(row.project.publicId)"
|
|
>
|
|
{{ row.project.name }}
|
|
</t-link>
|
|
<span v-else>不涉及项目</span>
|
|
</template>
|
|
<template #source="{ row }">{{ sourceLabel(row.sourceSystem) }} · {{ row.sourceNo }}</template>
|
|
<template #status="{ row }">
|
|
<t-tag :theme="statusTheme(row.status)" variant="light">{{ statusLabel(row.status) }}</t-tag>
|
|
</template>
|
|
<template #updatedAt="{ row }">{{ formatTime(row.updatedAt) }}</template>
|
|
<template #createdAt="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
<template #op="{ row }">
|
|
<t-space size="small">
|
|
<t-link theme="primary" @click="openDocument(row)">{{
|
|
row.allowedActions.includes('EDIT') ? '继续填写' : '查看'
|
|
}}</t-link>
|
|
<t-link v-if="row.allowedActions.includes('VALIDATE')" theme="primary" @click="validateDocument(row)"
|
|
>校验</t-link
|
|
>
|
|
</t-space>
|
|
</template>
|
|
</t-table>
|
|
<t-empty v-else description="当前条件下暂无表单记录" />
|
|
<div v-if="pagination.total > 0" class="pagination-row">
|
|
<t-pagination
|
|
v-model="pagination.current"
|
|
v-model:page-size="pagination.pageSize"
|
|
:total="pagination.total"
|
|
:page-size-options="[20, 50, 100]"
|
|
show-jumper
|
|
@current-change="loadForms"
|
|
@page-size-change="pageSizeChanged"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { AddIcon, DownloadIcon, RefreshIcon, SearchIcon, UploadIcon } from 'tdesign-icons-vue-next';
|
|
import type { PrimaryTableCol } from 'tdesign-vue-next';
|
|
import { MessagePlugin } from 'tdesign-vue-next';
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
import type { SourceFormQuery, SourceFormSummary, SourceTemplate } from '@/api/source';
|
|
import { getSourceForms, getSourceTemplate, getSourceTemplates, validateSourceForm } from '@/api/source';
|
|
|
|
defineOptions({ name: 'FormCenterPage' });
|
|
|
|
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const loading = ref(false);
|
|
const validatingId = ref('');
|
|
const errorMessage = ref('');
|
|
const templates = ref<SourceTemplate[]>([]);
|
|
const rows = ref<SourceFormSummary[]>([]);
|
|
const groups = [
|
|
{ code: 'PROJECT_PREPARATION', name: '项目准备' },
|
|
{ code: 'INCOME_EXPENSE', name: '收支执行' },
|
|
{ code: 'PROJECT_PROCESS', name: '项目过程' },
|
|
{ code: 'CLOSING_ARCHIVE', name: '结束归档' },
|
|
{ code: 'BANK_INFORMATION', name: '银行资料' },
|
|
];
|
|
const requestedGroup = typeof route.query.groupCode === 'string' ? route.query.groupCode : route.query.group;
|
|
const activeGroup = ref(
|
|
groups.some((group) => group.code === requestedGroup) ? String(requestedGroup) : groups[0].code,
|
|
);
|
|
const parsedTemplateVersion = Number(
|
|
typeof route.query.templateVersion === 'string' ? route.query.templateVersion : '',
|
|
);
|
|
const sortValues = new Set<NonNullable<SourceFormQuery['sort']>>([
|
|
'updatedAt,asc',
|
|
'updatedAt,desc',
|
|
'createdAt,asc',
|
|
'createdAt,desc',
|
|
'businessNo,asc',
|
|
'businessNo,desc',
|
|
'status,asc',
|
|
'status,desc',
|
|
]);
|
|
const parseSort = (value: unknown): NonNullable<SourceFormQuery['sort']> =>
|
|
typeof value === 'string' && sortValues.has(value as NonNullable<SourceFormQuery['sort']>)
|
|
? (value as NonNullable<SourceFormQuery['sort']>)
|
|
: 'updatedAt,desc';
|
|
const filters = reactive<{
|
|
keyword: string;
|
|
formType: string;
|
|
templateVersion?: number;
|
|
projectId: string;
|
|
sourceSystem: string;
|
|
status: string;
|
|
createdByMe: boolean;
|
|
sort: NonNullable<SourceFormQuery['sort']>;
|
|
}>({
|
|
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
|
formType: typeof route.query.formType === 'string' ? route.query.formType : '',
|
|
templateVersion:
|
|
Number.isInteger(parsedTemplateVersion) && parsedTemplateVersion > 0 ? parsedTemplateVersion : undefined,
|
|
projectId: typeof route.query.projectId === 'string' ? route.query.projectId : '',
|
|
sourceSystem: typeof route.query.sourceSystem === 'string' ? route.query.sourceSystem : '',
|
|
status: typeof route.query.status === 'string' ? route.query.status : '',
|
|
createdByMe: route.query.createdByMe === 'true',
|
|
sort: parseSort(route.query.sort),
|
|
});
|
|
const createdDateRange = ref<string[]>(
|
|
typeof route.query.createdDateFrom === 'string' && typeof route.query.createdDateTo === 'string'
|
|
? [route.query.createdDateFrom, route.query.createdDateTo]
|
|
: [],
|
|
);
|
|
const pagination = reactive({
|
|
current: Number(route.query.page) || 1,
|
|
pageSize: Number(route.query.size) || 20,
|
|
total: 0,
|
|
});
|
|
|
|
const groupedTemplates = computed(() =>
|
|
templates.value.reduce<Record<string, SourceTemplate[]>>((result, item) => {
|
|
(result[item.groupCode] ||= []).push(item);
|
|
return result;
|
|
}, {}),
|
|
);
|
|
const templateOptions = computed(() =>
|
|
templates.value.map((item) => ({ label: `${item.formType} ${item.name}`, value: item.formType })),
|
|
);
|
|
const sourceOptions = [
|
|
{ label: '在线录入', value: 'MANUAL' },
|
|
{ label: 'OA 导出', value: 'OA_EXPORT' },
|
|
{ label: 'Excel/CSV 导入', value: 'EXCEL_IMPORT' },
|
|
];
|
|
const statusOptions = [
|
|
{ label: '草稿', value: 'DRAFT' },
|
|
{ label: '项目复核中', value: 'PROJECT_REVIEW' },
|
|
{ label: '财务复核中', value: 'FINANCE_REVIEW' },
|
|
{ label: '已退回', value: 'RETURNED' },
|
|
{ label: '已驳回', value: 'REJECTED' },
|
|
{ label: '已通过', value: 'APPROVED' },
|
|
{ label: '待归档', value: 'ARCHIVE_PENDING' },
|
|
{ label: '已归档', value: 'ARCHIVED' },
|
|
{ label: '已作废', value: 'VOID' },
|
|
];
|
|
const sortOptions: Array<{ label: string; value: NonNullable<SourceFormQuery['sort']> }> = [
|
|
{ label: '更新时间从新到旧', value: 'updatedAt,desc' },
|
|
{ label: '更新时间从旧到新', value: 'updatedAt,asc' },
|
|
{ label: '创建时间从新到旧', value: 'createdAt,desc' },
|
|
{ label: '创建时间从旧到新', value: 'createdAt,asc' },
|
|
{ label: '业务编号升序', value: 'businessNo,asc' },
|
|
{ label: '业务编号降序', value: 'businessNo,desc' },
|
|
{ label: '状态升序', value: 'status,asc' },
|
|
{ label: '状态降序', value: 'status,desc' },
|
|
];
|
|
|
|
const columns: PrimaryTableCol[] = [
|
|
{ title: '业务编号', colKey: 'businessNo', width: 175, fixed: 'left', ellipsis: true },
|
|
{ title: '表单', colKey: 'form', width: 230 },
|
|
{ title: '项目', colKey: 'project', width: 180, ellipsis: true },
|
|
{ title: '来源', colKey: 'source', width: 210, ellipsis: true },
|
|
{ title: '申请人', colKey: 'applicantName', width: 110, ellipsis: true },
|
|
{ title: '状态', colKey: 'status', width: 115 },
|
|
{ title: '当前节点', colKey: 'currentNodeName', width: 120 },
|
|
{ title: '创建时间', colKey: 'createdAt', width: 165 },
|
|
{ title: '更新时间', colKey: 'updatedAt', width: 165 },
|
|
{ title: '操作', colKey: 'op', width: 145, fixed: 'right' },
|
|
];
|
|
|
|
function openProject(projectId: string) {
|
|
void router.push({
|
|
name: 'ProjectDetail',
|
|
params: { projectId },
|
|
query: { tab: 'forms', from: route.fullPath },
|
|
});
|
|
}
|
|
|
|
function query(): SourceFormQuery {
|
|
return {
|
|
groupCode: activeGroup.value,
|
|
keyword: filters.keyword.trim() || undefined,
|
|
formType: filters.formType || undefined,
|
|
templateVersion: filters.templateVersion,
|
|
projectId: filters.projectId || undefined,
|
|
sourceSystem: filters.sourceSystem || undefined,
|
|
status: filters.status || undefined,
|
|
createdByMe: filters.createdByMe || undefined,
|
|
createdDateFrom: createdDateRange.value[0] || undefined,
|
|
createdDateTo: createdDateRange.value[1] || undefined,
|
|
sort: filters.sort,
|
|
page: pagination.current,
|
|
size: pagination.pageSize,
|
|
};
|
|
}
|
|
|
|
async function syncUrl(params: SourceFormQuery) {
|
|
const next = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined && value !== ''));
|
|
await router.replace({ name: 'FormCenter', query: next as Record<string, string> });
|
|
}
|
|
|
|
async function loadTemplates() {
|
|
templates.value = await getSourceTemplates();
|
|
}
|
|
|
|
async function loadForms() {
|
|
loading.value = true;
|
|
errorMessage.value = '';
|
|
try {
|
|
const params = query();
|
|
await syncUrl(params);
|
|
const result = await getSourceForms(params);
|
|
rows.value = result.items;
|
|
pagination.total = result.meta.totalElements;
|
|
} catch (error) {
|
|
errorMessage.value = errorText(error, '表单记录加载失败');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function reloadAll() {
|
|
loading.value = true;
|
|
errorMessage.value = '';
|
|
try {
|
|
await Promise.all([loadTemplates(), loadForms()]);
|
|
} catch (error) {
|
|
errorMessage.value = errorText(error, '表单中心加载失败');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function search() {
|
|
pagination.current = 1;
|
|
loadForms();
|
|
}
|
|
|
|
function resetFilters() {
|
|
Object.assign(filters, {
|
|
keyword: '',
|
|
formType: '',
|
|
templateVersion: undefined,
|
|
projectId: '',
|
|
sourceSystem: '',
|
|
status: '',
|
|
createdByMe: false,
|
|
sort: 'updatedAt,desc',
|
|
});
|
|
createdDateRange.value = [];
|
|
pagination.current = 1;
|
|
loadForms();
|
|
}
|
|
|
|
function groupChanged() {
|
|
pagination.current = 1;
|
|
loadForms();
|
|
}
|
|
|
|
function pageSizeChanged() {
|
|
pagination.current = 1;
|
|
loadForms();
|
|
}
|
|
|
|
function openCreate(template: SourceTemplate) {
|
|
router.push({
|
|
name: 'FormEditor',
|
|
params: { formType: template.formType },
|
|
query: { mode: 'online', from: route.fullPath },
|
|
});
|
|
}
|
|
|
|
function openImport(template: SourceTemplate) {
|
|
router.push({
|
|
name: 'FormEditor',
|
|
params: { formType: template.formType },
|
|
query: { mode: 'import', from: route.fullPath },
|
|
});
|
|
}
|
|
|
|
function openDocument(row: SourceFormSummary) {
|
|
router.push({
|
|
name: 'FormEditor',
|
|
params: { formType: row.formType, documentId: row.publicId },
|
|
query: { from: route.fullPath },
|
|
});
|
|
}
|
|
|
|
function showTemplateHistory(template: SourceTemplate) {
|
|
filters.formType = template.formType;
|
|
activeGroup.value = template.groupCode;
|
|
pagination.current = 1;
|
|
loadForms();
|
|
document.querySelector('.history-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
async function validateDocument(row: SourceFormSummary) {
|
|
if (validatingId.value || !row.allowedActions.includes('VALIDATE')) return;
|
|
validatingId.value = row.publicId;
|
|
try {
|
|
const result = await validateSourceForm(row.publicId);
|
|
result.valid ? MessagePlugin.success('表单校验通过') : MessagePlugin.warning(`发现 ${result.errors.length} 项错误`);
|
|
await loadForms();
|
|
} catch (error) {
|
|
MessagePlugin.error(errorText(error, '表单校验失败'));
|
|
} finally {
|
|
validatingId.value = '';
|
|
}
|
|
}
|
|
|
|
async function downloadTemplate(template: SourceTemplate) {
|
|
try {
|
|
const detail = await getSourceTemplate(template.formType, template.templateVersion);
|
|
const headers = [
|
|
'来源单号',
|
|
'来源版本',
|
|
'公司编号',
|
|
'项目编号',
|
|
'申请人',
|
|
'业务日期',
|
|
...detail.fields.map((field) => field.label),
|
|
];
|
|
const csv = `\uFEFF${headers.map(csvCell).join(',')}\n`;
|
|
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `${template.formType}-${template.name}-V${template.templateVersion}.csv`;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
MessagePlugin.error(errorText(error, '模板下载失败'));
|
|
}
|
|
}
|
|
|
|
function csvCell(value: string) {
|
|
return `"${value.replaceAll('"', '""')}"`;
|
|
}
|
|
|
|
function sourceLabel(value: string) {
|
|
return sourceOptions.find((item) => item.value === value)?.label || value;
|
|
}
|
|
|
|
function statusLabel(value: string) {
|
|
return statusOptions.find((item) => item.value === value)?.label || value;
|
|
}
|
|
|
|
function roleLabel(value: string) {
|
|
return (
|
|
{
|
|
PROJECT_MANAGER: '项目管理人员',
|
|
FINANCE_MANAGER: '财务管理人员',
|
|
ARCHIVE_MANAGER: '资料管理人员',
|
|
}[value] || value
|
|
);
|
|
}
|
|
|
|
function statusTheme(value: string): TagTheme {
|
|
if (value === 'APPROVED' || value === 'ARCHIVED') return 'success';
|
|
if (value === 'RETURNED' || value === 'PROJECT_REVIEW' || value === 'FINANCE_REVIEW') return 'warning';
|
|
if (value === 'REJECTED' || value === 'VOID') return 'danger';
|
|
return value === 'DRAFT' ? 'default' : 'primary';
|
|
}
|
|
|
|
function formatTime(value: string) {
|
|
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value));
|
|
}
|
|
|
|
function errorText(error: unknown, fallback: string) {
|
|
const responseDetail = (error as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
|
|
return responseDetail || (error as Error)?.message || fallback;
|
|
}
|
|
|
|
onMounted(reloadAll);
|
|
</script>
|
|
<style scoped>
|
|
.form-center-page {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.page-header,
|
|
.section-heading {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
}
|
|
.page-header h1 {
|
|
margin: 0;
|
|
font-size: 24px;
|
|
line-height: 32px;
|
|
font-weight: 600;
|
|
}
|
|
.page-header p {
|
|
margin: 6px 0 0;
|
|
color: var(--td-text-color-secondary);
|
|
}
|
|
.alert-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
.template-section,
|
|
.history-section {
|
|
padding: 20px;
|
|
background: var(--td-bg-color-container);
|
|
border: 1px solid var(--td-component-border);
|
|
border-radius: var(--td-radius-default);
|
|
}
|
|
.section-heading {
|
|
margin-bottom: 16px;
|
|
}
|
|
.section-heading h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
line-height: 26px;
|
|
}
|
|
.section-heading span {
|
|
color: var(--td-text-color-secondary);
|
|
font-size: 13px;
|
|
}
|
|
.template-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(240px, 1fr));
|
|
gap: 14px;
|
|
padding-top: 16px;
|
|
}
|
|
.template-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
width: 100%;
|
|
}
|
|
.template-title > span:first-child {
|
|
white-space: nowrap;
|
|
}
|
|
.template-body {
|
|
min-height: 154px;
|
|
}
|
|
.template-body h3 {
|
|
margin: 0 0 8px;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
}
|
|
.template-body p {
|
|
min-height: 44px;
|
|
margin: 0;
|
|
color: var(--td-text-color-secondary);
|
|
line-height: 22px;
|
|
}
|
|
.template-metrics {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-top: 14px;
|
|
color: var(--td-text-color-secondary);
|
|
font-size: 13px;
|
|
}
|
|
.template-metrics strong {
|
|
margin-left: 4px;
|
|
color: var(--td-text-color-primary);
|
|
font-size: 15px;
|
|
}
|
|
.template-actions {
|
|
min-height: 28px;
|
|
margin-top: 14px;
|
|
}
|
|
.template-roles {
|
|
display: flex;
|
|
min-height: 24px;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
}
|
|
.template-roles > span {
|
|
flex: 0 0 auto;
|
|
color: var(--td-text-color-secondary);
|
|
font-size: 12px;
|
|
}
|
|
.filter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
|
gap: 4px 20px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.filter-actions {
|
|
display: flex;
|
|
grid-column: 1 / -1;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
}
|
|
.form-name-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
.form-name-cell span {
|
|
color: var(--td-text-color-secondary);
|
|
font-size: 12px;
|
|
}
|
|
.pagination-row {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
}
|
|
@media (max-width: 1280px) {
|
|
.template-grid {
|
|
grid-template-columns: repeat(2, minmax(240px, 1fr));
|
|
}
|
|
.filter-grid {
|
|
grid-template-columns: repeat(3, minmax(160px, 1fr));
|
|
}
|
|
}
|
|
@media (max-width: 760px) {
|
|
.template-grid,
|
|
.filter-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.page-header {
|
|
flex-direction: column;
|
|
}
|
|
.template-section,
|
|
.history-section {
|
|
padding: 16px;
|
|
}
|
|
}
|
|
</style>
|