功能:按资源配置平台列表搜索
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"staff-organization:check": "node scripts/check-staff-organization-linkage.mjs",
|
||||
"staff-relations:check": "node scripts/check-staff-relation-policy.mjs",
|
||||
"user-address-display:check": "node scripts/check-user-address-relation-display.mjs",
|
||||
"resource-search:check": "node scripts/check-resource-search.mjs",
|
||||
"audit:platform": "node scripts/check-backend-contract.mjs",
|
||||
"lint": "biome lint .",
|
||||
"lint:fix": "biome lint --write .",
|
||||
|
||||
@@ -32,6 +32,11 @@ const embeddedResources = new Set([
|
||||
'payment_refund',
|
||||
'wallet_apply_cash',
|
||||
]);
|
||||
const fieldLabelKeys = new Set(
|
||||
[...source.matchAll(/^\s{2}([a-z0-9_]+):\s*'/gm)].map(
|
||||
(match) => match[1],
|
||||
),
|
||||
);
|
||||
|
||||
for (const name of frontendNames) {
|
||||
const item = backend.get(name);
|
||||
@@ -50,8 +55,31 @@ for (const item of contract.resources) {
|
||||
);
|
||||
if (!embeddedResources.has(item.name) && !routes.includes(`'/${item.name}'`))
|
||||
throw new Error(`缺少后端资源路由:${item.name}`);
|
||||
const searchKeys = new Set();
|
||||
for (const field of item.searchFields ?? []) {
|
||||
if (searchKeys.has(field.key))
|
||||
throw new Error(`搜索字段重复:${item.name}.${field.key}`);
|
||||
searchKeys.add(field.key);
|
||||
if (!fieldLabelKeys.has(field.key))
|
||||
throw new Error(`搜索字段缺少中文列名:${item.name}.${field.key}`);
|
||||
if (!['text', 'enum'].includes(field.kind))
|
||||
throw new Error(`搜索字段类型无效:${item.name}.${field.key}`);
|
||||
if (field.kind === 'enum') {
|
||||
if (!field.values?.length)
|
||||
throw new Error(`搜索枚举缺少值:${item.name}.${field.key}`);
|
||||
for (const value of field.values) {
|
||||
if (!value.value || !value.label || value.value === value.label)
|
||||
throw new Error(`搜索枚举中英文映射无效:${item.name}.${field.key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.pageKind === 'tree' && searchKeys.size)
|
||||
throw new Error(`树形资源不应声明标准列表搜索:${item.name}`);
|
||||
}
|
||||
|
||||
if (backend.get('user_address')?.searchFields?.length)
|
||||
throw new Error('用户地址包含动态用户关系,本轮必须隐藏搜索框');
|
||||
|
||||
const forbidden = [
|
||||
'/platform/platform_',
|
||||
'/gas/gas_',
|
||||
|
||||
40
frontend/platform_admin/scripts/check-resource-search.mjs
Normal file
40
frontend/platform_admin/scripts/check-resource-search.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 功能:校验标准资源列表仅在具备真实搜索能力时显示动态中文提示。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const listPage = readFileSync(
|
||||
resolve(root, 'src/views/shared/CrudListPage.vue'),
|
||||
'utf8',
|
||||
);
|
||||
const searchState = readFileSync(
|
||||
resolve(root, 'src/views/shared/use-resource-list-search.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const resources = readFileSync(resolve(root, 'src/api/resources.ts'), 'utf8');
|
||||
|
||||
const expectations = [
|
||||
[listPage, 'v-if="searchEnabled"', '不支持搜索时隐藏表单'],
|
||||
[listPage, ':placeholder="searchPlaceholder"', '动态显示可搜索字段'],
|
||||
[listPage, 'const keyword = requestKeyword();', '只发送受支持的关键字'],
|
||||
[searchState, 'definition.value.searchFields', '读取后端搜索契约'],
|
||||
[searchState, 'resourceListDisplayFields', '搜索字段与实际列表列求交集'],
|
||||
[searchState, 'if (searchEnabled.value || !route.query.keyword) return;', '清理不支持页面的陈旧关键字'],
|
||||
[searchState, '`可搜索:${searchableFields.value', '明确列出可搜索中文字段'],
|
||||
[resources, 'searchFields: resourceSearchFields(name)', '资源定义消费后端搜索契约'],
|
||||
[resources, "resourceSearchEnumOptions('staff_account', 'role_code')", '工作人员中文角色与搜索契约同源'],
|
||||
[resources, "resourceSearchEnumOptions('gasorder_basic', 'creator_type')", '订单创建方中文枚举与搜索契约同源'],
|
||||
];
|
||||
|
||||
for (const [content, fragment, description] of expectations) {
|
||||
if (!content.includes(fragment)) throw new Error(`资源搜索检查失败:${description}`);
|
||||
}
|
||||
if (listPage.includes('关键字段模糊搜索')) {
|
||||
throw new Error('仍存在未说明具体字段的旧搜索提示');
|
||||
}
|
||||
|
||||
console.log('资源搜索检查通过:动态提示、隐藏逻辑、URL 清理和中文枚举均已覆盖');
|
||||
47
frontend/platform_admin/src/api/resource-search-contract.ts
Normal file
47
frontend/platform_admin/src/api/resource-search-contract.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 功能描述:读取后端生成的平台资源搜索契约,统一搜索字段和中文枚举选项。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import platformContract from '@/contracts/platform-resources.json';
|
||||
|
||||
export type ResourceSearchValue = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type ResourceSearchField = {
|
||||
key: string;
|
||||
kind: 'text' | 'enum';
|
||||
values?: ResourceSearchValue[];
|
||||
};
|
||||
|
||||
type ContractResource = {
|
||||
name: string;
|
||||
searchFields?: ResourceSearchField[];
|
||||
};
|
||||
|
||||
const searchFieldsByResource = Object.fromEntries(
|
||||
(platformContract.resources as ContractResource[]).map((resource) => [
|
||||
resource.name,
|
||||
resource.searchFields ?? [],
|
||||
]),
|
||||
) as Record<string, ResourceSearchField[]>;
|
||||
|
||||
/** 返回后端已确认可用的资源搜索字段副本。 */
|
||||
export function resourceSearchFields(name: string): ResourceSearchField[] {
|
||||
return (searchFieldsByResource[name] ?? []).map((field) => ({
|
||||
...field,
|
||||
values: field.values?.map((value) => ({ ...value })),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 返回搜索枚举的中文展示选项,确保页面显示与后端匹配规则同源。 */
|
||||
export function resourceSearchEnumOptions(resource: string, key: string) {
|
||||
const field = (searchFieldsByResource[resource] ?? []).find(
|
||||
(item) => item.key === key && item.kind === 'enum',
|
||||
);
|
||||
if (!field?.values?.length) {
|
||||
throw new Error(`资源搜索枚举缺失:${resource}.${key}`);
|
||||
}
|
||||
return field.values.map((value) => ({ ...value }));
|
||||
}
|
||||
@@ -4,6 +4,12 @@
|
||||
| 'append_only'
|
||||
| 'editable'
|
||||
| 'managed';
|
||||
import {
|
||||
type ResourceSearchField,
|
||||
resourceSearchEnumOptions,
|
||||
resourceSearchFields,
|
||||
} from './resource-search-contract';
|
||||
|
||||
export type ResourcePageKind = 'list' | 'tree';
|
||||
export type ResourceFieldType =
|
||||
| 'text'
|
||||
@@ -63,6 +69,7 @@ export type ResourceUiDefinition = {
|
||||
mode: ResourceMode;
|
||||
pageKind: ResourcePageKind;
|
||||
fields: ResourceField[];
|
||||
searchFields: ResourceSearchField[];
|
||||
detailActions?: DetailAction[];
|
||||
canCreate: boolean;
|
||||
canEdit: boolean;
|
||||
@@ -302,13 +309,13 @@ function relation(
|
||||
}
|
||||
|
||||
/** 创建固定管理员角色字段,页面展示中文名称,接口仍使用稳定编码。 */
|
||||
function fixedAdminRole(label: string): ResourceField {
|
||||
function fixedAdminRole(resource: string): ResourceField {
|
||||
return f('role_code', {
|
||||
label: '角色',
|
||||
listLabel: '角色',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ label, value: 'admin' }],
|
||||
options: resourceSearchEnumOptions(resource, 'role_code'),
|
||||
defaultValue: 'admin',
|
||||
readonlyOnCreate: true,
|
||||
unknownValueLabel: '未知角色',
|
||||
@@ -343,6 +350,7 @@ function define(
|
||||
mode,
|
||||
pageKind,
|
||||
fields,
|
||||
searchFields: resourceSearchFields(name),
|
||||
...defaults,
|
||||
...capabilities,
|
||||
...(detailActions ? { detailActions } : {}),
|
||||
@@ -353,10 +361,10 @@ const reason = [f('reason', { required: true })];
|
||||
|
||||
export const resources: ResourceUiDefinition[] = [
|
||||
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
|
||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('气站管理员'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('gas_account'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), f('gas_basic_identity', { label: '气站', listLabel: '气站名称', type: 'identity', relation: '/gas_basic', displayRelationLabel: true, emptyText: '平台直属', placeholder: '请选择气站,留空表示平台直属' }), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
|
||||
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('配送点管理员'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), f('gas_basic_identity', { label: '所属气站', type: 'identity', relation: '/gas_basic' }), f('delivery_basic_identity', { label: '所属配送点', type: 'identity', relation: '/delivery_basic', relationLinkage: { parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', filterKey: 'gas_basic_identities', backfillParent: true } }), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||
define('delivery_account', '配送点账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), fixedAdminRole('delivery_account'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: resourceSearchEnumOptions('staff_account', 'role_code') }), f('gas_basic_identity', { label: '所属气站', type: 'identity', relation: '/gas_basic' }), f('delivery_basic_identity', { label: '所属配送点', type: 'identity', relation: '/delivery_basic', relationLinkage: { parentKey: 'gas_basic_identity', optionParentKey: 'gas_basic_identity', filterKey: 'gas_basic_identities', backfillParent: true } }), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
|
||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: 'context', lockPrefilled: true, showIdentityCopy: true } }), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
|
||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true, { listLabel: '用户账户', listRelationNameOnly: true }), f('address', { required: true, emptyText: '未填写', placeholder: '未填写' }), f('longitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('latitude', { displayPrecision: 6, emptyText: '未填写', placeholder: '未填写' }), f('is_default')]),
|
||||
@@ -369,7 +377,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
{ name: '修改智能气阀状态', resource: '/product_info/:identity/lifecycle', method: 'PATCH', fields: [f('product_status', { required: true, type: 'select', options: [{ label: '待处理', value: 10 }, { label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '维修中', value: 31 }, { label: '已报废', value: 27 }] })] },
|
||||
{ name: '变更智能气阀归属', resource: '/product_info/:identity', method: 'PUT', fields: [relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('action', { required: true, type: 'select', options: [{ label: '入库', value: 'warehouse' }, { label: '分配', value: 'assigned' }, { label: '归还', value: 'returned' }, { label: '人工调整', value: 'manual' }] }), f('reason', { required: true }), f('remark')] },
|
||||
]),
|
||||
define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: [{ label: '待处理', value: 'pending' }, { label: '通过', value: 'passed' }, { label: '未通过', value: 'failed' }] }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]),
|
||||
define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: resourceSearchEnumOptions('product_repair', 'result') }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]),
|
||||
define('product_owner', '智能气阀归属记录', 'readonly', []),
|
||||
|
||||
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
|
||||
@@ -381,7 +389,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
||||
]),
|
||||
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
|
||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: resourceSearchEnumOptions('gasorder_basic', 'creator_type') }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true, { staffRelation: { roles: ['delivery'], enabledOnly: true, workStatus: 'on_duty' } }), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
|
||||
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } },
|
||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
|
||||
@@ -453,7 +461,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
||||
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
||||
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true, unknownValueLabel: '未知角色' }), f('phone')]),
|
||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: [{ label: '脱敏坐标', value: 'standard' }, { label: '精确坐标', value: 'precise' }] })], 'list', [
|
||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('location_scope', { required: true, type: 'select', options: resourceSearchEnumOptions('platform_role', 'location_scope') })], 'list', [
|
||||
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
||||
]),
|
||||
define('platform_menu', '平台菜单', 'readonly', [], 'tree'),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,9 +14,9 @@
|
||||
请从详情页中的专用操作推进流程,操作结果会按服务端状态校验并保留审计记录。
|
||||
</a-alert>
|
||||
<div class="list-toolbar">
|
||||
<a-form :model="filters" layout="inline" class="filters" @submit="search">
|
||||
<a-form-item label="关键字">
|
||||
<a-input v-model="filters.keyword" allow-clear placeholder="关键字段模糊搜索" />
|
||||
<a-form v-if="searchEnabled" :model="filters" layout="inline" class="filters" @submit="search">
|
||||
<a-form-item label="模糊搜索">
|
||||
<a-input v-model="filters.keyword" allow-clear :placeholder="searchPlaceholder" />
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
@@ -185,6 +185,7 @@ import {
|
||||
protectedListAvatarDisplayName,
|
||||
} from './protected-list-avatar-loader';
|
||||
import { useResourceListExtras } from './use-resource-list-extras';
|
||||
import { useResourceListSearch } from './use-resource-list-search';
|
||||
|
||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||
const route = useRoute();
|
||||
@@ -196,9 +197,16 @@ const page = ref(Math.max(1, Number(route.query.page) || 1));
|
||||
const pageSize = 50;
|
||||
const total = ref(0);
|
||||
const list = ref<ResourceRow[]>([]);
|
||||
const filters = reactive({
|
||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||
});
|
||||
const {
|
||||
clearUnsupportedKeyword,
|
||||
displayFields,
|
||||
filters,
|
||||
requestKeyword,
|
||||
resetSearchState,
|
||||
searchEnabled,
|
||||
searchPlaceholder,
|
||||
syncQuery,
|
||||
} = useResourceListSearch(definitionRef, page);
|
||||
const relations = useResourceRelations(() => ({
|
||||
staffType: String(route.query.staff_type ?? route.meta.staffType ?? ''),
|
||||
}));
|
||||
@@ -244,20 +252,6 @@ const canArchive = computed(
|
||||
props.definition.canArchive &&
|
||||
(!requiresRoot.value || userStore.role === 'root'),
|
||||
);
|
||||
const displayFields = computed(() =>
|
||||
props.definition.fields
|
||||
.filter((field) => field.key !== 'identity' && field.type !== 'password')
|
||||
.filter(
|
||||
(field) =>
|
||||
!(
|
||||
props.definition.name === 'gas_basic' &&
|
||||
['credit_code', 'address', 'longitude', 'latitude'].includes(
|
||||
field.key,
|
||||
)
|
||||
),
|
||||
)
|
||||
.slice(0, 6),
|
||||
);
|
||||
const listTitle = computed(() => {
|
||||
const owner = String(route.query.owner_name ?? '');
|
||||
return owner
|
||||
@@ -280,7 +274,8 @@ async function load() {
|
||||
if (props.definition.name === 'delivery_account')
|
||||
serverFilters.delivery_basic_identities = managedOwnerIdentity.value;
|
||||
}
|
||||
if (filters.keyword.trim()) serverFilters.keyword = filters.keyword.trim();
|
||||
const keyword = requestKeyword();
|
||||
if (keyword) serverFilters.keyword = keyword;
|
||||
const result = await resourceApi.list<ResourceRow>(
|
||||
props.definition.resource,
|
||||
page.value,
|
||||
@@ -423,23 +418,6 @@ function confirmArchive(row: ResourceRow) {
|
||||
});
|
||||
}
|
||||
|
||||
async function syncQuery() {
|
||||
const query: Record<string, string> = {};
|
||||
for (const key of [
|
||||
'owner_identity',
|
||||
'owner_name',
|
||||
'relation_key',
|
||||
'staff_type',
|
||||
'return_to',
|
||||
]) {
|
||||
const value = route.query[key];
|
||||
if (typeof value === 'string' && value) query[key] = value;
|
||||
}
|
||||
if (page.value > 1) query.page = String(page.value);
|
||||
if (filters.keyword.trim()) query.keyword = filters.keyword.trim();
|
||||
await router.replace({ query });
|
||||
}
|
||||
|
||||
async function search() {
|
||||
page.value = 1;
|
||||
await syncQuery();
|
||||
@@ -483,6 +461,7 @@ async function loadFieldOptions() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await clearUnsupportedKeyword();
|
||||
await Promise.all([
|
||||
relations.preload(displayFields.value),
|
||||
loadFieldOptions(),
|
||||
@@ -493,8 +472,7 @@ onBeforeUnmount(avatarLoader.reset);
|
||||
watch(
|
||||
() => props.definition.resource,
|
||||
async () => {
|
||||
page.value = 1;
|
||||
filters.keyword = '';
|
||||
await resetSearchState();
|
||||
await Promise.all([
|
||||
relations.preload(displayFields.value),
|
||||
loadFieldOptions(),
|
||||
|
||||
@@ -4,9 +4,27 @@
|
||||
*/
|
||||
import { optionLabel } from '@/api/resource-display';
|
||||
import type { ResourceRow } from '@/api/resource-page-rules';
|
||||
import type { ResourceField } from '@/api/resources';
|
||||
import type { ResourceField, ResourceUiDefinition } from '@/api/resources';
|
||||
import { isProtectedListAvatarField } from './protected-list-avatar-loader';
|
||||
|
||||
/** 返回标准列表实际渲染的业务字段,搜索提示与表格共同复用该规则。 */
|
||||
export function resourceListDisplayFields(
|
||||
definition: ResourceUiDefinition,
|
||||
): ResourceField[] {
|
||||
return definition.fields
|
||||
.filter((field) => field.key !== 'identity' && field.type !== 'password')
|
||||
.filter(
|
||||
(field) =>
|
||||
!(
|
||||
definition.name === 'gas_basic' &&
|
||||
['credit_code', 'address', 'longitude', 'latitude'].includes(
|
||||
field.key,
|
||||
)
|
||||
),
|
||||
)
|
||||
.slice(0, 6);
|
||||
}
|
||||
|
||||
/** 读取关系列表字段的完整唯一标识。 */
|
||||
export function identityFieldValue(field: ResourceField, row: ResourceRow) {
|
||||
return String(row[field.key] ?? row[`${field.key}_masked`] ?? '');
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 功能描述:管理标准资源列表的可搜索字段、关键字状态和 URL 查询参数。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import { computed, reactive, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { ResourceUiDefinition } from '@/api/resources';
|
||||
import { resourceListDisplayFields } from './resource-list-field-display';
|
||||
|
||||
/**
|
||||
* 创建资源列表搜索状态。
|
||||
* 参数:definition 为当前资源定义,page 为当前页码。
|
||||
* 返回值:实际显示字段、搜索提示、请求关键字及 URL 同步方法。
|
||||
*/
|
||||
export function useResourceListSearch(
|
||||
definition: Ref<ResourceUiDefinition>,
|
||||
page: Ref<number>,
|
||||
) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const filters = reactive({
|
||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||
});
|
||||
const displayFields = computed(() =>
|
||||
resourceListDisplayFields(definition.value),
|
||||
);
|
||||
const searchableFields = computed(() => {
|
||||
const searchableKeys = new Set(
|
||||
definition.value.searchFields.map((field) => field.key),
|
||||
);
|
||||
return displayFields.value.filter((field) => searchableKeys.has(field.key));
|
||||
});
|
||||
const searchEnabled = computed(() => searchableFields.value.length > 0);
|
||||
const searchPlaceholder = computed(
|
||||
() =>
|
||||
`可搜索:${searchableFields.value
|
||||
.map((field) => field.listLabel ?? field.label)
|
||||
.join('、')}`,
|
||||
);
|
||||
|
||||
/** 仅在当前资源支持搜索时返回去除首尾空格的关键字。 */
|
||||
function requestKeyword() {
|
||||
return searchEnabled.value ? filters.keyword.trim() : '';
|
||||
}
|
||||
|
||||
/** 将受支持的搜索状态写入 URL,并移除无效或陈旧的 keyword。 */
|
||||
async function syncQuery() {
|
||||
const query: Record<string, string> = {};
|
||||
for (const key of [
|
||||
'owner_identity',
|
||||
'owner_name',
|
||||
'relation_key',
|
||||
'staff_type',
|
||||
'return_to',
|
||||
]) {
|
||||
const value = route.query[key];
|
||||
if (typeof value === 'string' && value) query[key] = value;
|
||||
}
|
||||
if (page.value > 1) query.page = String(page.value);
|
||||
const keyword = requestKeyword();
|
||||
if (keyword) query.keyword = keyword;
|
||||
await router.replace({ query });
|
||||
}
|
||||
|
||||
/** 首次进入不支持搜索的页面时清除地址栏遗留关键字。 */
|
||||
async function clearUnsupportedKeyword() {
|
||||
if (searchEnabled.value || !route.query.keyword) return;
|
||||
filters.keyword = '';
|
||||
await syncQuery();
|
||||
}
|
||||
|
||||
/** 切换资源时复位搜索状态,并同步移除旧资源关键字。 */
|
||||
async function resetSearchState() {
|
||||
page.value = 1;
|
||||
filters.keyword = '';
|
||||
if (route.query.keyword) await syncQuery();
|
||||
}
|
||||
|
||||
return {
|
||||
clearUnsupportedKeyword,
|
||||
displayFields,
|
||||
filters,
|
||||
requestKeyword,
|
||||
resetSearchState,
|
||||
searchEnabled,
|
||||
searchPlaceholder,
|
||||
syncQuery,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user