feat: 表格已读行操作标记

master^2
layhuts 2026-05-08 20:03:05 +08:00
parent 51e8b27d4c
commit e1f6449073
9 changed files with 882 additions and 2 deletions

View File

@ -46,6 +46,16 @@ export class VxeGridApi<
private stateHandler: StateHandler; private stateHandler: StateHandler;
// 已读行相关方法(由 use-vxe-grid.vue 注入)
private viewedRowHelper: null | {
clearViewed: () => void;
isViewed: (record: T) => boolean;
markAsViewed: (record: T) => void;
markKeysAsViewed: (keys: Array<number | string>) => void;
removeKeys: (keys: Array<number | string>) => void;
viewedSet: { value: Set<number | string> };
} = null;
constructor(options: VxeGridProps<T, D, P> = {} as VxeGridProps<T, D, P>) { constructor(options: VxeGridProps<T, D, P> = {} as VxeGridProps<T, D, P>) {
const storeState = { ...options }; const storeState = { ...options };
@ -64,6 +74,41 @@ export class VxeGridApi<
bindMethods(this); bindMethods(this);
} }
/**
*
*/
clearViewedRows() {
this.viewedRowHelper?.clearViewed();
}
/**
* key
*/
getViewedKeys(): Set<number | string> {
return this.viewedRowHelper?.viewedSet.value ?? new Set();
}
/**
*
*/
isRowViewed(record: T): boolean {
return this.viewedRowHelper?.isViewed(record) ?? false;
}
/**
*
*/
markKeysAsViewed(keys: Array<number | string>) {
this.viewedRowHelper?.markKeysAsViewed(keys);
}
/**
*
*/
markRowAsViewed(record: T) {
this.viewedRowHelper?.markAsViewed(record);
}
mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) { mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) {
if (!this.isMounted && instance) { if (!this.isMounted && instance) {
this.grid = instance; this.grid = instance;
@ -89,6 +134,13 @@ export class VxeGridApi<
} }
} }
/**
* key
*/
removeViewedKeys(keys: Array<number | string>) {
this.viewedRowHelper?.removeKeys(keys);
}
setGridOptions(options: Partial<VxeGridProps<T, D, P>['gridOptions']>) { setGridOptions(options: Partial<VxeGridProps<T, D, P>['gridOptions']>) {
this.setState({ this.setState({
gridOptions: options, gridOptions: options,
@ -117,6 +169,14 @@ export class VxeGridApi<
} }
} }
/**
* helper
* @internal
*/
setViewedRowHelper(helper: VxeGridApi<T, D, P>['viewedRowHelper']) {
this.viewedRowHelper = helper;
}
toggleSearchForm(show?: boolean) { toggleSearchForm(show?: boolean) {
this.setState({ this.setState({
showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm, showSearchForm: isBoolean(show) ? show : !this.state?.showSearchForm,

View File

@ -117,3 +117,12 @@
.vxe-grid--layout-body-content-wrapper { .vxe-grid--layout-body-content-wrapper {
overflow: hidden; overflow: hidden;
} }
/* 已读行默认样式 */
.vxe-row--viewed {
color: hsl(var(--foreground) / 50%);
.vxe-body--column {
opacity: 0.9;
}
}

View File

@ -2,6 +2,7 @@ import type {
VxeGridListeners, VxeGridListeners,
VxeGridPropTypes, VxeGridPropTypes,
VxeGridProps as VxeTableGridProps, VxeGridProps as VxeTableGridProps,
VxeTablePropTypes,
VxeUIExport, VxeUIExport,
} from 'vxe-table'; } from 'vxe-table';
@ -38,6 +39,71 @@ export interface SeparatorOptions {
backgroundColor?: string; backgroundColor?: string;
} }
/**
*
* APIIndexedDB wrapper
*/
export interface ViewedRowStorageAdapter {
/** 读取所有已查看的 key 列表 */
getKeys(): Promise<Array<number | string>>;
/** 移除所有已查看数据 */
removeKeys(): Promise<void>;
/** 持久化已查看的 key 列表 */
setKeys(keys: Array<number | string>): Promise<void>;
}
/**
*
*/
export interface ViewedRowPersistOptions {
/**
* 'localStorage'
* - memory:
* - localStorage: 使 localStorage
* - sessionStorage: 使 sessionStorage
* - indexedDB: 使 IndexedDB TTL
* - custom:
*/
type?: 'custom' | 'indexedDB' | 'localStorage' | 'memory' | 'sessionStorage';
/** 存储 key / prefixtype 为 localStorage/sessionStorage/indexedDB 时必传) */
key?: string;
/** 持久化数据的存活时间(毫秒) */
ttl?: number;
/** 最大缓存数量,超出时淘汰最早标记的 keyFIFO默认 100 */
maxSize?: number;
/** IndexedDB 数据库名称(仅 type='indexedDB' 时生效,默认 'viewed-table-db' */
dbName?: string;
/** IndexedDB 数据库版本(仅 type='indexedDB' 时生效,默认 1 */
dbVersion?: number;
/** IndexedDB 对象存储名称(仅 type='indexedDB' 时生效,默认 'viewed-table-row' */
storeName?: string;
/** 自定义存储适配器(仅 type='custom' 时生效,不传则降级为 memory */
storage?: ViewedRowStorageAdapter;
}
/**
* row
*/
export interface ViewedRowOptions<T = any> {
/** 点击 CellOperation 中匹配的 code 时,自动将该行标记为已读 */
actionCodes?: string | string[];
/** 行唯一标识字段,默认取 gridOptions.rowConfig.keyField最终兜底 'id' */
keyField?: string;
/** 已查看的行key列表 */
viewedKeys?: Array<number | string> | Ref<Array<number | string>>;
/**
*
* - string使 localStorage storage key
* - object
* - memory
*/
persist?: string | ViewedRowPersistOptions;
rowClassName?: VxeTablePropTypes.RowClassName<T>;
rowStyle?: VxeTablePropTypes.RowStyle<T>;
}
export interface VxeGridProps< export interface VxeGridProps<
T extends Record<string, any> = any, T extends Record<string, any> = any,
D extends BaseFormComponentType = BaseFormComponentType, D extends BaseFormComponentType = BaseFormComponentType,
@ -83,6 +149,10 @@ export interface VxeGridProps<
* *
*/ */
separator?: boolean | SeparatorOptions; separator?: boolean | SeparatorOptions;
/**
*
*/
viewedRow?: boolean | ViewedRowOptions<T>;
} }
export type ExtendedVxeGridApi< export type ExtendedVxeGridApi<

View File

@ -0,0 +1,503 @@
import type {VxeGridProps as VxeTableGridProps} from 'vxe-table';
import type {
ViewedRowOptions,
ViewedRowPersistOptions,
ViewedRowStorageAdapter,
} from './types';
import {isRef, shallowRef, toRaw, triggerRef, watch} from 'vue';
import {isBoolean, isFunction} from '@vben/utils';
import {
IndexedDBDriver,
LocalStorageDriver,
StorageManager,
} from '@vben-core/shared/cache';
import {useDebounceFn} from '@vueuse/core';
const DEFAULT_VIEWED_CLASS = 'vxe-row--viewed';
// ========== 持久化策略 ==========
/**
* localStorage / sessionStorage
* key [1, 2, 3]
*/
function createWebStorageAdapter(
storageType: 'localStorage' | 'sessionStorage',
key: string,
ttl?: number,
): ViewedRowStorageAdapter {
const manager = new StorageManager({
driver: new LocalStorageDriver({storageType}),
});
return {
async getKeys() {
const stored = await manager.getItem<Array<number | string>>(key);
return stored ?? [];
},
async removeKeys() {
await manager.removeItem(key);
},
async setKeys(keys) {
await manager.setItem(key, keys, ttl);
},
};
}
/**
* IndexedDB
* prefix:1 { expiry, value: 1 }
*/
function createIndexedDBAdapter(
opts: ViewedRowPersistOptions,
): ViewedRowStorageAdapter {
const prefix = opts.key || 'viewed';
const manager = new StorageManager({
driver: new IndexedDBDriver({
dbName: opts.dbName || 'viewed-table-db',
dbVersion: opts.dbVersion || 1,
storeName: opts.storeName || 'viewed-table-row',
}),
prefix,
});
return {
async getKeys() {
try {
// 通过 StorageManager 的 driver 获取所有 key再逐条读取自动过滤过期
const allKeys = (await (manager as any).driver.keys()) as string[];
const fullPrefix = prefix ? `${prefix}-` : '';
const prefixedKeys = allKeys.filter((k: string) =>
k.startsWith(fullPrefix),
);
const results: Array<number | string> = [];
for (const fullKey of prefixedKeys) {
const shortKey = fullKey.replace(fullPrefix, '');
const value = await manager.getItem<number | string>(shortKey);
if (value !== null) {
results.push(value);
}
}
return results;
} catch (error) {
console.error('[viewedRow] indexedDB restore failed:', error);
return [];
}
},
async removeKeys() {
try {
await manager.clear();
} catch (error) {
console.error('[viewedRow] indexedDB clear failed:', error);
}
},
async setKeys(keys) {
try {
// 先清除旧数据,再逐条写入
await manager.clear();
await Promise.all(
keys.map((key) => manager.setItem(String(key), key, opts.ttl)),
);
} catch (error) {
console.error('[viewedRow] indexedDB persist failed:', error);
}
},
};
}
/**
* persist
*/
function createStorageAdapter(
persist?: string | ViewedRowPersistOptions,
): null | ViewedRowStorageAdapter {
if (!persist) return null;
// 简写模式string → localStorage
if (typeof persist === 'string') {
return createWebStorageAdapter('localStorage', persist);
}
const {type = 'localStorage'} = persist;
switch (type) {
case 'custom': {
if (!persist.storage) {
// 没有提供 storage 适配器,降级为 memory
return null;
}
// 用户自定义适配器,解除 Vue 响应式代理
return toRaw(persist.storage);
}
case 'indexedDB': {
if (!persist.key) {
console.warn('[viewedRow] persist.key is required for indexedDB type');
return null;
}
return createIndexedDBAdapter(persist);
}
case 'localStorage': {
if (!persist.key) {
console.warn(
'[viewedRow] persist.key is required for localStorage type',
);
return null;
}
return createWebStorageAdapter('localStorage', persist.key, persist.ttl);
}
case 'memory': {
return null;
}
case 'sessionStorage': {
if (!persist.key) {
console.warn(
'[viewedRow] persist.key is required for sessionStorage type',
);
return null;
}
return createWebStorageAdapter(
'sessionStorage',
persist.key,
persist.ttl,
);
}
default: {
return null;
}
}
}
// ========== maxSize 淘汰 ==========
/**
* maxSize keyFIFO
*/
function enforceMaxSize(set: Set<number | string>, maxSize: number): void {
if (maxSize > 0 && set.size > maxSize) {
const iterator = set.values();
while (set.size > maxSize) {
const oldest = iterator.next().value;
if (oldest !== undefined) {
set.delete(oldest);
}
}
}
}
// ========== 核心 composable ==========
export function useViewedRow<T = any>(
options: ViewedRowOptions<T> & { keyField: string },
) {
// ========== 解析持久化配置 ==========
const persistOpts: null | ViewedRowPersistOptions = options.persist
? (typeof options.persist === 'string'
? {key: options.persist, type: 'localStorage'}
: options.persist)
: null;
const adapter = createStorageAdapter(options.persist);
const maxSize = persistOpts?.maxSize ?? 100;
// ========== 初始化已读集合 ==========
const viewedSet = shallowRef<Set<number | string>>(new Set());
// ========== 持久化(防抖) ==========
function persistImmediate() {
if (!adapter) return;
adapter.setKeys([...viewedSet.value]).catch((error) => {
console.error('[viewedRow] persist failed:', error);
});
}
const persist = useDebounceFn(persistImmediate, 300);
// ========== 从存储恢复 ==========
function restoreFromStorage() {
if (!adapter) return;
adapter
.getKeys()
.then((stored) => {
if (stored && stored.length > 0) {
for (const key of stored) {
viewedSet.value.add(key);
}
if (maxSize > 0) {
enforceMaxSize(viewedSet.value, maxSize);
}
triggerRef(viewedSet);
}
})
.catch((error) => {
console.error('[viewedRow] restore failed:', error);
});
}
restoreFromStorage();
// 合并外部传入的 viewedKeys
if (options.viewedKeys) {
const keys = isRef(options.viewedKeys)
? options.viewedKeys.value
: options.viewedKeys;
for (const key of keys) {
viewedSet.value.add(key);
}
}
// ========== 更新 viewedSet 的统一入口 ==========
function updateViewedSet(updater: (set: Set<number | string>) => boolean) {
const changed = updater(viewedSet.value);
if (changed) {
if (maxSize > 0) {
enforceMaxSize(viewedSet.value, maxSize);
}
triggerRef(viewedSet);
persist();
}
}
// ========== 监听外部 viewedKeys 变化(如果是 Ref ==========
if (isRef(options.viewedKeys)) {
watch(options.viewedKeys, (newKeys) => {
updateViewedSet((set) => {
let changed = false;
for (const key of newKeys) {
if (!set.has(key)) {
set.add(key);
changed = true;
}
}
return changed;
});
});
}
// ========== 标记已读 ==========
function markAsViewed(record: T) {
const key = (record as Record<string, any>)[options.keyField] as
| number
| string;
if (key === null || key === undefined) return;
updateViewedSet((set) => {
if (set.has(key)) return false;
set.add(key);
return true;
});
}
function markKeysAsViewed(keys: Array<number | string>) {
updateViewedSet((set) => {
let changed = false;
for (const key of keys) {
if (!set.has(key)) {
set.add(key);
changed = true;
}
}
return changed;
});
}
// ========== 查询 ==========
function isViewed(record: T): boolean {
const key = (record as Record<string, any>)[options.keyField] as
| number
| string;
return viewedSet.value.has(key);
}
// ========== 清除 ==========
function clearViewed() {
const hadData = viewedSet.value.size > 0;
viewedSet.value.clear();
if (hadData) {
triggerRef(viewedSet);
}
if (adapter) {
adapter.removeKeys().catch((error) => {
console.error('[viewedRow] clear persist failed:', error);
});
}
}
// ========== 移除指定 keys ==========
function removeKeys(keys: Array<number | string>) {
updateViewedSet((set) => {
let changed = false;
for (const key of keys) {
if (set.has(key)) {
set.delete(key);
changed = true;
}
}
return changed;
});
}
// ========== rowClassName 函数 ==========
function getRowClassName(params: any): string {
if (!isViewed(params.row)) return '';
const {rowClassName} = options;
if (rowClassName === undefined || rowClassName === null) {
return DEFAULT_VIEWED_CLASS;
}
if (typeof rowClassName === 'string') {
return rowClassName;
}
if (isFunction(rowClassName)) {
return normalizeClassName(rowClassName(params));
}
return DEFAULT_VIEWED_CLASS;
}
// ========== rowStyle 函数 ==========
function getRowStyle(params: any): any {
if (!isViewed(params.row)) return undefined;
const {rowStyle} = options;
if (rowStyle === undefined || rowStyle === null) {
return undefined;
}
if (isFunction(rowStyle)) {
return rowStyle(params);
}
return rowStyle;
}
return {
clearViewed,
getRowClassName,
getRowStyle,
isViewed,
markAsViewed,
markKeysAsViewed,
removeKeys,
viewedSet,
};
}
// ========== 工具函数 ==========
function normalizeClassName(value: any): string {
if (!value) return '';
if (typeof value === 'string') return value;
if (typeof value === 'object') {
return Object.entries(value)
.filter(([, v]) => v)
.map(([k]) => k)
.join(' ');
}
return '';
}
function mergeClassNames(...classNames: any[]): string {
return classNames
.map((c) => normalizeClassName(c))
.filter(Boolean)
.join(' ');
}
/**
* columns CellOperation onClick actionCodes
* columns cloneDeep
*/
function wrapColumnsForViewedRow(
columns: any[],
actionCodes: string[],
markAsViewed: (record: any) => void,
): any[] {
return columns.map((column) => {
if (!column || typeof column !== 'object') return column;
const nextColumn = {...column};
if (nextColumn.cellRender?.name === 'CellOperation') {
const cellRender = {...nextColumn.cellRender};
const attrs = {...cellRender.attrs};
const originalOnClick = attrs.onClick;
attrs.onClick = (params: { code: string; row: any }) => {
originalOnClick?.(params);
if (actionCodes.includes(params.code)) {
markAsViewed(params.row);
}
};
cellRender.attrs = attrs;
nextColumn.cellRender = cellRender;
}
if (Array.isArray(nextColumn.children)) {
nextColumn.children = wrapColumnsForViewedRow(
nextColumn.children,
actionCodes,
markAsViewed,
);
}
return nextColumn;
});
}
/**
* viewedRow mergedOptions
* rowClassNamerowStylecolumns
*/
export function applyViewedRowOptions(
mergedOptions: VxeTableGridProps,
viewedRowConfig: boolean | ViewedRowOptions,
helper: ReturnType<typeof useViewedRow>,
) {
// 注入 rowClassName
const originalRowClassName = mergedOptions.rowClassName;
mergedOptions.rowClassName = (params: any) => {
return mergeClassNames(
isFunction(originalRowClassName)
? originalRowClassName(params)
: originalRowClassName,
helper.getRowClassName(params),
);
};
// 注入 rowStyle
const originalRowStyle = mergedOptions.rowStyle;
mergedOptions.rowStyle = (params: any) => {
const viewedStyle = helper.getRowStyle(params);
const originalStyle = isFunction(originalRowStyle)
? originalRowStyle(params)
: originalRowStyle;
if (!viewedStyle && !originalStyle) return undefined;
if (!originalStyle) return viewedStyle;
if (!viewedStyle) return originalStyle;
return {...originalStyle, ...viewedStyle};
};
// 拦截 CellOperation columns
const actionCodes =
!isBoolean(viewedRowConfig) && viewedRowConfig.actionCodes
? (Array.isArray(viewedRowConfig.actionCodes)
? viewedRowConfig.actionCodes
: [viewedRowConfig.actionCodes])
: [];
if (actionCodes.length > 0 && Array.isArray(mergedOptions.columns)) {
mergedOptions.columns = wrapColumnsForViewedRow(
mergedOptions.columns,
actionCodes,
helper.markAsViewed,
);
}
}

View File

@ -19,10 +19,12 @@ import {
nextTick, nextTick,
onMounted, onMounted,
onUnmounted, onUnmounted,
shallowRef,
toRaw, toRaw,
useSlots, useSlots,
useTemplateRef, useTemplateRef,
watch, watch,
watchEffect,
} from 'vue'; } from 'vue';
import { usePriorityValues } from '@vben/hooks'; import { usePriorityValues } from '@vben/hooks';
@ -44,6 +46,7 @@ import { VxeGrid, VxeUI } from 'vxe-table';
import { extendProxyOptions } from './extends'; import { extendProxyOptions } from './extends';
import { useTableForm } from './init'; import { useTableForm } from './init';
import {applyViewedRowOptions, useViewedRow} from './use-viewed-row';
import 'vxe-table/styles/cssvar.scss'; import 'vxe-table/styles/cssvar.scss';
import 'vxe-pc-ui/styles/cssvar.scss'; import 'vxe-pc-ui/styles/cssvar.scss';
@ -76,8 +79,45 @@ const {
tableTitleHelp, tableTitleHelp,
showSearchForm, showSearchForm,
separator, separator,
viewedRow,
} = usePriorityValues(props, state); } = usePriorityValues(props, state);
// ========== viewedRow ==========
const defaultKeyField = (gridOptions.value?.rowConfig as any)?.keyField || 'id';
const viewedRowHelper = shallowRef<null | ReturnType<typeof useViewedRow>>(
null,
);
// + helper
watch(
viewedRow,
(cfg) => {
if (!cfg) {
viewedRowHelper.value = null;
props.api?.setViewedRowHelper?.(null);
return;
}
const resolvedOptions = isBoolean(cfg)
? {keyField: defaultKeyField}
: {keyField: defaultKeyField, ...cfg};
viewedRowHelper.value = useViewedRow(resolvedOptions);
// API helper
if (props.api?.setViewedRowHelper) {
props.api.setViewedRowHelper(viewedRowHelper.value);
}
},
{immediate: true},
);
// viewedSet grid
watchEffect(() => {
const helper = viewedRowHelper.value;
if (!helper) return;
// 访 viewedSet.value
void helper.viewedSet.value;
});
const { isMobile } = usePreferences(); const { isMobile } = usePreferences();
const isSeparator = computed(() => { const isSeparator = computed(() => {
if ( if (
@ -234,6 +274,16 @@ const options = computed(() => {
mergedOptions.data = tableData.value; mergedOptions.data = tableData.value;
} }
} }
// rowClassNamerowStylecolumns
if (viewedRow.value && viewedRowHelper.value) {
applyViewedRowOptions(
mergedOptions,
viewedRow.value,
viewedRowHelper.value,
);
}
return mergedOptions; return mergedOptions;
}); });

View File

@ -38,7 +38,8 @@
"editCell": "Edit Cell", "editCell": "Edit Cell",
"editRow": "Edit Row", "editRow": "Edit Row",
"custom-cell": "Custom Cell", "custom-cell": "Custom Cell",
"form": "Form Table" "form": "Form Table",
"viewed": "Row Marker"
}, },
"captcha": { "captcha": {
"title": "Captcha", "title": "Captcha",

View File

@ -41,7 +41,8 @@
"editCell": "单元格编辑", "editCell": "单元格编辑",
"editRow": "行编辑", "editRow": "行编辑",
"custom-cell": "自定义单元格", "custom-cell": "自定义单元格",
"form": "搜索表单" "form": "搜索表单",
"viewed": "行标记"
}, },
"captcha": { "captcha": {
"title": "验证码", "title": "验证码",

View File

@ -193,6 +193,14 @@ const routes: RouteRecordRaw[] = [
title: $t('examples.vxeTable.virtual'), title: $t('examples.vxeTable.virtual'),
}, },
}, },
{
name: 'VxeTableViewedExample',
path: '/examples/vxe-table/viewed',
component: () => import('#/views/examples/vxe-table/viewed.vue'),
meta: {
title: $t('examples.vxeTable.viewed'),
},
},
], ],
}, },
{ {

View File

@ -0,0 +1,178 @@
<script lang="ts" setup>
import type {OnActionClickParams, VxeGridProps} from '#/adapter/vxe-table';
import {ref} from 'vue';
import {Page, useVbenModal} from '@vben/common-ui';
import {$t} from '@vben/locales';
import {Button, message} from 'ant-design-vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {getExampleTableApi} from '#/api';
interface RowType {
category: string;
color: string;
id: string;
price: string;
productName: string;
releaseDate: string;
}
const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: 'name',
},
columns: [
{title: '序号', type: 'seq', width: 50},
{field: 'category', sortable: true, title: 'Category'},
{field: 'color', sortable: true, title: 'Color'},
{field: 'productName', sortable: true, title: 'Product Name'},
{field: 'price', sortable: true, title: 'Price'},
{field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime'},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'view',
text: '查看',
},
'edit',
],
},
field: 'operation',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
title: $t('system.menu.operation'),
width: 200,
},
],
exportConfig: {},
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({page, sort}) => {
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
sortBy: sort.field,
sortOrder: sort.order,
});
},
},
sort: true,
},
sortConfig: {
defaultSort: {field: 'category', order: 'desc'},
remote: true,
},
toolbarConfig: {
custom: true,
export: true,
// import: true,
refresh: true,
zoom: true,
},
};
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
viewedRow: {
//
actionCodes: ['view'],
//
keyField: 'id',
// 使 localStorage
// persist: 'viewed_rows',
persist: {
key: 'viewed-rows',
type: 'indexedDB',
ttl: 7 * 24 * 60 * 60 * 1000, // 7
maxSize: 200,
},
},
});
function onActionClick({code, row}: OnActionClickParams<RowType>) {
switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'view': {
onView(row);
break;
}
default: {
break;
}
}
}
const editRow = ref<RowType>();
const [Modal, modalApi] = useVbenModal({
draggable: true,
onConfirm: () => {
modalApi.setState({loading: true});
setTimeout(() => {
editRow.value && gridApi.markRowAsViewed(editRow.value);
modalApi.setState({loading: false});
modalApi.close();
}, 1500);
},
});
function onEdit(row: RowType) {
editRow.value = row;
modalApi.open();
}
function onView(row: RowType) {
message.success({
content: `查看${row.category}`,
key: 'action_process_msg_id',
});
}
function onCustomSet() {
gridApi.markKeysAsViewed([
'0da74a21-362d-42ba-9c7e-078e47477620',
'1c7785d9-f16b-448b-b6a2-fb4b3557550a',
]);
}
function onClearViewed() {
gridApi.clearViewedRows();
}
</script>
<template>
<Page
auto-content-height
description="表格行标记支持存储类型 custom | indexedDB | localStorage | memory | sessionStorage
默认使用memory存储当设置custom时需要自己实现getKeys()/setKeys()/removeKeys()
具体属性查看packages/effects/plugins/src/vxe-table/types.ts可通过gridApi调用
clearViewedRows()/getViewedKeys()/isRowViewed()/markKeysAsViewed()/markRowAsViewed()/removeViewedKeys()"
title="表格行标记示例"
>
<Modal class="w-150" title="数据修改"> 数据修改完成后设置行标记</Modal>
<Grid table-title="" table-title-help="">
<template #toolbar-tools>
<Button class="mr-2" type="primary" @click="onCustomSet">
手动设置
</Button>
<Button type="primary" @click="onClearViewed"> </Button>
</template>
</Grid>
</Page>
</template>