Order-revision workflow kept as is: the direct admin line-edit routes were not ported. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
762 lines
22 KiB
Vue
762 lines
22 KiB
Vue
<template>
|
|
<!--
|
|
Ported from legacy henza_admin/src/components/x-table/XTable.vue (572 lines).
|
|
|
|
Preserved: the server contract (POST {search, filters, page, limit, select,
|
|
titles, report_title}), the { rows, query } response shape, the filter bar,
|
|
the column show/hide picker, search, sorting and the named cell slots.
|
|
|
|
Dropped: the fullscreen toggle and the excel/json/print export buttons —
|
|
exports needed a legacy endpoint that returned a file url, which the new
|
|
backend does not implement. The buttons are omitted rather than left as
|
|
dead controls that silently fail.
|
|
-->
|
|
<div class="x-table" :class="{ 'x-table--cards': cardMode }">
|
|
<div class="x-table__toolbar">
|
|
<div class="x-table__search-group">
|
|
<a-input-search
|
|
v-if="!model._hideSearch"
|
|
v-model:value="query.search"
|
|
class="x-table__search"
|
|
placeholder="جستجو"
|
|
allow-clear
|
|
@search="onSearch"
|
|
/>
|
|
|
|
<div class="x-table__actions">
|
|
<a-badge v-if="!model._noFilters" :count="query.filters.length">
|
|
<a-button type="text" :class="{ 'x-table__action--active': activeFilter }" @click="toggleVisibleFilter">
|
|
فیلتر
|
|
<template #icon><FilterOutlined /></template>
|
|
</a-button>
|
|
</a-badge>
|
|
|
|
<a-tooltip title="چاپ">
|
|
<a-button type="text" shape="circle">
|
|
<template #icon><PrinterOutlined /></template>
|
|
</a-button>
|
|
</a-tooltip>
|
|
|
|
<a-dropdown :trigger="['click']">
|
|
<a-tooltip title="ستونها">
|
|
<a-button type="text" shape="circle">
|
|
<template #icon><SettingOutlined /></template>
|
|
</a-button>
|
|
</a-tooltip>
|
|
<template #overlay>
|
|
<a-menu>
|
|
<a-menu-item v-for="col in cols" :key="col.key">
|
|
<a-checkbox v-model:checked="col.show">{{ col.title }}</a-checkbox>
|
|
</a-menu-item>
|
|
</a-menu>
|
|
</template>
|
|
</a-dropdown>
|
|
|
|
<a-tooltip title="بارگذاری مجدد">
|
|
<a-button class="refresh-btn" type="text" shape="circle" :loading="loading" @click="fetch()">
|
|
<template #icon><ReloadOutlined /></template>
|
|
</a-button>
|
|
</a-tooltip>
|
|
|
|
<!-- card view has no column headers to click, so sorting moves here -->
|
|
<a-select
|
|
v-if="cardMode && sortOptions.length"
|
|
:value="sortValue"
|
|
class="x-table__sort"
|
|
size="small"
|
|
placeholder="مرتبسازی"
|
|
allow-clear
|
|
:options="sortOptions"
|
|
@change="onSortSelect"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="$slots.add" class="x-table__add">
|
|
<slot name="add" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- filter bar: one row per active filter -->
|
|
<div v-if="activeFilter" class="x-table__filters">
|
|
<div v-for="(f, index) in query.filters" :key="f.id" class="x-table__filter-row">
|
|
<a-select
|
|
v-model:value="f.field"
|
|
placeholder="ستون"
|
|
style="width: 180px"
|
|
:options="filterableOptions"
|
|
/>
|
|
<a-select v-model:value="f.op" placeholder="شرط" style="width: 130px" :options="opOptions" />
|
|
<!--
|
|
A column declared with filterSelect({...}) gets a dropdown of its
|
|
value->label map; a date column gets a date picker; everything else
|
|
gets a free-text box, as in legacy.
|
|
-->
|
|
<a-select
|
|
v-if="selectItemsFor(f.field)"
|
|
v-model:value="f.value"
|
|
placeholder="مقدار"
|
|
style="width: 200px"
|
|
:options="selectItemsFor(f.field)"
|
|
/>
|
|
<a-date-picker
|
|
v-else-if="isDateField(f.field)"
|
|
v-model:value="f.value"
|
|
placeholder="مقدار"
|
|
style="width: 200px"
|
|
value-format="YYYY-MM-DD"
|
|
/>
|
|
<a-input v-else v-model:value="f.value" placeholder="مقدار" style="width: 200px" />
|
|
<a-input
|
|
v-if="f.op === 'between'"
|
|
v-model:value="f.value2"
|
|
placeholder="تا"
|
|
style="width: 200px"
|
|
/>
|
|
<a-button danger @click="removeFilter(index)">حذف</a-button>
|
|
</div>
|
|
<div class="x-table__filter-actions">
|
|
<a-button @click="addFilterRow">افزودن فیلتر</a-button>
|
|
<a-button type="primary" @click="doFilter">اعمال</a-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="cardMode" class="x-table__cards">
|
|
<a-spin :spinning="loading">
|
|
<a-empty v-if="!data.length && !loading" :description="locale.emptyText" />
|
|
<a-checkbox
|
|
v-if="hasSelection && data.length"
|
|
class="x-table__select-all"
|
|
:checked="allOnPageSelected"
|
|
:indeterminate="someOnPageSelected && !allOnPageSelected"
|
|
@change="(e: any) => selectPage(e.target.checked)"
|
|
>
|
|
انتخاب همه در این صفحه
|
|
</a-checkbox>
|
|
<div
|
|
v-for="record in data"
|
|
:key="String(record.id)"
|
|
class="x-card"
|
|
:class="{ 'x-card--selected': hasSelection && isSelected(record) }"
|
|
>
|
|
<div class="x-card__head">
|
|
<div class="x-card__title">
|
|
<a-checkbox
|
|
v-if="hasSelection"
|
|
:checked="isSelected(record)"
|
|
@change="(e: any) => toggleRow(record, e.target.checked)"
|
|
/>
|
|
<span v-if="cardMediaCol" class="x-card__media">
|
|
<slot :name="cardMediaCol.slot" :text="cellValue(record, cardMediaCol)" :record="record" />
|
|
</span>
|
|
<span v-if="cardIdCol" class="x-card__id">#{{ cellValue(record, cardIdCol) }}</span>
|
|
<span class="x-card__name">
|
|
<template v-for="col in cardTitleCols" :key="col.key">
|
|
<slot v-if="col.slot" :name="col.slot" :text="cellValue(record, col)" :record="record" />
|
|
<template v-else>{{ displayValue(record, col) }} </template>
|
|
</template>
|
|
</span>
|
|
</div>
|
|
<div v-if="cardActionCol" class="x-card__actions">
|
|
<slot :name="cardActionCol.slot" :text="cellValue(record, cardActionCol)" :record="record" />
|
|
</div>
|
|
</div>
|
|
<dl v-if="cardBodyCols.length" class="x-card__body">
|
|
<div v-for="col in cardBodyCols" :key="col.key" class="x-card__row">
|
|
<dt>{{ col.title }}</dt>
|
|
<dd>
|
|
<slot v-if="col.slot" :name="col.slot" :text="cellValue(record, col)" :record="record" />
|
|
<template v-else>{{ displayValue(record, col) }}</template>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</a-spin>
|
|
|
|
<div v-if="!model._hidePagination && query.total > 0" class="x-table__card-pager">
|
|
<span class="x-table__card-total">{{ query.total }} مورد</span>
|
|
<a-pagination
|
|
size="small"
|
|
show-less-items
|
|
:current="query.page"
|
|
:page-size="query.limit"
|
|
:total="query.total"
|
|
:show-size-changer="false"
|
|
@change="onCardPage"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<a-table
|
|
v-else
|
|
:columns="antColumns"
|
|
:data-source="data"
|
|
:loading="loading"
|
|
:pagination="paginationConfig"
|
|
:locale="locale"
|
|
:row-selection="rowSelection"
|
|
row-key="id"
|
|
:size="layout.isMobile ? 'small' : 'middle'"
|
|
:scroll="layout.isSmallScreen ? { x: 'max-content' } : undefined"
|
|
bordered
|
|
@change="onTableChange"
|
|
>
|
|
<template #bodyCell="{ column, record, text }">
|
|
<!--
|
|
Legacy used named scoped slots (renderSlot('opt') -> slot="opt").
|
|
Vue 3 routes them through #bodyCell, and the slot name is forwarded to
|
|
the parent under the same name, so ported views keep their markup.
|
|
-->
|
|
<template v-if="slotNameFor(column)">
|
|
<slot :name="slotNameFor(column)" :text="text" :record="record" />
|
|
</template>
|
|
<template v-else-if="column.__date">{{ gridDate(text) }}</template>
|
|
</template>
|
|
</a-table>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
import { FilterOutlined, PrinterOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
|
import api from '../../api/client';
|
|
import type { XTbl, Xtc } from './model';
|
|
import { gridDate } from '../../utils/filters';
|
|
import { useLayoutStore } from '../../stores/layout';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
model: XTbl;
|
|
/** legacy prop: renders a checkbox column (ProductList uses this) */
|
|
hasSelection?: boolean;
|
|
/** legacy used v-model for the selected rows */
|
|
modelValue?: Record<string, unknown>[];
|
|
/** On phones, render each row as a card instead of a wide table. Opt-in per page. */
|
|
mobileCards?: boolean;
|
|
}>(),
|
|
{ hasSelection: false, modelValue: () => [], mobileCards: false },
|
|
);
|
|
|
|
const layout = useLayoutStore();
|
|
const cardMode = computed(() => props.mobileCards && layout.isMobile);
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'query', q: unknown): void;
|
|
(e: 'update:modelValue', rows: Record<string, unknown>[]): void;
|
|
}>();
|
|
|
|
/** Only present when hasSelection is set, so other tables are unaffected. */
|
|
const rowSelection = computed(() =>
|
|
props.hasSelection
|
|
? {
|
|
selectedRowKeys: props.modelValue.map((r) => r.id as string),
|
|
onChange: (_keys: (string | number)[], rows: Record<string, unknown>[]) => {
|
|
emit('update:modelValue', rows);
|
|
},
|
|
}
|
|
: undefined,
|
|
);
|
|
|
|
interface FilterRow {
|
|
id: number;
|
|
field?: string;
|
|
op?: string;
|
|
value?: unknown;
|
|
value2?: unknown;
|
|
}
|
|
|
|
const data = ref<Record<string, unknown>[]>([]);
|
|
const loading = ref(false);
|
|
const activeFilter = ref(false);
|
|
const sort = ref<string | undefined>(undefined);
|
|
const sortDirection = ref<'asc' | 'desc'>('asc');
|
|
|
|
// legacy data.query
|
|
const query = reactive({
|
|
search: undefined as string | undefined,
|
|
filters: [] as FilterRow[],
|
|
total: 0,
|
|
limit: 10,
|
|
page: 1,
|
|
});
|
|
|
|
const locale = { emptyText: 'هیج رکوردی یافت نشد.' };
|
|
|
|
const opOptions = [
|
|
{ value: '=', label: 'برابر' },
|
|
{ value: '!=', label: 'نامساوی' },
|
|
{ value: 'like', label: 'شامل' },
|
|
{ value: '>', label: 'بزرگتر' },
|
|
{ value: '>=', label: 'بزرگتر مساوی' },
|
|
{ value: '<', label: 'کوچکتر' },
|
|
{ value: '<=', label: 'کوچکتر مساوی' },
|
|
{ value: 'between', label: 'بین' },
|
|
];
|
|
|
|
// The model is a plain class instance (see model.ts), so its columns are not
|
|
// reactive on their own. Wrapping it here lets the column picker's `col.show`
|
|
// checkbox toggles re-run visibleCols / antColumns.
|
|
const cols = computed<Xtc[]>(() => (props.model ? (reactive(props.model) as XTbl).cols : []));
|
|
const visibleCols = computed(() => cols.value.filter((c) => c.show));
|
|
const filterableOptions = computed(() =>
|
|
cols.value.filter((c) => c.filtered).map((c) => ({ value: c.__fkey ?? c.key, label: c.title })),
|
|
);
|
|
|
|
/** legacy `order` computed: '+field' / '-field' */
|
|
const order = computed(() => {
|
|
if (!sort.value) return undefined;
|
|
// A column may be keyed by the reference's name for it; its filter key, when it
|
|
// has one, is the real field it stands for, so that is what is sorted on.
|
|
const col = cols.value.find((c) => c.key === sort.value);
|
|
return `${sortDirection.value === 'asc' ? '+' : '-'}${col?.__fkey ?? sort.value}`;
|
|
});
|
|
|
|
const antColumns = computed(() =>
|
|
visibleCols.value.map((c) => ({
|
|
key: c.key,
|
|
title: c.title,
|
|
dataIndex: c.dataIndex.includes('.') ? c.dataIndex.split('.') : c.dataIndex,
|
|
sorter: c.__sorter,
|
|
width: c.width,
|
|
__slot: c.scopedSlots?.customRender,
|
|
__date: c.filterType === 'date',
|
|
})),
|
|
);
|
|
|
|
function slotNameFor(column: { __slot?: string }) {
|
|
return column.__slot;
|
|
}
|
|
|
|
// ── phone card view ──────────────────────────────────────────────────
|
|
interface CardCol {
|
|
key: string;
|
|
title: string;
|
|
path: string[];
|
|
slot?: string;
|
|
date: boolean;
|
|
}
|
|
|
|
const ACTION_KEYS = ['opt', 'op', 'action'];
|
|
|
|
const cardCols = computed<CardCol[]>(() =>
|
|
visibleCols.value.map((c: Xtc) => ({
|
|
key: c.key,
|
|
title: c.title,
|
|
path: c.dataIndex.split('.'),
|
|
slot: c.scopedSlots?.customRender,
|
|
date: c.filterType === 'date',
|
|
})),
|
|
);
|
|
const cardActionCol = computed(() => cardCols.value.find((c) => ACTION_KEYS.includes(c.key) && c.slot));
|
|
const cardIdCol = computed(() => cardCols.value.find((c) => c.key === 'id'));
|
|
const cardMediaCol = computed(() => cardCols.value.find((c) => c.slot === 'image'));
|
|
const cardTitleCols = computed<CardCol[]>(() => {
|
|
const rest = cardCols.value.filter(
|
|
(c) => c !== cardActionCol.value && c !== cardIdCol.value && c !== cardMediaCol.value,
|
|
);
|
|
const named = rest.find((c) => c.key === 'name' || c.key === 'title');
|
|
if (named) return [named];
|
|
const fname = rest.find((c) => c.key === 'fname');
|
|
if (fname) return [fname, ...rest.filter((c) => c.key === 'lname')];
|
|
return rest.slice(0, 1);
|
|
});
|
|
const cardBodyCols = computed(() =>
|
|
cardCols.value.filter(
|
|
(c) =>
|
|
c !== cardActionCol.value &&
|
|
c !== cardIdCol.value &&
|
|
c !== cardMediaCol.value &&
|
|
!cardTitleCols.value.includes(c),
|
|
),
|
|
);
|
|
|
|
// card-mode selection, same v-model contract as the table's rowSelection
|
|
function isSelected(record: Record<string, unknown>) {
|
|
return props.modelValue.some((r) => r.id === record.id);
|
|
}
|
|
function toggleRow(record: Record<string, unknown>, checked: boolean) {
|
|
const rest = props.modelValue.filter((r) => r.id !== record.id);
|
|
emit('update:modelValue', checked ? [...rest, record] : rest);
|
|
}
|
|
const allOnPageSelected = computed(() => data.value.length > 0 && data.value.every(isSelected));
|
|
const someOnPageSelected = computed(() => data.value.some(isSelected));
|
|
function selectPage(checked: boolean) {
|
|
const pageIds = new Set(data.value.map((r) => r.id));
|
|
const rest = props.modelValue.filter((r) => !pageIds.has(r.id));
|
|
emit('update:modelValue', checked ? [...rest, ...data.value] : rest);
|
|
}
|
|
|
|
function cellValue(record: Record<string, unknown>, col: CardCol): unknown {
|
|
return col.path.reduce<unknown>(
|
|
(v, k) => (v && typeof v === 'object' ? (v as Record<string, unknown>)[k] : undefined),
|
|
record,
|
|
);
|
|
}
|
|
|
|
function displayValue(record: Record<string, unknown>, col: CardCol) {
|
|
const v = cellValue(record, col);
|
|
if (v === null || v === undefined || v === '') return '-';
|
|
return col.date ? gridDate(v) : v;
|
|
}
|
|
|
|
const sortOptions = computed(() =>
|
|
visibleCols.value
|
|
.filter((c) => c.__sorter)
|
|
.flatMap((c) => [
|
|
{ value: `${c.key}|asc`, label: `${c.title} ↑` },
|
|
{ value: `${c.key}|desc`, label: `${c.title} ↓` },
|
|
]),
|
|
);
|
|
const sortValue = computed(() => (sort.value ? `${sort.value}|${sortDirection.value}` : undefined));
|
|
|
|
function onSortSelect(value?: string) {
|
|
if (value) {
|
|
const [key, dir] = value.split('|');
|
|
sort.value = key;
|
|
sortDirection.value = dir as 'asc' | 'desc';
|
|
} else {
|
|
sort.value = undefined;
|
|
}
|
|
query.page = 1;
|
|
fetch();
|
|
}
|
|
|
|
function onCardPage(page: number, pageSize: number) {
|
|
query.page = page;
|
|
query.limit = pageSize;
|
|
fetch();
|
|
document.getElementById('content-area')?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
/** Resolves a filter row's column back to its Xtc definition. */
|
|
function colForField(field?: string) {
|
|
if (!field) return undefined;
|
|
return cols.value.find((c) => (c.__fkey ?? c.key) === field);
|
|
}
|
|
|
|
/** Dropdown options for a filterSelect column, or undefined for other types. */
|
|
function selectItemsFor(field?: string) {
|
|
const col = colForField(field);
|
|
if (col?.filterType !== 'select' || !col.filterItems) return undefined;
|
|
return Object.entries(col.filterItems).map(([value, label]) => ({ value, label }));
|
|
}
|
|
|
|
function isDateField(field?: string) {
|
|
return colForField(field)?.filterType === 'date';
|
|
}
|
|
|
|
const paginationConfig = computed(() =>
|
|
props.model?._hidePagination
|
|
? false
|
|
: {
|
|
current: query.page,
|
|
pageSize: query.limit,
|
|
total: query.total,
|
|
showSizeChanger: true,
|
|
},
|
|
);
|
|
|
|
/** legacy requestData() */
|
|
function requestData(op?: string) {
|
|
const titles: Record<string, string> = {};
|
|
visibleCols.value.forEach((c) => {
|
|
titles[c.key] = c.title;
|
|
});
|
|
const select = visibleCols.value.map((c) => c.key);
|
|
const base = {
|
|
search: query.search,
|
|
filters: query.filters.filter((f) => f.field && f.op),
|
|
total: query.total,
|
|
limit: query.limit,
|
|
page: query.page,
|
|
order: order.value,
|
|
select,
|
|
titles,
|
|
report_title: props.model?.title,
|
|
};
|
|
return op ? { ...base, op } : base;
|
|
}
|
|
|
|
/** legacy fetch() + getDataUsage() in 'laravel' mode */
|
|
async function fetch(op?: string) {
|
|
if (!props.model) return;
|
|
try {
|
|
loading.value = true;
|
|
const { data: res } = await api.post(props.model.url, requestData(op));
|
|
data.value = res.rows;
|
|
query.total = res.query?.total ?? 0;
|
|
query.page = res.query?.page ?? query.page;
|
|
query.limit = res.query?.limit ?? query.limit;
|
|
emit('query', res.query);
|
|
} catch (error) {
|
|
console.log(error);
|
|
throw error;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function onSearch(value: string) {
|
|
query.search = value;
|
|
query.page = 1;
|
|
fetch();
|
|
}
|
|
|
|
function toggleVisibleFilter() {
|
|
activeFilter.value = !activeFilter.value;
|
|
if (activeFilter.value && query.filters.length === 0) addFilterRow();
|
|
}
|
|
|
|
/** legacy addFilterRow(): id is a random float used only as a list key */
|
|
function addFilterRow() {
|
|
query.filters.push({ id: parseFloat(`${query.filters.length}.${Math.floor(Math.random() * 1000)}`) });
|
|
}
|
|
|
|
function removeFilter(index: number) {
|
|
query.filters.splice(index, 1);
|
|
}
|
|
|
|
function doFilter() {
|
|
activeFilter.value = false;
|
|
query.page = 1;
|
|
fetch();
|
|
}
|
|
|
|
function onTableChange(
|
|
pagination: { current?: number; pageSize?: number },
|
|
_filters: unknown,
|
|
sorter: { columnKey?: string; order?: string },
|
|
) {
|
|
query.page = pagination.current ?? 1;
|
|
query.limit = pagination.pageSize ?? query.limit;
|
|
if (sorter?.columnKey && sorter.order) {
|
|
sort.value = String(sorter.columnKey);
|
|
sortDirection.value = sorter.order === 'ascend' ? 'asc' : 'desc';
|
|
} else {
|
|
sort.value = undefined;
|
|
}
|
|
fetch();
|
|
}
|
|
|
|
onMounted(() => {
|
|
// legacy applied model.defaultFilters before the first fetch
|
|
for (const df of props.model?.defaultFilters ?? []) {
|
|
query.filters.push({ id: Math.random(), ...df });
|
|
}
|
|
fetch();
|
|
});
|
|
|
|
defineExpose({ fetch });
|
|
</script>
|
|
|
|
<style scoped>
|
|
.x-table__toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.x-table__search-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24px;
|
|
flex-wrap: nowrap;
|
|
}
|
|
.x-table__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: nowrap;
|
|
}
|
|
.x-table__action--active {
|
|
color: #1677ff;
|
|
}
|
|
.x-table__add {
|
|
display: flex;
|
|
}
|
|
.x-table__filters {
|
|
background: #fafafa;
|
|
border: 1px solid #f0f0f0;
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.x-table__filter-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.x-table__filter-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.refresh-btn {
|
|
transition: all 0.15s ease;
|
|
}
|
|
.x-table__search {
|
|
max-width: 260px;
|
|
}
|
|
.x-table__sort {
|
|
width: 150px;
|
|
}
|
|
|
|
/* ── phone card view ── */
|
|
.x-table__cards {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.x-table__cards :deep(.ant-spin-container) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
min-height: 80px;
|
|
}
|
|
.x-card {
|
|
border: 1px solid #eef0f3;
|
|
border-radius: 10px;
|
|
background: #fff;
|
|
overflow: hidden;
|
|
}
|
|
.x-card__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding: 10px 12px;
|
|
background: #fafbfc;
|
|
border-bottom: 1px solid #eef0f3;
|
|
}
|
|
.x-card__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
}
|
|
.x-card__id {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
color: #8a909c;
|
|
background: #eef0f3;
|
|
border-radius: 4px;
|
|
padding: 1px 6px;
|
|
direction: ltr;
|
|
}
|
|
.x-card__name {
|
|
font-weight: 600;
|
|
color: #1c2d56;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.x-card__actions {
|
|
flex-shrink: 0;
|
|
font-size: 16px;
|
|
}
|
|
.x-card--selected {
|
|
border-color: #91caff;
|
|
}
|
|
.x-card--selected .x-card__head {
|
|
background: #f0f7ff;
|
|
}
|
|
.x-card__media {
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
}
|
|
.x-card__media :deep(img) {
|
|
width: 44px !important;
|
|
height: 44px !important;
|
|
object-fit: cover !important;
|
|
border-radius: 6px;
|
|
}
|
|
.x-table__select-all {
|
|
font-size: 12px;
|
|
color: #8a909c;
|
|
}
|
|
.x-card__body {
|
|
margin: 0;
|
|
padding: 4px 12px;
|
|
}
|
|
.x-card__row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 7px 0;
|
|
font-size: 13px;
|
|
}
|
|
.x-card__row + .x-card__row {
|
|
border-top: 1px dashed #f0f0f0;
|
|
}
|
|
.x-card__row dt {
|
|
color: #8a909c;
|
|
flex-shrink: 0;
|
|
}
|
|
.x-card__row dd {
|
|
margin: 0;
|
|
color: #333;
|
|
text-align: left;
|
|
min-width: 0;
|
|
word-break: break-word;
|
|
}
|
|
.x-card__row dd :deep(.ant-tag) {
|
|
margin: 0;
|
|
}
|
|
.x-table__card-pager {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
.x-table__card-total {
|
|
font-size: 12px;
|
|
color: #8a909c;
|
|
}
|
|
|
|
@media (max-width: 575px) {
|
|
.x-table__toolbar {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
/* flatten so search takes row one and actions + add share row two */
|
|
.x-table__search-group {
|
|
display: contents;
|
|
}
|
|
.x-table__search {
|
|
max-width: none;
|
|
flex: 1 1 100%;
|
|
}
|
|
.x-table__actions {
|
|
gap: 2px;
|
|
}
|
|
.x-table__sort {
|
|
width: 130px;
|
|
}
|
|
.x-table__add {
|
|
margin-inline-start: auto;
|
|
}
|
|
.x-table__filters {
|
|
padding: 10px;
|
|
}
|
|
.x-table__filter-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px dashed #e8e8e8;
|
|
}
|
|
/* the filter inputs carry fixed inline widths for desktop */
|
|
.x-table__filter-row > * {
|
|
width: 100% !important;
|
|
}
|
|
.x-table__filter-row > :nth-child(n + 3) {
|
|
grid-column: 1 / -1;
|
|
}
|
|
.x-table__filter-actions > * {
|
|
flex: 1;
|
|
}
|
|
}
|
|
</style>
|