重构配送点独立资源页面

This commit is contained in:
czl231
2026-08-22 20:35:57 +08:00
parent da0185d1aa
commit 25254cb773
21 changed files with 1594 additions and 1285 deletions

View File

@@ -0,0 +1,86 @@
/**
* 功能描述:统一配送点资源独立页面的字段名称和值展示。
* 版本v1.0.0。
*/
import dayjs from 'dayjs';
import { resourceFieldLabel, type ResourceUiDefinition } from './resources';
import type { ResourceRow } from './resource-record-form';
const aliases: Record<string, string> = {
id: 'ID',
identity: '系统唯一标识',
created_at: '创建时间',
updated_at: '更新时间',
items: '订单明细',
assignments: '分配记录',
statuses: '状态记录',
tracks: '运行轨迹',
confirmations: '确认记录',
payments: '支付记录',
revisions: '修订记录',
products: '合同气瓶',
};
const moneyKeys = new Set([
'amount', 'unit_price', 'balance', 'withdrawal_balance', 'delivery_fee',
'discount_amount', 'total_amount', 'default_delivery_fee', 'fee',
]);
/** 返回聚合详情中的主记录。 */
export function primaryRecord(detail: ResourceRow): ResourceRow {
for (const key of ['order', 'contract', 'track', 'user']) {
if (detail[key] && typeof detail[key] === 'object') return detail[key] as ResourceRow;
}
return detail;
}
/** 返回字段中文名称。 */
export function detailFieldLabel(key: string) {
return aliases[key] ?? resourceFieldLabel(key);
}
/** 返回通用记录状态名称。 */
export function recordStatusLabel(status: number) {
return { 0: '待审核', 1: '启用', 2: '停用', 3: '已归档', 4: '已冻结' }[status] ?? '未知状态';
}
/** 返回通用记录状态颜色。 */
export function recordStatusColor(status: number) {
return { 0: 'orange', 1: 'green', 2: 'red', 3: 'gray', 4: 'purple' }[status] ?? 'gray';
}
/** 将接口字段值转换为中文可读内容。 */
export function displayResourceValue(
definition: ResourceUiDefinition,
key: string,
value: unknown,
) {
if (value == null || value === '') return '-';
const field = definition.fields.find((item) => item.key === key);
const option = field?.options?.find((item) => String(item.value) === String(value));
if (option) return option.label;
if (key === 'role_code') {
return { delivery: '配送人员', installer: '安装人员', operations: '运维人员' }[String(value)]
?? String(value);
}
if (typeof value === 'boolean') return value ? '是' : '否';
if (key === 'status') return recordStatusLabel(Number(value));
if (moneyKeys.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') || key === 'created_at' || key === 'updated_at') {
const date = dayjs(String(value));
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : String(value);
}
if (typeof value === 'object') return JSON.stringify(value, null, 2);
return String(value);
}
/** 返回合同编辑阻止原因。 */
export function recordEditReason(definition: ResourceUiDefinition, row: ResourceRow) {
if (definition.name !== 'gasorder_contract') return '';
if (Number(row.contract_status) !== 0) return '只有草稿状态的合同可以编辑';
if (![0, 1].includes(Number(row.status))) return '停用、已归档或已冻结的合同不可编辑';
return '';
}

View File

@@ -0,0 +1,39 @@
/**
* 功能描述:构造配送点资源列表与独立记录页之间的安全导航地址。
* 版本v1.0.0。
*/
import type { RouteLocationRaw, Router } from 'vue-router';
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 returnToList(router: Router, returnPath: string, listRouteName: string) {
return router.push(returnPath || { name: listRouteName });
}

View File

@@ -0,0 +1,72 @@
/**
* 功能描述:初始化与校验配送点资源独立页面表单。
* 版本v1.0.0。
*/
import { isMissingField } from './resource-form';
import type { ResourceField } from './resources';
export type ResourceRow = Record<string, unknown>;
const editableFieldKeys: Record<string, Set<string>> = {
staff_account: new Set(['name', 'phone', 'avatar', 'work_status']),
staff_credential: new Set(['staff_account_identity', 'credential_type', 'credential_no', 'expired_at']),
user_account: new Set(['name', 'phone', 'avatar', 'real_name']),
user_address: new Set(['user_account_identity', 'address', 'longitude', 'latitude', 'is_default']),
gasorder_contract: new Set([
'title', 'terms', 'file_uri', 'default_delivery_fee',
'signed_at', 'effective_at', 'expired_at',
]),
};
/** 按后端更新契约返回当前页面允许提交的字段,避免展示无效的只读字段。 */
export function recordFormFields(
resourceName: string,
fields: ResourceField[],
mode: 'create' | 'edit',
) {
const available = fields.filter((field) => field.key !== 'identity');
if (mode === 'create') return available;
const editable = editableFieldKeys[resourceName];
return editable
? available.filter((field) => editable.has(field.key))
: available.filter((field) => field.type !== 'password');
}
/** 使用详情快照初始化表单,并应用来源关系预填。 */
export function resetResourceRecordForm(
form: Record<string, any>,
fields: ResourceField[],
row: ResourceRow,
prefill: { relationKey?: string; ownerIdentity?: 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
? field.defaultValue
: field.type === 'money'
? Number(value) / 100
: value;
}
if (prefill.relationKey && prefill.ownerIdentity)
form[prefill.relationKey] = prefill.ownerIdentity;
}
/** 返回表单首个校验错误;空字符串表示可以提交。 */
export function validateResourceRecordForm(
form: Record<string, any>,
fields: ResourceField[],
mode: 'create' | 'edit',
) {
const required = fields.filter(
(field) => field.required && !(mode === 'edit' && field.type === 'password'),
);
if (required.some((field) => isMissingField(form[field.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 '';
}

View File

@@ -1,3 +1,7 @@
/**
* 功能描述:定义配送点后台资源能力、字段契约和受控业务动作。
* 版本v1.1.0。
*/
export type ResourceMode =
| 'writable'
| 'readonly'
@@ -234,6 +238,12 @@ const fieldLabels: Record<string, string> = {
withdrawable: '计入可提现余额',
};
/** 返回资源字段的中文名称,未知字段保留原键用于排障。 */
export function resourceFieldLabel(key: string) {
const normalized = key.endsWith('_masked') ? key.slice(0, -7) : key;
return fieldLabels[normalized] ?? normalized;
}
const numbers = new Set([
'sort_no', 'stock_quantity', 'quantity', 'score', 'version_no',
'attempt_no', 'target_product_status',
@@ -525,9 +535,9 @@ const deliveryOverrides: ResourceUiDefinition[] = [
]),
define('wallet_basic', '钱包', 'readonly', []),
define('wallet_bank', '银行卡', 'readonly', []),
define('wallet_payment', '支付记录', 'readonly', []),
define('payment_order', '支付记录', 'readonly', []),
define('wallet_record', '钱包流水', 'readonly', []),
define('wallet_refund', '退款记录', 'readonly', []),
define('payment_refund', '退款记录', 'readonly', []),
define('wallet_recharge', '钱包充值', 'append_only', [f('request_no', { required: true }), f('amount', { required: true }), ...reason, f('remark')]),
define('wallet_apply_cash', '提现申请', 'append_only', [relation('wallet_bank_identity', '/wallet_bank'), f('request_no', { required: true }), f('amount', { required: true }), withdrawalChannelField, f('remark')]),
define('fin_settlement', '结算结果', 'readonly', []),