Files
platforms/frontend/platform_admin/src/api/resource-page-rules.ts
czl231 27733b313e 固定生产商管理员角色
新建生产商时由服务端固定写入 admin,编辑时忽略并禁止修改角色。平台页面将 admin 只读显示为‘生产商管理员’,历史未知角色保持原值,不修改数据库或历史数据。补充后端回归测试、操作日志和项目文档。
2026-08-13 02:07:55 +08:00

324 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 功能:定义标准资源在新建、详情和编辑页面中的字段能力与动态编辑限制。
* 版本v1.0.0
*/
import type { ResourceField, ResourceUiDefinition } from './resources';
export type RecordPageMode = 'create' | 'detail' | 'edit';
export type ResourceRow = Record<string, unknown>;
export type ResourcePageContext = {
role: string;
accountIdentity: string;
};
type ResourcePageRule = {
createHiddenKeys?: string[];
editKeys: string[];
editOptionalKeys?: string[];
editSubmitOnlyKeys?: string[];
accountSummary?: boolean;
};
/** 每个可编辑资源都显式声明更新字段,禁止把创建字段直接提交给更新接口。 */
const pageRules: Record<string, ResourcePageRule> = {
gas_basic: {
editKeys: [
'name',
'credit_code',
'principal',
'address',
'longitude',
'latitude',
],
},
gas_account: {
editKeys: ['display_name', 'gas_basic_identity'],
accountSummary: true,
},
delivery_basic: {
editKeys: ['name', 'gas_basic_identity', 'principal', 'address'],
},
delivery_account: {
editKeys: ['display_name', 'delivery_basic_identity'],
accountSummary: true,
},
staff_account: {
editKeys: [
'name',
'phone',
'avatar',
'role_code',
'gas_basic_identity',
'delivery_basic_identity',
'work_status',
],
accountSummary: true,
},
staff_credential: {
editKeys: ['credential_type', 'credential_no', 'expired_at'],
editSubmitOnlyKeys: ['staff_account_identity'],
},
user_account: {
editKeys: ['name', 'phone', 'avatar', 'real_name'],
accountSummary: true,
},
user_address: {
editKeys: ['address', 'longitude', 'latitude', 'is_default'],
editSubmitOnlyKeys: ['user_account_identity'],
},
user_service_relation: {
editKeys: [
'gas_basic_identity',
'delivery_basic_identity',
'staff_account_identity',
],
editSubmitOnlyKeys: ['user_account_identity'],
},
producer_account: {
editKeys: [
'name',
'credit_code',
'principal',
'phone',
'address',
'password',
'display_name',
'remark',
],
editOptionalKeys: ['password'],
accountSummary: true,
},
product_type: { editKeys: ['code', 'name'] },
product_warehouse: {
editKeys: ['code', 'name', 'address', 'manager', 'phone'],
},
product_info: {
editKeys: [
'name',
'producer_account_identity',
'product_type_identity',
'params',
'produced_at',
],
},
product_repair: {
editKeys: [
'repair_no',
'repair_type',
'started_at',
'completed_at',
'result',
'target_product_status',
'content',
'operator',
'remark',
],
editSubmitOnlyKeys: ['product_info_identity'],
},
gasorder_contract: {
editKeys: [
'delivery_basic_identity',
'title',
'terms',
'file_uri',
'default_delivery_fee',
'signed_at',
'effective_at',
'expired_at',
],
},
ec_product: {
editKeys: [
'ec_category_identity',
'product_code',
'name',
'price_amount',
'stock_quantity',
],
},
ec_product_attribute: {
editKeys: ['ec_product_identity', 'name', 'value', 'sort_no'],
},
ec_product_image: {
editKeys: ['ec_product_identity', 'image_uri', 'sort_no', 'is_cover'],
},
fin_settlement: {
editKeys: [
'settlement_no',
'subject_type',
'subject_identity',
'period_start',
'period_end',
],
},
cms_content: {
editKeys: ['content_type', 'title', 'body', 'version_no', 'publish_status'],
},
cs_ticket: {
editKeys: ['user_account_identity', 'ticket_no', 'category', 'priority'],
},
platform_account: {
editKeys: ['display_name', 'avatar', 'platform_role_code', 'phone'],
accountSummary: true,
},
platform_role: { editKeys: ['name', 'location_scope'] },
};
const ownerKeys: Record<string, string[]> = {
staff_credential: ['staff_account_identity'],
user_address: ['user_account_identity'],
user_service_relation: ['user_account_identity'],
product_info: [
'warehouse_identity',
'gas_basic_identity',
'delivery_basic_identity',
'user_account_identity',
],
product_repair: ['product_info_identity'],
gasorder_contract: ['user_account_identity', 'gas_basic_identity'],
};
/** 返回当前模式需要展示的字段;编辑页会保留不可变字段但将其设为只读。 */
export function pageFields(
definition: ResourceUiDefinition,
mode: Exclude<RecordPageMode, 'detail'>,
row: ResourceRow,
context: ResourcePageContext,
) {
const rule = pageRules[definition.name];
if (mode === 'create') {
const hidden = new Set(rule?.createHiddenKeys ?? []);
return definition.fields.filter((field) => !hidden.has(field.key));
}
let editableKeys =
rule?.editKeys ?? definition.fields.map((field) => field.key);
if (definition.name === 'product_repair' && row.result !== 'pending') {
editableKeys = ['remark'];
}
if (definition.name === 'platform_account' && context.role !== 'root') {
editableKeys = editableKeys.filter((key) => key !== 'platform_role_code');
}
const visibleKeys = new Set([
...editableKeys,
...(ownerKeys[definition.name] ?? []),
...definition.fields
.filter((field) =>
[
'username',
'code',
'delivery_code',
'producer_code',
'contract_no',
'role_code',
].includes(field.key),
)
.map((field) => field.key),
]);
return definition.fields.filter(
(field) =>
visibleKeys.has(field.key) &&
(field.type !== 'password' || editableKeys.includes(field.key)),
);
}
/** 返回编辑请求允许提交的字段。 */
export function editableFields(
definition: ResourceUiDefinition,
row: ResourceRow,
context: ResourcePageContext,
): ResourceField[] {
const rule = pageRules[definition.name];
let keys = rule?.editKeys ?? definition.fields.map((field) => field.key);
if (definition.name === 'product_repair' && row.result !== 'pending')
keys = ['remark'];
if (definition.name === 'platform_account' && context.role !== 'root') {
keys = keys.filter((key) => key !== 'platform_role_code');
}
const allowed = new Set(keys);
return definition.fields.filter((field) => allowed.has(field.key));
}
/** 返回更新协议需要提交的字段,其中只读归属字段仅原样回传。 */
export function updatePayloadFields(
definition: ResourceUiDefinition,
row: ResourceRow,
context: ResourcePageContext,
) {
const keys = new Set([
...editableFields(definition, row, context).map((field) => field.key),
...(pageRules[definition.name]?.editSubmitOnlyKeys ?? []),
]);
return definition.fields.filter((field) => keys.has(field.key));
}
/** 判断编辑字段在当前模式是否必填。 */
export function fieldRequired(
definition: ResourceUiDefinition,
field: ResourceField,
mode: Exclude<RecordPageMode, 'detail'>,
) {
if (mode === 'create') return Boolean(field.required);
const optional = new Set(pageRules[definition.name]?.editOptionalKeys ?? []);
return Boolean(field.required && !optional.has(field.key));
}
/** 判断资源是否使用账户头像与账号摘要。 */
export function usesAccountSummary(definition: ResourceUiDefinition) {
return Boolean(pageRules[definition.name]?.accountSummary);
}
/** 返回直达新建页时的阻止原因,空字符串代表允许新建。 */
export function createBlockedReason(
definition: ResourceUiDefinition,
context: ResourcePageContext,
) {
if (!definition.canCreate) return '当前资源不支持新建';
if (
['platform_account', 'platform_role'].includes(definition.name) &&
context.role !== 'root'
) {
return '只有 root 可以新建平台账户或平台角色';
}
return '';
}
/** 返回直达编辑页时的阻止原因,空字符串代表允许编辑。 */
export function editBlockedReason(
definition: ResourceUiDefinition,
row: ResourceRow,
context: ResourcePageContext,
) {
if (!definition.canEdit) return '当前资源不支持编辑';
if (definition.name === 'gasorder_contract') {
if (Number(row.contract_status) !== 0)
return '只有草稿状态的合同可以编辑';
if (![0, 1].includes(Number(row.status)))
return '停用、已归档或已冻结的合同不可编辑';
}
if (definition.name === 'platform_role') {
if (context.role !== 'root') return '只有 root 可以编辑平台角色';
if (row.is_system === true) return '系统角色不允许编辑';
}
if (
definition.name === 'platform_account' &&
context.role !== 'root' &&
String(row.identity ?? '') !== context.accountIdentity
) {
return '普通平台管理员只能编辑自己的账户资料';
}
return '';
}
/** 校验每个列表型可编辑资源都有显式更新字段规则。 */
export function assertResourcePageRules(definitions: ResourceUiDefinition[]) {
const missing = definitions
.filter(
(definition) => definition.pageKind === 'list' && definition.canEdit,
)
.filter((definition) => !pageRules[definition.name])
.map((definition) => definition.name);
if (missing.length)
throw new Error(`资源缺少编辑字段规则:${missing.join('、')}`);
}