feat: 标准资源使用独立页面并优化详情布局
This commit is contained in:
@@ -1,13 +1,28 @@
|
||||
/** 平台总后台的共享 HTTP 客户端,统一处理响应体和 JWT 请求头。 */
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:12426/heqi/platform/v1';
|
||||
const apiBaseURL =
|
||||
import.meta.env.VITE_API_BASE_URL ||
|
||||
'http://localhost:12426/heqi/platform/v1';
|
||||
|
||||
export const tokenStorageKey = 'token';
|
||||
|
||||
export type PageResult<T> = { total: number; list: T[] };
|
||||
|
||||
/** 保留 HTTP 状态,供独立页面区分无权限、不存在和普通请求错误。 */
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
// 将后端 SDK 的通用英文错误转换为面向用户的中文提示。
|
||||
const API_ERROR_MESSAGES: Record<string, string> = {
|
||||
'Invalid Argument': '请求参数不正确,请检查填写内容',
|
||||
'Record Not Found': '记录不存在',
|
||||
'Permission Denied': '无权访问该记录',
|
||||
};
|
||||
|
||||
// 优先保留后端提供的具体中文信息,仅翻译已知的通用英文错误。
|
||||
@@ -31,9 +46,16 @@ export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
} catch {
|
||||
throw new Error('无法连接服务器,请确认服务已启动');
|
||||
}
|
||||
const payload = (await response.json()) as { code?: number; message?: string; details?: T };
|
||||
const payload = (await response.json()) as {
|
||||
code?: number;
|
||||
message?: string;
|
||||
details?: T;
|
||||
};
|
||||
if (!response.ok || payload.code !== 0) {
|
||||
throw new Error(localizeApiErrorMessage(payload.message));
|
||||
throw new ApiError(
|
||||
localizeApiErrorMessage(payload.message),
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
return payload.details as T;
|
||||
}
|
||||
|
||||
191
frontend/platform_admin/src/api/resource-display.ts
Normal file
191
frontend/platform_admin/src/api/resource-display.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 功能:统一资源列表与详情页面的字段名称、关系、金额、状态和时间展示。
|
||||
* 版本:v1.1.0
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
import type { ResourceField, ResourceUiDefinition } from './resources';
|
||||
import type { RecordPageMode, ResourceRow } from './resource-page-rules';
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
identity: '唯一标识',
|
||||
id: 'ID',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
deleted_at: '删除时间',
|
||||
DeletedAt: '删除时间',
|
||||
status: '状态',
|
||||
version: '版本',
|
||||
items: '订单明细',
|
||||
assignments: '分配记录',
|
||||
statuses: '状态记录',
|
||||
tracks: '运行轨迹',
|
||||
confirmations: '确认记录',
|
||||
payments: '支付记录',
|
||||
revisions: '修订记录',
|
||||
products: '合同气瓶',
|
||||
order: '订单',
|
||||
contract: '合同',
|
||||
wallet: '钱包',
|
||||
is_system: '系统内置',
|
||||
};
|
||||
|
||||
const amountKeys = new Set([
|
||||
'amount',
|
||||
'unit_price',
|
||||
'balance',
|
||||
'withdrawal_balance',
|
||||
'default_delivery_fee',
|
||||
'discount_amount',
|
||||
'total_amount',
|
||||
'delivery_fee',
|
||||
'price_amount',
|
||||
'sale_amount',
|
||||
'difference_amount',
|
||||
'fee',
|
||||
]);
|
||||
|
||||
/** 返回资源独立页面的模式名称。 */
|
||||
export function recordPageModeLabel(mode: RecordPageMode) {
|
||||
return { create: '新建', detail: '详情', edit: '编辑' }[mode];
|
||||
}
|
||||
|
||||
/** 返回资源独立页面的错误标题。 */
|
||||
export function recordPageErrorTitle(status: '403' | '404' | 'error') {
|
||||
return {
|
||||
'403': '无法执行此操作',
|
||||
'404': '记录不存在',
|
||||
error: '页面加载失败',
|
||||
}[status];
|
||||
}
|
||||
|
||||
/** 返回聚合详情中的主记录。 */
|
||||
export function primaryRecord(detail: ResourceRow): ResourceRow {
|
||||
if (detail.order && typeof detail.order === 'object')
|
||||
return detail.order as ResourceRow;
|
||||
if (detail.contract && typeof detail.contract === 'object')
|
||||
return detail.contract as ResourceRow;
|
||||
return detail;
|
||||
}
|
||||
|
||||
/** 返回字段中文名称,脱敏字段沿用原字段名称。 */
|
||||
export function resourceFieldLabel(
|
||||
definition: ResourceUiDefinition,
|
||||
key: string,
|
||||
) {
|
||||
const normalized = key.endsWith('_masked') ? key.slice(0, -7) : key;
|
||||
return (
|
||||
definition.fields.find((field) => field.key === normalized)?.label ??
|
||||
aliases[key] ??
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
/** 将标准实体状态转换为稳定的中文展示。 */
|
||||
export function recordStatusLabel(status: number) {
|
||||
return (
|
||||
{ 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ??
|
||||
`未知(${status})`
|
||||
);
|
||||
}
|
||||
|
||||
export function recordStatusColor(status: number) {
|
||||
return (
|
||||
{ 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ??
|
||||
'gray'
|
||||
);
|
||||
}
|
||||
|
||||
/** 选择关系记录的首选可读名称。 */
|
||||
export function optionLabel(option: ResourceRow) {
|
||||
return String(
|
||||
option.name ??
|
||||
option.title ??
|
||||
option.display_name ??
|
||||
option.code ??
|
||||
option.username ??
|
||||
option.contract_no ??
|
||||
option.order_no ??
|
||||
option.identity ??
|
||||
'-',
|
||||
);
|
||||
}
|
||||
|
||||
function relationLabel(
|
||||
field: ResourceField,
|
||||
identity: string,
|
||||
relationOptions: Record<string, ResourceRow[]>,
|
||||
) {
|
||||
const match = (relationOptions[field.relation ?? ''] ?? []).find(
|
||||
(option) => String(option.identity) === identity,
|
||||
);
|
||||
if (!match) return identity;
|
||||
return field.displayRelationLabel
|
||||
? optionLabel(match)
|
||||
: `${optionLabel(match)} · ${identity}`;
|
||||
}
|
||||
|
||||
/** 格式化不依赖字段定义的通用值。 */
|
||||
export function displayRawValue(key: string, value: unknown) {
|
||||
if (value == null || value === '') return '-';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (key === 'status') return recordStatusLabel(Number(value));
|
||||
if (
|
||||
amountKeys.has(key) ||
|
||||
key.endsWith('_amount') ||
|
||||
key.endsWith('_balance_after')
|
||||
) {
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount)
|
||||
? `¥${(amount / 100).toFixed(2)}`
|
||||
: String(value);
|
||||
}
|
||||
if (key.endsWith('_at') || ['created_at', 'updated_at'].includes(key)) {
|
||||
const date = dayjs(String(value));
|
||||
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : String(value);
|
||||
}
|
||||
if (
|
||||
['deleted_at', 'DeletedAt'].includes(key) &&
|
||||
typeof value === 'object' &&
|
||||
value &&
|
||||
'Time' in value
|
||||
) {
|
||||
const date = dayjs(String((value as { Time?: unknown }).Time ?? ''));
|
||||
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||
}
|
||||
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** 按字段选项和关系配置格式化值。 */
|
||||
export function displayResourceField(
|
||||
field: ResourceField,
|
||||
row: ResourceRow,
|
||||
relationOptions: Record<string, ResourceRow[]>,
|
||||
) {
|
||||
const value = row[field.key] ?? row[`${field.key}_masked`];
|
||||
if (value == null || value === '') return field.emptyText ?? '-';
|
||||
const option = field.options?.find(
|
||||
(item) => String(item.value) === String(value),
|
||||
);
|
||||
if (option) return option.label;
|
||||
if (field.type === 'identity' && typeof value === 'string') {
|
||||
return relationLabel(field, value, relationOptions);
|
||||
}
|
||||
if (field.type === 'identity-list' && Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => relationLabel(field, String(item), relationOptions))
|
||||
.join('、');
|
||||
}
|
||||
return displayRawValue(field.key, value);
|
||||
}
|
||||
|
||||
/** 判断软删除结构是否为空。 */
|
||||
export function isEmptyDeletedAt(value: unknown) {
|
||||
if (value == null || value === '') return true;
|
||||
return Boolean(
|
||||
typeof value === 'object' &&
|
||||
value &&
|
||||
'Valid' in value &&
|
||||
!(value as { Valid?: boolean }).Valid,
|
||||
);
|
||||
}
|
||||
60
frontend/platform_admin/src/api/resource-navigation.ts
Normal file
60
frontend/platform_admin/src/api/resource-navigation.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 功能:构造标准资源列表、新建、详情和编辑页面之间的安全导航地址。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import type { RouteLocationRaw, Router } from 'vue-router';
|
||||
import type { ResourceRow } from './resource-page-rules';
|
||||
|
||||
export type RecordNavigationMode = 'create' | 'detail' | 'edit';
|
||||
|
||||
/** 只接受站内绝对路径,避免 return_to 被利用为外部跳转。 */
|
||||
export function safeReturnPath(value: unknown) {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!value.startsWith('/') ||
|
||||
value.startsWith('//') ||
|
||||
value.includes('\\') ||
|
||||
/[\r\n]/.test(value)
|
||||
)
|
||||
return '';
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 返回共享资源页的命名路由位置。 */
|
||||
export function recordRouteLocation(
|
||||
listRouteName: string,
|
||||
mode: RecordNavigationMode,
|
||||
identity: string,
|
||||
returnTo: string,
|
||||
): RouteLocationRaw {
|
||||
return {
|
||||
name: `${listRouteName}-${mode}`,
|
||||
...(mode === 'create' ? {} : { params: { identity } }),
|
||||
query: returnTo ? { return_to: returnTo } : {},
|
||||
};
|
||||
}
|
||||
|
||||
/** 新建工作人员后按照实际角色进入对应详情页。 */
|
||||
export function createdRecordListRoute(
|
||||
resourceName: string,
|
||||
fallback: string,
|
||||
row: ResourceRow,
|
||||
) {
|
||||
if (resourceName !== 'staff_account') return fallback;
|
||||
return (
|
||||
{
|
||||
installer: 'staff-installers',
|
||||
delivery: 'staff-delivery',
|
||||
operations: 'staff-operations',
|
||||
}[String(row.role_code ?? '')] ?? fallback
|
||||
);
|
||||
}
|
||||
|
||||
/** 返回原列表;来源缺失时使用路由名称。 */
|
||||
export function returnToList(
|
||||
router: Router,
|
||||
returnPath: string,
|
||||
listRouteName: string,
|
||||
) {
|
||||
return router.push(returnPath || { name: listRouteName });
|
||||
}
|
||||
325
frontend/platform_admin/src/api/resource-page-rules.ts
Normal file
325
frontend/platform_admin/src/api/resource-page-rules.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 功能:定义标准资源在新建、详情和编辑页面中的字段能力与动态编辑限制。
|
||||
* 版本: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', 'role_code', 'gas_basic_identity'],
|
||||
accountSummary: true,
|
||||
},
|
||||
delivery_basic: {
|
||||
editKeys: ['name', 'gas_basic_identity', 'principal', 'address'],
|
||||
},
|
||||
delivery_account: {
|
||||
createHiddenKeys: ['role_code'],
|
||||
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',
|
||||
'role_code',
|
||||
'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' &&
|
||||
Number(row.contract_status) !== 10
|
||||
) {
|
||||
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('、')}`);
|
||||
}
|
||||
47
frontend/platform_admin/src/api/resource-record-form.ts
Normal file
47
frontend/platform_admin/src/api/resource-record-form.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 功能:初始化和校验标准资源独立页面的表单数据。
|
||||
* 版本:v1.0.0
|
||||
*/
|
||||
import { isMissingField } from './resource-form';
|
||||
import type { ResourceField } from './resources';
|
||||
import type { ResourceRow } from './resource-page-rules';
|
||||
|
||||
/** 使用详情快照初始化表单,并应用来源关系和工作人员类型预填。 */
|
||||
export function resetResourceRecordForm(
|
||||
form: Record<string, any>,
|
||||
fields: ResourceField[],
|
||||
row: ResourceRow,
|
||||
prefill: { relationKey?: string; ownerIdentity?: string; staffType?: string },
|
||||
) {
|
||||
for (const key of Object.keys(form)) delete form[key];
|
||||
for (const field of fields) {
|
||||
const value = row[field.key];
|
||||
form[field.key] =
|
||||
value == null
|
||||
? undefined
|
||||
: field.type === 'money'
|
||||
? Number(value) / 100
|
||||
: value;
|
||||
}
|
||||
if (prefill.relationKey && prefill.ownerIdentity) {
|
||||
form[prefill.relationKey] = prefill.ownerIdentity;
|
||||
}
|
||||
if (prefill.staffType) form.role_code = prefill.staffType;
|
||||
}
|
||||
|
||||
/** 返回表单首个校验错误,空字符串代表可以提交。 */
|
||||
export function validateResourceRecordForm(
|
||||
form: Record<string, any>,
|
||||
fields: ResourceField[],
|
||||
requiredKeys: string[],
|
||||
) {
|
||||
if (requiredKeys.some((key) => isMissingField(form[key])))
|
||||
return '请填写必填字段';
|
||||
const password = fields.find(
|
||||
(field) => field.type === 'password' && !isMissingField(form[field.key]),
|
||||
);
|
||||
if (password && Array.from(String(form[password.key])).length < 6) {
|
||||
return '密码长度不能少于 6 个字符';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -333,6 +333,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
||||
define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [
|
||||
{ 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_owner', '智能气阀归属记录', 'readonly', []),
|
||||
|
||||
Reference in New Issue
Block a user